diff --git a/.claude/rules/openwolf.md b/.claude/rules/openwolf.md deleted file mode 100644 index 9785410..0000000 --- a/.claude/rules/openwolf.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -description: OpenWolf protocol enforcement — active on all files -globs: **/* ---- - -- Check .wolf/anatomy.md before reading any project file -- Check .wolf/cerebrum.md Do-Not-Repeat list before generating code -- After writing or editing files, update .wolf/anatomy.md and append to .wolf/memory.md -- After receiving a user correction, update .wolf/cerebrum.md immediately (Preferences, Learnings, or Do-Not-Repeat) -- LEARN from every interaction: if you discover a convention, user preference, or project pattern, add it to .wolf/cerebrum.md. Low threshold — when in doubt, log it. -- BEFORE fixing any bug or error: read .wolf/buglog.json for known fixes -- AFTER fixing any bug, error, failed test, failed build, or user-reported problem: ALWAYS log to .wolf/buglog.json with error_message, root_cause, fix, and tags -- If you edit a file more than twice in a session, that likely indicates a bug — log it to .wolf/buglog.json -- When the user asks to check/evaluate UI design: run `openwolf designqc` to capture screenshots, then read them from .wolf/designqc-captures/ -- When the user asks to change/pick/migrate UI framework: read .wolf/reframe-frameworks.md, ask decision questions, recommend a framework, then execute with the framework's prompt diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index b6bf5ad..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(dotnet restore:*)", - "Bash(dotnet build:*)", - "Bash(az webapp:*)", - "Bash(kill %1)", - "Bash(curl -s -o /dev/null -w \"%{http_code}\" https://app-talent-ids-dev.azurewebsites.net)", - "Bash(az sql:*)", - "Bash(gh workflow:*)" - ] - }, - "hooks": { - "SessionStart": [ - { - "matcher": "", - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/session-start.js\"", - "timeout": 5 - } - ] - } - ], - "PreToolUse": [ - { - "matcher": "Read", - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-read.js\"", - "timeout": 5 - } - ] - }, - { - "matcher": "Write|Edit|MultiEdit", - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/pre-write.js\"", - "timeout": 5 - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Read", - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/post-read.js\"", - "timeout": 5 - } - ] - }, - { - "matcher": "Write|Edit|MultiEdit", - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js\"", - "timeout": 10 - } - ] - } - ], - "Stop": [ - { - "matcher": "", - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/stop.js\"", - "timeout": 10 - } - ] - } - ] - } -} \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 65ab5d9..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(mkdir:*)", - "Bash(node -e \":*)", - "Bash(git add blogs/series-2-dotnet-api/2.1-dotnet-clean-architecture.md)", - "Bash(git commit -m \"Add brief EasyCaching mention to 2.1 clean architecture article\")", - "Bash(git push)", - "Bash(gh pr:*)", - "Bash(node -e \"console.log\(require\('c:/apps/AngularNetTutotial/node_modules/playwright/package.json'\).version\)\")", - "Bash(node -e \"console.log\(require\('c:/apps/AngularNetTutotial/Tests/AngularNetTutorial-Playwright/node_modules/playwright/package.json'\).version\)\")", - "Bash(npx playwright:*)", - "Bash(npm show:*)", - "Bash(npm install:*)", - "Bash(powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Get-Content 'c:/apps/AngularNetTutotial/Tests/AngularNetTutorial-Playwright/scripts/speak.ps1' | Select-Object -First 10\")", - "Bash(powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"c:/apps/AngularNetTutotial/Tests/AngularNetTutorial-Playwright/scripts/speak.ps1\" -Text \"Test audio.\" -OutputPath \"c:/apps/AngularNetTutotial/Tests/AngularNetTutorial-Playwright/scripts/test.wav\")", - "Bash(node -e \"const p = require\('c:/apps/AngularNetTutotial/node_modules/@playwright/test/package.json'\); console.log\(JSON.stringify\(p.dependencies, null, 2\)\)\")", - "Bash(xargs grep:*)", - "Bash(npx ng:*)", - "Bash(gh pr create --title '6.3 Angular AI Chat Widget: AiService, AiChatComponent, route, and menu' --body ':*)", - "Bash(gh pr create --title '6.3 Angular AI Chat Widget: blog article and Clients submodule ref' --body ':*)", - "mcp__MCP_DOCKER__list_directory", - "mcp__MCP_DOCKER__read_file", - "Bash(gh pr edit 4 --title '6.5 + 6.6 Natural language search and AI response caching' --body ':*)" - ], - "deny": [], - "ask": [] - } -} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index fe1152b..0000000 --- a/.dockerignore +++ /dev/null @@ -1,30 +0,0 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md -!**/.gitignore -!.git/HEAD -!.git/config -!.git/packed-refs -!.git/refs/heads/** \ No newline at end of file diff --git a/.github/agents/playwright-test-generator.agent.md b/.github/agents/playwright-test-generator.agent.md deleted file mode 100644 index fb15e6e..0000000 --- a/.github/agents/playwright-test-generator.agent.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: playwright-test-generator -description: 'Use this agent when you need to create automated browser tests using Playwright Examples: Context: User wants to generate a test for the test plan item. ' -tools: - - search - - playwright-test/browser_click - - playwright-test/browser_drag - - playwright-test/browser_evaluate - - playwright-test/browser_file_upload - - playwright-test/browser_handle_dialog - - playwright-test/browser_hover - - playwright-test/browser_navigate - - playwright-test/browser_press_key - - playwright-test/browser_select_option - - playwright-test/browser_snapshot - - playwright-test/browser_type - - playwright-test/browser_verify_element_visible - - playwright-test/browser_verify_list_visible - - playwright-test/browser_verify_text_visible - - playwright-test/browser_verify_value - - playwright-test/browser_wait_for - - playwright-test/generator_read_log - - playwright-test/generator_setup_page - - playwright-test/generator_write_test -model: Claude Sonnet 4 -mcp-servers: - playwright-test: - type: stdio - command: npx - args: - - playwright - - run-test-mcp-server - tools: - - "*" ---- - -You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. -Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate -application behavior. - -# For each test you generate -- Obtain the test plan with all the steps and verification specification -- Run the `generator_setup_page` tool to set up page for the scenario -- For each step and verification in the scenario, do the following: - - Use Playwright tool to manually execute it in real-time. - - Use the step description as the intent for each Playwright tool call. -- Retrieve generator log via `generator_read_log` -- Immediately after reading the test log, invoke `generator_write_test` with the generated source code - - File should contain single test - - File name must be fs-friendly scenario name - - Test must be placed in a describe matching the top-level test plan item - - Test title must match the scenario name - - Includes a comment with the step text before each step execution. Do not duplicate comments if step requires - multiple actions. - - Always use best practices from the log when generating tests. - - - For following plan: - - ```markdown file=specs/plan.md - ### 1. Adding New Todos - **Seed:** `tests/seed.spec.ts` - - #### 1.1 Add Valid Todo - **Steps:** - 1. Click in the "What needs to be done?" input field - - #### 1.2 Add Multiple Todos - ... - ``` - - Following file is generated: - - ```ts file=add-valid-todo.spec.ts - // spec: specs/plan.md - // seed: tests/seed.spec.ts - - test.describe('Adding New Todos', () => { - test('Add Valid Todo', async { page } => { - // 1. Click in the "What needs to be done?" input field - await page.click(...); - - ... - }); - }); - ``` - diff --git a/.github/agents/playwright-test-healer.agent.md b/.github/agents/playwright-test-healer.agent.md deleted file mode 100644 index 2e04486..0000000 --- a/.github/agents/playwright-test-healer.agent.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: playwright-test-healer -description: Use this agent when you need to debug and fix failing Playwright tests -tools: - - search - - edit - - playwright-test/browser_console_messages - - playwright-test/browser_evaluate - - playwright-test/browser_generate_locator - - playwright-test/browser_network_requests - - playwright-test/browser_snapshot - - playwright-test/test_debug - - playwright-test/test_list - - playwright-test/test_run -model: Claude Sonnet 4 -mcp-servers: - playwright-test: - type: stdio - command: npx - args: - - playwright - - run-test-mcp-server - tools: - - "*" ---- - -You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and -resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix -broken Playwright tests using a methodical approach. - -Your workflow: -1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests -2. **Debug failed tests**: For each failing test run `test_debug`. -3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to: - - Examine the error details - - Capture page snapshot to understand the context - - Analyze selectors, timing issues, or assertion failures -4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining: - - Element selectors that may have changed - - Timing and synchronization issues - - Data dependencies or test environment problems - - Application changes that broke test assumptions -5. **Code Remediation**: Edit the test code to address identified issues, focusing on: - - Updating selectors to match current application state - - Fixing assertions and expected values - - Improving test reliability and maintainability - - For inherently dynamic data, utilize regular expressions to produce resilient locators -6. **Verification**: Restart the test after each fix to validate the changes -7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly - -Key principles: -- Be systematic and thorough in your debugging approach -- Document your findings and reasoning for each fix -- Prefer robust, maintainable solutions over quick hacks -- Use Playwright best practices for reliable test automation -- If multiple errors exist, fix them one at a time and retest -- Provide clear explanations of what was broken and how you fixed it -- You will continue this process until the test runs successfully without any failures or errors. -- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() - so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead - of the expected behavior. -- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test. -- Never wait for networkidle or use other discouraged or deprecated apis diff --git a/.github/agents/playwright-test-planner.agent.md b/.github/agents/playwright-test-planner.agent.md deleted file mode 100644 index 1f2e788..0000000 --- a/.github/agents/playwright-test-planner.agent.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -name: playwright-test-planner -description: Use this agent when you need to create comprehensive test plan for a web application or website -tools: - - search - - playwright-test/browser_click - - playwright-test/browser_close - - playwright-test/browser_console_messages - - playwright-test/browser_drag - - playwright-test/browser_evaluate - - playwright-test/browser_file_upload - - playwright-test/browser_handle_dialog - - playwright-test/browser_hover - - playwright-test/browser_navigate - - playwright-test/browser_navigate_back - - playwright-test/browser_network_requests - - playwright-test/browser_press_key - - playwright-test/browser_run_code - - playwright-test/browser_select_option - - playwright-test/browser_snapshot - - playwright-test/browser_take_screenshot - - playwright-test/browser_type - - playwright-test/browser_wait_for - - playwright-test/planner_setup_page - - playwright-test/planner_save_plan -model: Claude Sonnet 4 -mcp-servers: - playwright-test: - type: stdio - command: npx - args: - - playwright - - run-test-mcp-server - tools: - - "*" ---- - -You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test -scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage -planning. - -You will: - -1. **Navigate and Explore** - - Invoke the `planner_setup_page` tool once to set up page before using any other tools - - Explore the browser snapshot - - Do not take screenshots unless absolutely necessary - - Use `browser_*` tools to navigate and discover interface - - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality - -2. **Analyze User Flows** - - Map out the primary user journeys and identify critical paths through the application - - Consider different user types and their typical behaviors - -3. **Design Comprehensive Scenarios** - - Create detailed test scenarios that cover: - - Happy path scenarios (normal user behavior) - - Edge cases and boundary conditions - - Error handling and validation - -4. **Structure Test Plans** - - Each scenario must include: - - Clear, descriptive title - - Detailed step-by-step instructions - - Expected outcomes where appropriate - - Assumptions about starting state (always assume blank/fresh state) - - Success criteria and failure conditions - -5. **Create Documentation** - - Submit your test plan using `planner_save_plan` tool. - -**Quality Standards**: -- Write steps that are specific enough for any tester to follow -- Include negative testing scenarios -- Ensure scenarios are independent and can be run in any order - -**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and -professional formatting suitable for sharing with development and QA teams. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml deleted file mode 100644 index d9b5b71..0000000 --- a/.github/workflows/copilot-setup-steps.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "Copilot Setup Steps" - -on: - workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml - pull_request: - paths: - - .github/workflows/copilot-setup-steps.yml - -jobs: - copilot-setup-steps: - runs-on: ubuntu-latest - - permissions: - contents: read - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - - name: Install dependencies - run: npm ci - - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - # Customize this step as needed - - name: Build application - run: npx run build diff --git a/.github/workflows/deploy-angular.yml b/.github/workflows/deploy-angular.yml deleted file mode 100644 index 56e8eb2..0000000 --- a/.github/workflows/deploy-angular.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Deploy Angular to Azure Static Web Apps - -on: - push: - branches: - - master - paths: - - 'Clients/TalentManagement-Angular-Material' - - '.github/workflows/deploy-angular.yml' - workflow_dispatch: - -permissions: - id-token: write # Required for OIDC token request - contents: read - -env: - ANGULAR_APP_DIR: 'Clients/TalentManagement-Angular-Material/talent-management' - STATIC_WEB_APP_NAME: 'swa-talent-ui-dev' - RESOURCE_GROUP: 'rg-talent-dev' - -jobs: - build-and-deploy: - name: Build and Deploy Angular - runs-on: ubuntu-latest - - steps: - - name: Checkout repository (with submodules) - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '22.x' - cache: 'npm' - cache-dependency-path: '${{ env.ANGULAR_APP_DIR }}/package-lock.json' - - - name: Install dependencies - working-directory: ${{ env.ANGULAR_APP_DIR }} - run: npm ci - - - name: Inject production environment variables - working-directory: ${{ env.ANGULAR_APP_DIR }} - env: - API_URL: ${{ secrets.API_APP_URL }} - IDENTITY_SERVER_URL: ${{ secrets.IDENTITY_SERVER_URL }} - run: | - sed -i "s|https://your-production-api.com/api/v1|${API_URL}/api/v1|g" src/environments/environment.prod.ts - sed -i "s|https://localhost:44310|${IDENTITY_SERVER_URL}|g" src/environments/environment.prod.ts - - - name: Build Angular (production) - working-directory: ${{ env.ANGULAR_APP_DIR }} - run: npm run build -- --configuration production - - - name: Log in to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Get Static Web App deployment token - id: swa-token - run: | - SWA_TOKEN=$(az staticwebapp secrets list \ - --name ${{ env.STATIC_WEB_APP_NAME }} \ - --resource-group ${{ env.RESOURCE_GROUP }} \ - --query properties.apiKey \ - --output tsv) - echo "token=${SWA_TOKEN}" >> $GITHUB_OUTPUT - - - name: Deploy to Azure Static Web Apps - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ steps.swa-token.outputs.token }} - action: upload - app_location: 'Clients/TalentManagement-Angular-Material/talent-management/dist/talent-management/browser' - skip_app_build: true diff --git a/.github/workflows/deploy-api.yml b/.github/workflows/deploy-api.yml deleted file mode 100644 index 226e801..0000000 --- a/.github/workflows/deploy-api.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Deploy .NET API to Azure App Service - -on: - push: - branches: - - master - paths: - - 'ApiResources/TalentManagement-API/**' - - '.github/workflows/deploy-api.yml' - workflow_dispatch: - -permissions: - id-token: write # Required for OIDC token request - contents: read - -env: - DOTNET_VERSION: '10.x' - PROJECT_PATH: 'ApiResources/TalentManagement-API/TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj' - SOLUTION_PATH: 'ApiResources/TalentManagement-API/TalentManagementAPI.slnx' - PUBLISH_DIR: '${{ github.workspace }}/publish/api' - APP_SERVICE_NAME: 'app-talent-api-dev' - RESOURCE_GROUP: 'rg-talent-dev' - -jobs: - build-and-deploy: - name: Build, Test, and Deploy API - runs-on: ubuntu-latest - - steps: - - name: Checkout repository (with submodules) - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up .NET ${{ env.DOTNET_VERSION }} - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Restore dependencies - run: dotnet restore ${{ env.SOLUTION_PATH }} - - - name: Build - run: dotnet build ${{ env.SOLUTION_PATH }} --configuration Release --no-restore - - - name: Run unit tests - run: dotnet test ${{ env.SOLUTION_PATH }} --configuration Release --no-build --verbosity normal - - - name: Publish - run: | - dotnet publish ${{ env.PROJECT_PATH }} \ - --configuration Release \ - --no-build \ - --output ${{ env.PUBLISH_DIR }} - - - name: Log in to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Configure App Service settings - env: - IDS_URL: ${{ secrets.IDENTITY_SERVER_URL }} - ANGULAR_URL: ${{ secrets.ANGULAR_APP_URL }} - KV_URI: ${{ secrets.KEY_VAULT_URI }} - run: | - az webapp config appsettings set \ - --resource-group ${{ env.RESOURCE_GROUP }} \ - --name ${{ env.APP_SERVICE_NAME }} \ - --settings \ - "ConnectionStrings__DefaultConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--DefaultConnection)" \ - "JWTSettings__Key=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/JWTSettings--Key)" \ - "Sts__ServerUrl=$IDS_URL" \ - "Sts__ValidIssuer=$IDS_URL" \ - "Sts__Audience=app.api.talentmanagement" \ - "JWTSettings__Issuer=CoreIdentity" \ - "JWTSettings__Audience=CoreIdentityUser" \ - "JWTSettings__DurationInMinutes=60" \ - "FeatureManagement__AuthEnabled=true" \ - "Cors__AllowedOrigins__0=$ANGULAR_URL" \ - "Cors__AllowedOrigins__1=https://workcontrolgit.github.io" \ - "ASPNETCORE_ENVIRONMENT=Production" - - - name: Deploy to Azure App Service - uses: azure/webapps-deploy@v3 - with: - app-name: ${{ env.APP_SERVICE_NAME }} - package: ${{ env.PUBLISH_DIR }} diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml deleted file mode 100644 index cc640d2..0000000 --- a/.github/workflows/deploy-gh-pages.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Deploy Angular to GitHub Pages - -on: - push: - branches: - - master - paths: - - 'Clients/TalentManagement-Angular-Material' - - '.github/workflows/deploy-gh-pages.yml' - workflow_dispatch: - -permissions: - contents: write - -env: - ANGULAR_APP_DIR: 'Clients/TalentManagement-Angular-Material/talent-management' - BASE_HREF: '/AngularNetTutorial/' - -jobs: - build-and-deploy: - name: Build and Deploy to GitHub Pages - runs-on: ubuntu-latest - - steps: - - name: Checkout repository (with submodules) - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '22.x' - cache: 'npm' - cache-dependency-path: '${{ env.ANGULAR_APP_DIR }}/package-lock.json' - - - name: Install dependencies - working-directory: ${{ env.ANGULAR_APP_DIR }} - run: npm ci - - - name: Inject production environment variables - working-directory: ${{ env.ANGULAR_APP_DIR }} - env: - API_URL: ${{ secrets.API_APP_URL }} - IDENTITY_SERVER_URL: ${{ secrets.IDENTITY_SERVER_URL }} - run: | - sed -i "s|https://your-production-api.com/api/v1|${API_URL}/api/v1|g" src/environments/environment.prod.ts - sed -i "s|https://localhost:44310|${IDENTITY_SERVER_URL}|g" src/environments/environment.prod.ts - - - name: Build Angular for GitHub Pages - working-directory: ${{ env.ANGULAR_APP_DIR }} - run: npm run build -- --configuration production --base-href "${{ env.BASE_HREF }}" - - - name: Add 404.html for SPA deep-link routing - working-directory: ${{ env.ANGULAR_APP_DIR }}/dist/talent-management/browser - run: cp index.html 404.html - - - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4 - with: - folder: ${{ env.ANGULAR_APP_DIR }}/dist/talent-management/browser - branch: gh-pages - clean: true diff --git a/.github/workflows/deploy-identityserver.yml b/.github/workflows/deploy-identityserver.yml deleted file mode 100644 index 6e9532e..0000000 --- a/.github/workflows/deploy-identityserver.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: Deploy IdentityServer to Azure App Service - -on: - push: - branches: - - master - paths: - - 'TokenService/Duende-IdentityServer/**' - - '.github/workflows/deploy-identityserver.yml' - workflow_dispatch: - -permissions: - id-token: write # Required for OIDC token request - contents: read - -env: - DOTNET_VERSION: '8.x' - STS_PROJECT_PATH: 'TokenService/Duende-IdentityServer/src/DuendeIdentityServer.STS.Identity/DuendeIdentityServer.STS.Identity.csproj' - ADMIN_PROJECT_PATH: 'TokenService/Duende-IdentityServer/src/DuendeIdentityServer.Admin/DuendeIdentityServer.Admin.csproj' - STS_PUBLISH_DIR: '${{ github.workspace }}/publish/ids' - ADMIN_PUBLISH_DIR: '${{ github.workspace }}/publish/admin' - APP_SERVICE_NAME: 'app-talent-ids-dev' - ADMIN_APP_SERVICE_NAME: 'app-talent-admin-dev' - RESOURCE_GROUP: 'rg-talent-dev' - -jobs: - build-and-deploy: - name: Build and Deploy IdentityServer - runs-on: ubuntu-latest - - steps: - - name: Checkout repository (with submodules) - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up .NET ${{ env.DOTNET_VERSION }} - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Restore dependencies - run: | - dotnet restore ${{ env.STS_PROJECT_PATH }} - dotnet restore ${{ env.ADMIN_PROJECT_PATH }} - - - name: Build - run: | - dotnet build ${{ env.STS_PROJECT_PATH }} --configuration Release --no-restore - dotnet build ${{ env.ADMIN_PROJECT_PATH }} --configuration Release --no-restore - - - name: Publish STS - run: | - dotnet publish ${{ env.STS_PROJECT_PATH }} \ - --configuration Release \ - --no-build \ - --output ${{ env.STS_PUBLISH_DIR }} - - - name: Publish Admin - run: | - dotnet publish ${{ env.ADMIN_PROJECT_PATH }} \ - --configuration Release \ - --no-build \ - --output ${{ env.ADMIN_PUBLISH_DIR }} - - - name: Log in to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Configure STS App Service settings - env: - IDS_URL: ${{ secrets.IDENTITY_SERVER_URL }} - ADMIN_URL: ${{ secrets.IDENTITY_ADMIN_URL }} - KV_URI: ${{ secrets.KEY_VAULT_URI }} - run: | - az webapp config appsettings set \ - --resource-group ${{ env.RESOURCE_GROUP }} \ - --name ${{ env.APP_SERVICE_NAME }} \ - --settings \ - "ConnectionStrings__ConfigurationDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "ConnectionStrings__PersistedGrantDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "ConnectionStrings__IdentityDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "ConnectionStrings__DataProtectionDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "AdminConfiguration__IdentityServerBaseUrl=$IDS_URL" \ - "AdminConfiguration__IdentityAdminBaseUrl=$ADMIN_URL" \ - "CspTrustedDomains__0=www.gravatar.com" \ - "CspTrustedDomains__1=fonts.googleapis.com" \ - "CspTrustedDomains__2=fonts.gstatic.com" \ - "CspTrustedDomains__3=workcontrolgit.github.io" \ - "CspTrustedDomains__4=mango-flower-0ced4011e.4.azurestaticapps.net" \ - "ASPNETCORE_ENVIRONMENT=Production" - - - name: Configure Admin App Service settings - env: - IDS_URL: ${{ secrets.IDENTITY_SERVER_URL }} - ADMIN_URL: ${{ secrets.IDENTITY_ADMIN_URL }} - KV_URI: ${{ secrets.KEY_VAULT_URI }} - run: | - az webapp config appsettings set \ - --resource-group ${{ env.RESOURCE_GROUP }} \ - --name ${{ env.ADMIN_APP_SERVICE_NAME }} \ - --settings \ - "ConnectionStrings__ConfigurationDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "ConnectionStrings__PersistedGrantDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "ConnectionStrings__IdentityDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "ConnectionStrings__AdminLogDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "ConnectionStrings__AdminAuditLogDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "ConnectionStrings__DataProtectionDbConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--IdsDbConnection)" \ - "AdminConfiguration__IdentityServerBaseUrl=$IDS_URL" \ - "AdminConfiguration__IdentityAdminRedirectUri=$ADMIN_URL/signin-oidc" \ - "SeedConfiguration__ApplySeed=false" \ - "DatabaseMigrationsConfiguration__ApplyDatabaseMigrations=false" \ - "ASPNETCORE_ENVIRONMENT=Production" - - - name: Deploy STS to Azure App Service - uses: azure/webapps-deploy@v3 - with: - app-name: ${{ env.APP_SERVICE_NAME }} - package: ${{ env.STS_PUBLISH_DIR }} - - - name: Deploy Admin to Azure App Service - uses: azure/webapps-deploy@v3 - with: - app-name: ${{ env.ADMIN_APP_SERVICE_NAME }} - package: ${{ env.ADMIN_PUBLISH_DIR }} diff --git a/.github/workflows/deploy-infra.yml b/.github/workflows/deploy-infra.yml deleted file mode 100644 index 24af801..0000000 --- a/.github/workflows/deploy-infra.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Deploy Azure Infrastructure (Bicep) - -on: - workflow_dispatch: - inputs: - environment: - description: 'Target environment' - required: true - default: 'dev' - type: choice - options: - - dev - -permissions: - id-token: write # Required for OIDC token request - contents: read - -env: - RESOURCE_GROUP: 'rg-talent-dev' - BICEP_FILE: 'infra/main.bicep' - PARAMS_FILE: 'infra/parameters/dev.bicepparam' - -jobs: - deploy-infrastructure: - name: Deploy Bicep Infrastructure - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Deploy Bicep - env: - # readEnvironmentVariable('SQL_ADMIN_PASSWORD') in dev.bicepparam reads this - SQL_ADMIN_PASSWORD: ${{ secrets.SQL_ADMIN_PASSWORD }} - run: | - az deployment group create \ - --resource-group ${{ env.RESOURCE_GROUP }} \ - --template-file ${{ env.BICEP_FILE }} \ - --parameters ${{ env.PARAMS_FILE }} \ - --output json diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index 219894e..0000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -env: - PLAYWRIGHT_DIR: 'Tests/AngularNetTutorial-Playwright' - -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install dependencies - working-directory: ${{ env.PLAYWRIGHT_DIR }} - run: npm ci - - name: Install Playwright Browsers - working-directory: ${{ env.PLAYWRIGHT_DIR }} - run: npx playwright install --with-deps - - name: Run Playwright tests - working-directory: ${{ env.PLAYWRIGHT_DIR }} - env: - ANGULAR_APP_URL: ${{ secrets.ANGULAR_APP_URL }} - API_APP_URL: ${{ secrets.API_APP_URL }} - IDENTITY_SERVER_URL: ${{ secrets.IDENTITY_SERVER_URL }} - run: npx playwright test --project=smoke - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: ${{ env.PLAYWRIGHT_DIR }}/playwright-report/ - retention-days: 30 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4a9c43e..0000000 --- a/.gitignore +++ /dev/null @@ -1,58 +0,0 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. - -# IDE and Editor directories -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS Files -.DS_Store -Thumbs.db -Desktop.ini - -# Logs -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Temporary files -tmp/ -temp/ -*.tmp - -# Build outputs (will be in submodules) -dist/ -build/ -out/ - -# Dependencies (will be in submodules) -node_modules/ - -# Environment files (these should be in submodules, but add here as safety) -.env -.env.local -.env.*.local - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# Scripts with secrets -infra/scripts/setup-azure.ps1 - -# OpenWolf -.wolf/ - -# Playwright -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ -/playwright/.auth/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 5122ca9..0000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "Clients/TalentManagement-Angular-Material"] - path = Clients/TalentManagement-Angular-Material - url = https://github.com/workcontrolgit/TalentManagement-Angular-Material.git -[submodule "ApiResources/TalentManagement-API"] - path = ApiResources/TalentManagement-API - url = https://github.com/workcontrolgit/TalentManagement-API.git -[submodule "TokenService/Duende-IdentityServer"] - path = TokenService/Duende-IdentityServer - url = https://github.com/workcontrolgit/Duende-IdentityServer.git -[submodule "Tests/AngularNetTutorial-Playwright"] - path = Tests/AngularNetTutorial-Playwright - url = https://github.com/workcontrolgit/AngularNetTutorial-Playwright.git diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 6da4077..0000000 --- a/.mcp.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "mcpServers": { - "playwright-test": { - "command": "cmd", - "args": [ - "/c", - "npx", - "playwright", - "run-test-mcp-server" - ] - } - } -} \ No newline at end of file diff --git a/.wolf/OPENWOLF.md b/.wolf/OPENWOLF.md deleted file mode 100644 index aae2a47..0000000 --- a/.wolf/OPENWOLF.md +++ /dev/null @@ -1,135 +0,0 @@ -# OpenWolf Operating Protocol - -You are working in an OpenWolf-managed project. These rules apply every turn. - -## File Navigation - -1. Check `.wolf/anatomy.md` BEFORE reading any file. It has a 2-3 line description and token estimate for every file in the project. -2. If the description in anatomy.md is sufficient for your task, do NOT read the full file. -3. If a file is not in anatomy.md, search with Grep/Glob, then update anatomy.md with the new entry. - -## Code Generation - -1. Before generating code, read `.wolf/cerebrum.md` and respect every entry. -2. Check the `## Do-Not-Repeat` section — these are past mistakes that must not recur. -3. Follow all conventions in `## Key Learnings` and `## User Preferences`. - -## After Actions - -1. After every significant action, append a one-line entry to `.wolf/memory.md`: - `| HH:MM | description | file(s) | outcome | ~tokens |` -2. After creating, deleting, or renaming files: update `.wolf/anatomy.md`. - -## Cerebrum Learning (MANDATORY — every session) - -OpenWolf's value comes from learning across sessions. You MUST update `.wolf/cerebrum.md` whenever you learn something useful. This is not optional. - -**Update `## User Preferences` when the user:** -- Corrects your approach ("no, do it this way instead") -- Expresses a style preference (naming, structure, formatting) -- Shows a preferred workflow or tool choice -- Rejects a suggestion — record what they preferred instead -- Asks for more/less detail, verbosity, explanation - -**Update `## Key Learnings` when you discover:** -- A project convention not obvious from the code (e.g., "tests go in __tests__/ not test/") -- A framework-specific pattern this project uses -- An API behavior that surprised you -- A dependency quirk or version constraint -- How modules connect or data flows through the system - -**Update `## Do-Not-Repeat` (with date) when:** -- The user corrects a mistake you made -- You try something that fails and find the right approach -- You discover a gotcha that would trip up a fresh session - -**Update `## Decision Log` when:** -- A significant architectural or technical choice is made -- The user explains why they chose approach A over B -- A trade-off is explicitly discussed - -**The bar is LOW.** If in doubt, add it. A cerebrum entry that's slightly redundant costs nothing. A missing entry means the next session repeats the same discovery process. - -## Bug Logging (MANDATORY) - -**Log a bug to `.wolf/buglog.json` whenever ANY of these happen:** -- The user reports an error, bug, or problem -- A test fails or a command produces an error -- You fix something that was broken -- You edit a file more than twice to get it right -- An import, module, or dependency is missing or wrong -- A runtime error, type error, or syntax error occurs -- A build or lint command fails -- A feature doesn't work as expected -- You change error handling, try/catch blocks, or validation logic -- The user says something "doesn't work", "is broken", or "shows wrong X" - -**Before fixing:** Read `.wolf/buglog.json` first — the fix may already be known. - -**After fixing:** ALWAYS append to `.wolf/buglog.json` with this structure: -```json -{ - "id": "bug-NNN", - "timestamp": "ISO date", - "error_message": "exact error or user complaint", - "file": "file that was fixed", - "root_cause": "why it broke", - "fix": "what you changed to fix it", - "tags": ["relevant", "keywords"], - "related_bugs": [], - "occurrences": 1, - "last_seen": "ISO date" -} -``` - -**The threshold is LOW.** When in doubt, log it. A false positive in the bug log costs nothing. A missed bug means repeating the same mistake later. - -## Token Discipline - -- Never re-read a file already read this session unless it was modified since. -- Prefer anatomy.md descriptions over full file reads when possible. -- Prefer targeted Grep over full file reads when searching for specific code. -- If appending to a file, do not read the entire file first. - -## Design QC - -When the user asks you to check, evaluate, or improve the design/UI of their app: - -1. Run `openwolf designqc` via Bash to capture screenshots. - - The command auto-detects a running dev server, or starts one from package.json if needed - - Use `--url ` only if auto-detection fails - - The command saves compressed JPEG screenshots to `.wolf/designqc-captures/` - - Full pages are captured as sectioned viewport-height images (top, section2, ..., bottom) -2. Read the captured screenshot images from `.wolf/designqc-captures/` using the Read tool. -3. Evaluate the design against modern standards (Shadcn UI, Tailwind, clean React patterns): - - Spacing and whitespace consistency - - Typography hierarchy and readability - - Color contrast and accessibility (WCAG) - - Visual hierarchy and focal points - - Component consistency - - Whether the design looks "dull" or "white-coded" (generic, no personality) -4. Provide specific, actionable feedback with fix suggestions. -5. If the user approves, implement the fixes directly in their code. -6. After fixes, re-run `openwolf designqc` to capture new screenshots and verify improvement. - -**Token awareness:** Each screenshot costs ~2500 tokens. The command compresses images (JPEG quality 70, max width 1200px) to minimize cost. For large apps, use `--routes / /specific-page` to limit captures. - -## Reframe — UI Framework Selection - -When the user asks to change, pick, migrate, or "reframe" their project's UI framework: - -1. Read `.wolf/reframe-frameworks.md` for the full framework knowledge base. -2. Ask the user the decision questions from the file (current stack, priority, Tailwind usage, theme preference, app type). Stop early once the choice narrows to 1-2 options. -3. Present a recommendation with reasoning based on the comparison matrix. -4. Once the user confirms, use the selected framework's prompt from the file — **adapted to the actual project** using `.wolf/anatomy.md` for real file paths, routes, and components. -5. Execute the migration: install dependencies, update config, refactor components. -6. After migration, run `openwolf designqc` to verify the new look. - -**Do NOT read the entire reframe-frameworks.md into context upfront.** Read the decision questions and comparison matrix first (~50 lines). Only read the specific framework's prompt section after the user chooses. - -## Session End - -Before ending or when asked to wrap up: - -1. Write a session summary to `.wolf/memory.md`. -2. Review the session: did you learn anything? Did the user correct you? Did you fix a bug? If yes, update `.wolf/cerebrum.md` and/or `.wolf/buglog.json`. diff --git a/.wolf/anatomy.md b/.wolf/anatomy.md deleted file mode 100644 index 3892629..0000000 --- a/.wolf/anatomy.md +++ /dev/null @@ -1,578 +0,0 @@ -# anatomy.md - -> Auto-maintained by OpenWolf. Last scanned: 2026-04-28T02:35:14.407Z -> Files: 67 tracked | Anatomy hits: 0 | Misses: 0 - -## ../../Users/Fuji Nguyen/.claude/plans/ - -- `peppy-conjuring-leaf.md` — Plan: New Standalone Series — DotnetAiAgentMcp (~6061 tok) -- `toasty-shimmying-haven.md` — Secure Playwright Test Credentials with PKCE Global Auth Setup (~4045 tok) - -## ../../Users/Fuji Nguyen/.claude/projects/c--apps-AngularNetTutotial/memory/ - - -## ../DotnetMcpTutorial/ - -- `.gitignore` — Git ignore rules (~92 tok) -- `README.md` — Project documentation (~357 tok) - -## ./ - -- `.gitignore` — Git ignore rules (~219 tok) -- `package.json` — Node.js package manifest (~178 tok) - -## .claude/ - - -## .claude/rules/ - - -## .github/agents/ - - -## .github/workflows/ - - -## .vscode/ - -- `settings.json` (~94 tok) - -## ApiResources/TalentManagement-API/ - - -## ApiResources/TalentManagement-API/.dotnet/ - - -## ApiResources/TalentManagement-API/.dotnet/.dotnet/ - - -## ApiResources/TalentManagement-API/.nuget/packages/ardalis.specification.entityframeworkcore/9.3.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/ardalis.specification.entityframeworkcore/9.3.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/ardalis.specification.entityframeworkcore/9.3.1/lib/net9.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/ardalis.specification/9.3.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/ardalis.specification/9.3.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/ardalis.specification/9.3.1/lib/net9.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/ardalis.specification/9.3.1/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.abstractions/8.1.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.abstractions/8.1.0/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.abstractions/8.1.0/lib/netstandard1.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.abstractions/8.1.0/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.http/8.1.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.http/8.1.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.mvc.apiexplorer/8.1.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.mvc.apiexplorer/8.1.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.mvc/8.1.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/asp.versioning.mvc/8.1.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/autobogus/2.13.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.46.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.46.1/lib/net462/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.46.1/lib/net472/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.46.1/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.46.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.46.1/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.47.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.47.1/lib/net462/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.47.1/lib/net472/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.47.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.core/1.47.1/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.identity/1.14.2/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.identity/1.14.2/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/azure.identity/1.14.2/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/31.0.3/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/31.0.3/lib/net40/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/31.0.3/lib/netstandard1.3/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/31.0.3/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/35.6.5/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/35.6.5/lib/net40/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/35.6.5/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/35.6.5/lib/netstandard1.3/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bogus/35.6.5/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/bouncycastle.cryptography/2.6.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/castle.core/5.1.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/castle.core/5.1.1/lib/net462/ - - -## ApiResources/TalentManagement-API/.nuget/packages/castle.core/5.1.1/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/castle.core/5.1.1/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/castle.core/5.1.1/lib/netstandard2.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/coverlet.collector/6.0.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.core/1.8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.core/1.8.0/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.core/1.8.0/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.inmemory/1.8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.inmemory/1.8.0/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.inmemory/1.8.0/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.redis/1.8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.redis/1.8.0/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.redis/1.8.0/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.serialization.systemtextjson/1.8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.serialization.systemtextjson/1.8.0/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/easycaching.serialization.systemtextjson/1.8.0/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentassertions/6.12.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentassertions/6.12.0/lib/net47/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentassertions/6.12.0/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentassertions/6.12.0/lib/netcoreapp2.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentassertions/6.12.0/lib/netcoreapp3.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentassertions/6.12.0/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentassertions/6.12.0/lib/netstandard2.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentvalidation.dependencyinjectionextensions/12.1.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentvalidation.dependencyinjectionextensions/12.1.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentvalidation/12.1.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/fluentvalidation/12.1.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/humanizer.core/2.14.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/humanizer.core/2.14.1/lib/net6.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/humanizer.core/2.14.1/lib/netstandard1.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/humanizer.core/2.14.1/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mailkit/4.14.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mailkit/4.14.1/docs/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mailkit/4.14.1/lib/net462/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mailkit/4.14.1/lib/net47/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mailkit/4.14.1/lib/net48/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mailkit/4.14.1/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mailkit/4.14.1/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mailkit/4.14.1/lib/netstandard2.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mapster.core/1.2.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mapster.dependencyinjection/1.0.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mapster/5.0.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/mapster/7.4.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.aspnetcore.authentication.jwtbearer/10.0.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.aspnetcore.authentication.jwtbearer/10.0.1/lib/net10.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.asyncinterfaces/8.0.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.asyncinterfaces/8.0.0/buildTransitive/net461/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.asyncinterfaces/8.0.0/buildTransitive/net462/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.asyncinterfaces/8.0.0/lib/net462/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.asyncinterfaces/8.0.0/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.asyncinterfaces/8.0.0/lib/netstandard2.1/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/buildTransitive/net461/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/buildTransitive/net462/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/buildTransitive/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/buildTransitive/netcoreapp2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/lib/net462/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/lib/net9.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.bcl.cryptography/9.0.4/lib/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/17.11.31/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/17.11.31/lib/net472/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/17.11.31/lib/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/17.11.31/notices/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/17.11.31/ref/net472/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/17.11.31/ref/net8.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/17.11.31/ref/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/18.0.2/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/18.0.2/lib/net10.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/18.0.2/lib/net472/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/18.0.2/notices/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/18.0.2/ref/net10.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/18.0.2/ref/net472/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.build.framework/18.0.2/ref/netstandard2.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.codeanalysis.analyzers/3.11.0/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.codeanalysis.analyzers/3.11.0/buildTransitive/ - - -## ApiResources/TalentManagement-API/.nuget/packages/microsoft.codeanalysis.analyzers/3.11.0/buildTransitive/config/ - - -## ApiResources/TalentManagement-API/TalentManagementAPI.Infrastructure.Shared/ - - -## ApiResources/TalentManagement-API/TalentManagementAPI.Infrastructure.Shared/Services/ - - -## ApiResources/TalentManagement-API/TalentManagementAPI.Infrastructure.Tests/Services/ - - -## ApiResources/TalentManagement-API/TalentManagementAPI.WebApi.Tests/Controllers/ - - -## Clients/TalentManagement-Angular-Material/talent-management/public/data/ - - -## Clients/TalentManagement-Angular-Material/talent-management/src/ - - -## Clients/TalentManagement-Angular-Material/talent-management/src/app/ - -- `app.routes.ts` — Exports routes (~1443 tok) - -## Clients/TalentManagement-Angular-Material/talent-management/src/app/routes/ai/ai-assistant/ - - -## Clients/TalentManagement-Angular-Material/talent-management/src/app/routes/ai/ai-hr-insight/ - - -## Clients/TalentManagement-Angular-Material/talent-management/src/app/routes/ai/ai-nl-search/ - - -## Clients/TalentManagement-Angular-Material/talent-management/src/app/services/api/ - - -## Tests/AngularNetTutorial-Playwright/ - -- `.gitignore` — Git ignore rules (~152 tok) -- `package.json` — Node.js package manifest (~250 tok) -- `playwright.config.ts` — Load environment-specific credentials and URLs. (~1744 tok) -- `tsconfig.json` — TypeScript configuration (~91 tok) - -## Tests/AngularNetTutorial-Playwright/.github/workflows/ - -- `playwright.yml` — CI: Playwright Tests (~1274 tok) - -## Tests/AngularNetTutorial-Playwright/config/ - -- `environments.json` (~314 tok) -- `test-config.ts` — Centralized Test Configuration (~2050 tok) -- `test-users.json` (~267 tok) - -## Tests/AngularNetTutorial-Playwright/fixtures/ - -- `api.fixtures.ts` — API Fixtures (~3016 tok) -- `auth.fixtures.ts` — Authentication Fixtures (~4316 tok) - -## Tests/AngularNetTutorial-Playwright/scripts/ - -- `fetch-secrets.ts` — Azure Key Vault → .env generator (~927 tok) - -## Tests/AngularNetTutorial-Playwright/tests/ - -- `auth.setup.ts` — Global Authentication Setup (~536 tok) -- `diagnostic.spec.ts` — Declares currentUrl (~498 tok) - -## Tests/AngularNetTutorial-Playwright/tests/accessibility/ - -- `aria-labels.spec.ts` — ARIA Labels Tests (~2252 tok) -- `keyboard-navigation.spec.ts` — Keyboard Navigation Tests (~2162 tok) - -## Tests/AngularNetTutorial-Playwright/tests/ai/ - -- `ai-assistant.spec.ts` — AI Assistant Page Tests (~1506 tok) -- `ai-hr-insight.spec.ts` — AI HR Insight Page Tests (~1200 tok) -- `ai-navigation.spec.ts` — AI Submenu Navigation Tests (~1174 tok) -- `ai-nl-search.spec.ts` — AI NL Search Page Tests (~1478 tok) -- `ai-vector-search.spec.ts` — AI Vector Search Page Tests (~1679 tok) - -## Tests/AngularNetTutorial-Playwright/tests/api/ - -- `auth-api.spec.ts` — Authentication API Tests (~5266 tok) -- `cache-api.spec.ts` — Cache API Tests (~4486 tok) -- `departments-api.spec.ts` — Department API Tests (~4574 tok) -- `employees-api.spec.ts` — Employee API Tests (~5235 tok) - -## Tests/AngularNetTutorial-Playwright/tests/auth/ - -- `login.spec.ts` — Authentication Tests - Login Flow (~2167 tok) - -## Tests/AngularNetTutorial-Playwright/tests/dashboard/ - -- `dashboard-metrics.spec.ts` — Dashboard Metrics Tests (~2546 tok) -- `dashboard-navigation.spec.ts` — Dashboard Navigation Tests (~3406 tok) - -## Tests/AngularNetTutorial-Playwright/tests/department-management/ - -- `department-crud.spec.ts` — Department CRUD Tests (~2104 tok) -- `department-validation.spec.ts` — Department Validation Tests (~2776 tok) - -## Tests/AngularNetTutorial-Playwright/tests/employee-management/ - -- `employee-create.spec.ts` — Employee Create Tests (~3904 tok) -- `employee-delete.spec.ts` — Employee Delete Tests (~2562 tok) -- `employee-edit.spec.ts` — Employee Edit Tests (~2653 tok) -- `employee-smoke.spec.ts` — Employee Management Smoke Tests (~1898 tok) - -## Tests/AngularNetTutorial-Playwright/tests/error-handling/ - -- `api-errors.spec.ts` — API Error Handling Tests (~2315 tok) -- `network-errors.spec.ts` — Network Error Handling Tests (~2617 tok) - -## Tests/AngularNetTutorial-Playwright/tests/navigation/ - -- `routing.spec.ts` — Navigation & Routing Tests (~3421 tok) - -## Tests/AngularNetTutorial-Playwright/tests/performance/ - -- `large-datasets.spec.ts` — Large Datasets Performance Tests (~2161 tok) -- `load-time.spec.ts` — Load Time Performance Tests (~1689 tok) - -## Tests/AngularNetTutorial-Playwright/tests/position-management/ - -- `position-crud.spec.ts` — Position CRUD Tests (HRAdmin Only) (~2524 tok) - -## Tests/AngularNetTutorial-Playwright/tests/salary-ranges/ - -- `salary-range-crud.spec.ts` — Salary Range CRUD Tests (~2128 tok) -- `salary-range-validation.spec.ts` — Salary Range Validation Tests (~2615 tok) - -## Tests/AngularNetTutorial-Playwright/tests/screenshots/ - -- `blog-screenshots.spec.ts` — Blog Screenshots (~10494 tok) - -## Tests/AngularNetTutorial-Playwright/tests/validation/ - -- `form-validation.spec.ts` — Form Validation Edge Cases Tests (~4296 tok) - -## Tests/AngularNetTutorial-Playwright/tests/visual/ - -- `dashboard-visual.spec.ts` — Dashboard Visual Regression Tests (~1494 tok) -- `forms-visual.spec.ts` — Forms Visual Regression Tests (~1318 tok) - -## Tests/AngularNetTutorial-Playwright/tests/workflows/ - -- `complete-employee-workflow.spec.ts` — Complete Employee Workflow Test (~2659 tok) -- `hradmin-operations.spec.ts` — HRAdmin Operations Workflow Test (~3424 tok) -- `manager-daily-tasks.spec.ts` — Manager Daily Tasks Workflow Test (~3308 tok) - -## Tests/AngularNetTutorial-Playwright/utils/ - -- `token-manager.ts` — Token Manager Utility (~2218 tok) - -## blogs/ - -- `SERIES-NAVIGATION-TOC.md` — AngularNetTutorial — Series Navigation (~1927 tok) - -## blogs/series-6-ai-app-features/ - -- `6.1-dotnet-ai-foundation.md` — Run a Local LLM in Your .NET 10 API with Ollama (~6391 tok) -- `6.2-dotnet-ai-hr-assistant.md` — Build an HR AI Assistant That Knows Your Data (~4920 tok) -- `6.3-angular-ai-chat-widget.md` — Build a Dedicated AI Section in Angular with Submenu Navigation (~4967 tok) -- `6.4-angular-ai-nl-search.md` — Natural Language Employee Search in Angular Material (~4897 tok) -- `6.4.1-dotnet-natural-language-search.md` — Natural Language Employee Search with LLM Query Parsing (~7263 tok) -- `6.5-angular-ai-vector-search.md` — Semantic Position Search with Vector Embeddings in Angular Material (~4180 tok) -- `6.6-dotnet-ai-response-caching.md` — Cache Your AI Responses: Save Time and API Costs (~4185 tok) -- `6.7-dotnet-mssql-vector-search.md` — Semantic Position Search with SQL Server Native Vector Search (~6594 tok) -- `6.8-dotnet-ai-response-caching.md` — Cache Your AI Responses: Save Time and API Costs (~4228 tok) diff --git a/.wolf/buglog.json b/.wolf/buglog.json deleted file mode 100644 index 4b2cbee..0000000 --- a/.wolf/buglog.json +++ /dev/null @@ -1,885 +0,0 @@ -{ - "version": 1, - "bugs": [ - { - "id": "bug-001", - "timestamp": "2026-04-21T17:17:05.870Z", - "error_message": "Type error", - "file": "Tests/AngularNetTutorial-Playwright/tests/screenshots/blog-screenshots.spec.ts", - "root_cause": "Missing or incorrect type annotation", - "fix": "Added type assertion/annotation", - "tags": [ - "auto-detected", - "type-fix", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-21T17:17:05.870Z" - }, - { - "id": "bug-002", - "timestamp": "2026-04-22T02:40:07.617Z", - "error_message": "Incorrect value in code", - "file": "Clients/TalentManagement-Angular-Material/talent-management/public/data/menu.json", - "root_cause": "Had \"psychology\"", - "fix": "Changed to \"memory\"", - "tags": [ - "auto-detected", - "wrong-value", - "json" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-22T02:41:41.298Z" - }, - { - "id": "bug-003", - "timestamp": "2026-04-22T02:41:36.799Z", - "error_message": "Incorrect value in code", - "file": "Clients/TalentManagement-Angular-Material/talent-management/src/index.html", - "root_cause": "Had \"https://fonts.googleapis.com/icon?family=Material", - "fix": "Changed to \"preconnect\"", - "tags": [ - "auto-detected", - "wrong-value", - "html" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T02:41:36.799Z" - }, - { - "id": "bug-004", - "timestamp": "2026-04-22T02:52:28.052Z", - "error_message": "Wrong return value", - "file": "Clients/TalentManagement-Angular-Material/talent-management/src/app/services/api/ai.service.ts", - "root_cause": "Was returning: this.http.post(`${this.apiUrl}/", - "fix": "Now returns: this.http.post>(`${th", - "tags": [ - "auto-detected", - "return-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T02:52:28.052Z" - }, - { - "id": "bug-005", - "timestamp": "2026-04-22T02:58:35.635Z", - "error_message": "Wrong return value", - "file": "Clients/TalentManagement-Angular-Material/talent-management/src/app/services/api/ai.service.ts", - "root_cause": "Was returning: this.http.post(`${this.apiUrl}/a", - "fix": "Now returns: this.http.post>(`${thi", - "tags": [ - "auto-detected", - "return-value", - "ts" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-22T03:02:43.517Z" - }, - { - "id": "bug-006", - "timestamp": "2026-04-22T02:58:44.709Z", - "error_message": "Null/undefined access in ", - "file": "Clients/TalentManagement-Angular-Material/talent-management/src/app/routes/ai/ai-nl-search/ai-nl-search.component.html", - "root_cause": "Property access on potentially null/undefined value", - "fix": "Added null safety (optional chaining or null check)", - "tags": [ - "auto-detected", - "null-safety", - "html" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T02:58:44.709Z" - }, - { - "id": "bug-007", - "timestamp": "2026-04-22T03:04:54.340Z", - "error_message": "Null/undefined access in ", - "file": "Clients/TalentManagement-Angular-Material/talent-management/src/app/routes/ai/ai-nl-search/ai-nl-search.component.ts", - "root_cause": "Property access on potentially null/undefined value", - "fix": "Added null safety (optional chaining or null check)", - "tags": [ - "auto-detected", - "null-safety", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T03:04:54.340Z" - }, - { - "id": "bug-008", - "timestamp": "2026-04-22T03:21:14.869Z", - "error_message": "Incorrect value in code", - "file": "blogs/series-6-ai-app-features/6.3-angular-ai-chat-widget.md", - "root_cause": "Had 's AI features from a single `", - "fix": "Changed to `ai`", - "tags": [ - "auto-detected", - "wrong-value", - "md" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-22T03:21:36.132Z" - }, - { - "id": "bug-009", - "timestamp": "2026-04-22T11:15:22.923Z", - "error_message": "Significant refactor of ", - "file": "../../Users/Fuji Nguyen/.claude/projects/c--apps-AngularNetTutotial/memory/project_blog_image_paths.md", - "root_cause": "19 lines replaced/restructured", - "fix": "Rewrote 51→83 lines (19 removed)", - "tags": [ - "auto-detected", - "refactor", - "md" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T11:15:22.923Z" - }, - { - "id": "bug-010", - "timestamp": "2026-04-22T11:15:29.442Z", - "error_message": "Incorrect value in code", - "file": "../../Users/Fuji Nguyen/.claude/projects/c--apps-AngularNetTutotial/memory/MEMORY.md", - "root_cause": "Had `../../Tests/AngularNetTutorial-Playwright/screens", - "fix": "Changed to `../../Tests/AngularNetTutorial-Playwright/screens", - "tags": [ - "auto-detected", - "wrong-value", - "md" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T11:15:29.442Z" - }, - { - "id": "bug-011", - "timestamp": "2026-04-22T11:47:54.676Z", - "error_message": "Significant refactor of ", - "file": "blogs/series-6-ai-app-features/6.1-dotnet-ai-foundation.md", - "root_cause": "2 lines replaced/restructured", - "fix": "Rewrote 14→16 lines (2 removed) | Also: The `IChatClient` abstraction from Microsoft is th; * **Provider-agnostic AI abstractions** — The `ICh", - "tags": [ - "auto-detected", - "refactor", - "md" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-22T11:48:18.706Z" - }, - { - "id": "bug-012", - "timestamp": "2026-04-22T14:07:06.877Z", - "error_message": "Incorrect value in code", - "file": "ApiResources/TalentManagement-API/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj", - "root_cause": "Had \"OllamaSharp\"", - "fix": "Changed to \"Microsoft.Extensions.AI\"", - "tags": [ - "auto-detected", - "wrong-value", - "csproj" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-22T14:07:32.511Z" - }, - { - "id": "bug-013", - "timestamp": "2026-04-22T14:14:23.091Z", - "error_message": "Incorrect value in code", - "file": "ApiResources/TalentManagement-API/TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj", - "root_cause": "Had \"Microsoft.Extensions.AI\"", - "fix": "Changed to \"Microsoft.Extensions.AI.Abstractions\"", - "tags": [ - "auto-detected", - "wrong-value", - "csproj" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T14:14:23.091Z" - }, - { - "id": "bug-014", - "timestamp": "2026-04-22T18:13:22.809Z", - "error_message": "Significant refactor of ", - "file": "blogs/series-6-ai-app-features/6.1-dotnet-ai-foundation.md", - "root_cause": "3 lines replaced/restructured", - "fix": "Rewrote 7→9 lines (3 removed) | Also: Add OllamaSharp to the Infrastructure.Shared proje; **Why OllamaSharp instead of `Microsoft.Extensions | Also: In `Infrastructure.Shared/ServiceRegistration.cs`,; // Register the Ollama client as a singleton — one | Also: OllamaSharp's native `IAsyncEnumerable<>` streamin; The custom `IAiChatService` interface pattern is t", - "tags": [ - "auto-detected", - "refactor", - "md" - ], - "related_bugs": [], - "occurrences": 4, - "last_seen": "2026-04-22T18:14:46.417Z" - }, - { - "id": "bug-015", - "timestamp": "2026-04-22T18:15:05.533Z", - "error_message": "Incorrect value in code", - "file": "blogs/series-6-ai-app-features/6.1-dotnet-ai-foundation.md", - "root_cause": "Had `404`", - "fix": "Changed to `503 Service Unavailable`", - "tags": [ - "auto-detected", - "wrong-value", - "md" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T18:15:05.533Z" - }, - { - "id": "bug-016", - "timestamp": "2026-04-22T18:52:28.062Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/tests/screenshots/blog-screenshots.spec.ts", - "root_cause": "Had 'screenshots-output'", - "fix": "Changed to '..'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 3, - "last_seen": "2026-04-22T18:53:34.248Z" - }, - { - "id": "bug-017", - "timestamp": "2026-04-22T18:56:28.261Z", - "error_message": "Wrong reference: images should be screenshots", - "file": "Tests/AngularNetTutorial-Playwright/tests/screenshots/blog-screenshots.spec.ts", - "root_cause": "Used \"images\" instead of \"screenshots\"", - "fix": "Changed images → screenshots", - "tags": [ - "auto-detected", - "wrong-reference", - "ts" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-22T18:56:42.804Z" - }, - { - "id": "bug-018", - "timestamp": "2026-04-22T18:56:38.182Z", - "error_message": "Wrong reference: images should be screenshots", - "file": "blogs/series-6-ai-app-features/6.1-dotnet-ai-foundation.md", - "root_cause": "Used \"images\" instead of \"screenshots\"", - "fix": "Changed images → screenshots", - "tags": [ - "auto-detected", - "wrong-reference", - "md" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T18:56:38.182Z" - }, - { - "id": "bug-019", - "timestamp": "2026-04-22T20:34:48.779Z", - "error_message": "Significant refactor of ", - "file": "blogs/SERIES-NAVIGATION-TOC.md", - "root_cause": "3 lines replaced/restructured", - "fix": "Rewrote 8→10 lines (3 removed)", - "tags": [ - "auto-detected", - "refactor", - "md" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-22T20:34:48.779Z" - }, - { - "id": "bug-020", - "timestamp": "2026-04-23T02:33:32.404Z", - "error_message": "Incorrect value in code", - "file": "Clients/TalentManagement-Angular-Material/talent-management/src/app/app.routes.ts", - "root_cause": "Had 'ai-chat'", - "fix": "Changed to '403'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-23T02:33:32.404Z" - }, - { - "id": "bug-021", - "timestamp": "2026-04-23T02:34:00.669Z", - "error_message": "Incorrect value in code", - "file": "blogs/series-6-ai-app-features/6.3-angular-ai-chat-widget.md", - "root_cause": "Had `ai-chat`", - "fix": "Changed to `ai`", - "tags": [ - "auto-detected", - "wrong-value", - "md" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-23T02:34:00.669Z" - }, - { - "id": "bug-022", - "timestamp": "2026-04-28T01:04:57.687Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/package.json", - "root_cause": "2 lines replaced/restructured", - "fix": "Rewrote 14→23 lines (2 removed)", - "tags": [ - "auto-detected", - "refactor", - "json" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:04:57.687Z" - }, - { - "id": "bug-023", - "timestamp": "2026-04-28T01:06:22.653Z", - "error_message": "Type error", - "file": "Tests/AngularNetTutorial-Playwright/config/test-config.ts", - "root_cause": "Missing or incorrect type annotation", - "fix": "Added type assertion/annotation", - "tags": [ - "auto-detected", - "type-fix", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:06:22.653Z" - }, - { - "id": "bug-024", - "timestamp": "2026-04-28T01:07:15.206Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/fixtures/auth.fixtures.ts", - "root_cause": "Had '../config/test-users.json'", - "fix": "Changed to '../config/test-config'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:07:15.206Z" - }, - { - "id": "bug-025", - "timestamp": "2026-04-28T01:07:31.053Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/fixtures/auth.fixtures.ts", - "root_cause": "51 lines replaced/restructured", - "fix": "Rewrote 72→10 lines (51 removed)", - "tags": [ - "auto-detected", - "refactor", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:07:31.053Z" - }, - { - "id": "bug-026", - "timestamp": "2026-04-28T01:07:57.167Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/utils/token-manager.ts", - "root_cause": "4 lines replaced/restructured", - "fix": "Rewrote 6→6 lines (4 removed)", - "tags": [ - "auto-detected", - "refactor", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:07:57.167Z" - }, - { - "id": "bug-027", - "timestamp": "2026-04-28T01:09:25.096Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/tests/visual/dashboard-visual.spec.ts", - "root_cause": "Had '../../fixtures/auth.fixtures'", - "fix": "Changed to '../../config/test-config'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:09:25.096Z" - }, - { - "id": "bug-028", - "timestamp": "2026-04-28T01:09:25.182Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/tests/ai/ai-assistant.spec.ts", - "root_cause": "Had '../../fixtures/auth.fixtures'", - "fix": "Changed to '../../page-objects/ai-assistant.page'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:09:25.182Z" - }, - { - "id": "bug-029", - "timestamp": "2026-04-28T01:09:33.507Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/tests/ai/ai-hr-insight.spec.ts", - "root_cause": "Had '../../fixtures/auth.fixtures'", - "fix": "Changed to '../../page-objects/ai-hr-insight.page'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:09:33.507Z" - }, - { - "id": "bug-030", - "timestamp": "2026-04-28T01:09:34.340Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/tests/visual/forms-visual.spec.ts", - "root_cause": "Had '../../fixtures/auth.fixtures'", - "fix": "Changed to '../../config/test-config'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:09:34.340Z" - }, - { - "id": "bug-031", - "timestamp": "2026-04-28T01:09:50.908Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/tests/accessibility/aria-labels.spec.ts", - "root_cause": "9 lines replaced/restructured", - "fix": "Rewrote 12→4 lines (9 removed)", - "tags": [ - "auto-detected", - "refactor", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:09:50.908Z" - }, - { - "id": "bug-032", - "timestamp": "2026-04-28T01:09:52.830Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/tests/ai/ai-nl-search.spec.ts", - "root_cause": "Had '../../fixtures/auth.fixtures'", - "fix": "Changed to '../../page-objects/ai-nl-search.page'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:09:52.830Z" - }, - { - "id": "bug-033", - "timestamp": "2026-04-28T01:09:55.269Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/tests/accessibility/keyboard-navigation.spec.ts", - "root_cause": "Had '../../fixtures/auth.fixtures'", - "fix": "Changed to '../../page-objects/employee-form.page'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:09:55.269Z" - }, - { - "id": "bug-034", - "timestamp": "2026-04-28T01:09:59.701Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/tests/accessibility/keyboard-navigation.spec.ts", - "root_cause": "9 lines replaced/restructured", - "fix": "Rewrote 12→4 lines (9 removed)", - "tags": [ - "auto-detected", - "refactor", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:09:59.702Z" - }, - { - "id": "bug-035", - "timestamp": "2026-04-28T01:10:04.321Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/tests/ai/ai-vector-search.spec.ts", - "root_cause": "Had '../../fixtures/auth.fixtures'", - "fix": "Changed to '../../page-objects/ai-vector-search.page'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:10:04.321Z" - }, - { - "id": "bug-036", - "timestamp": "2026-04-28T01:15:41.133Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/playwright.config.ts", - "root_cause": "2 lines replaced/restructured", - "fix": "Rewrote 6→14 lines (2 removed)", - "tags": [ - "auto-detected", - "refactor", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:15:41.133Z" - }, - { - "id": "bug-037", - "timestamp": "2026-04-28T01:15:51.145Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/package.json", - "root_cause": "4 lines replaced/restructured", - "fix": "Rewrote 8→10 lines (4 removed)", - "tags": [ - "auto-detected", - "refactor", - "json" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:15:51.145Z" - }, - { - "id": "bug-038", - "timestamp": "2026-04-28T01:18:14.566Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/config/environments.json", - "root_cause": "5 lines replaced/restructured", - "fix": "Rewrote 8→9 lines (5 removed)", - "tags": [ - "auto-detected", - "refactor", - "json" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:18:14.566Z" - }, - { - "id": "bug-039", - "timestamp": "2026-04-28T01:24:04.116Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/fixtures/auth.fixtures.ts", - "root_cause": "Had 'a:has-text(\"", - "fix": "Changed to `a:has-text(\"", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-28T01:27:31.169Z" - }, - { - "id": "bug-040", - "timestamp": "2026-04-28T01:25:07.366Z", - "error_message": "Missing await", - "file": "Tests/AngularNetTutorial-Playwright/config/test-config.ts", - "root_cause": "Async call without await — returned Promise instead of value", - "fix": "Added await to async call", - "tags": [ - "auto-detected", - "async-fix", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:25:07.366Z" - }, - { - "id": "bug-041", - "timestamp": "2026-04-28T01:26:13.761Z", - "error_message": "Wrong condition in logic", - "file": "Tests/AngularNetTutorial-Playwright/tests/diagnostic.spec.ts", - "root_cause": "Condition was: if (currentUrl.includes('localhost:4200')", - "fix": "Changed to: if (currentUrl.includes(new URL(APP_URLS.angular)", - "tags": [ - "auto-detected", - "logic-fix", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:26:13.761Z" - }, - { - "id": "bug-042", - "timestamp": "2026-04-28T01:26:27.368Z", - "error_message": "Wrong reference: localhost should be getAngularUrlPattern", - "file": "Tests/AngularNetTutorial-Playwright/tests/auth/login.spec.ts", - "root_cause": "Used \"localhost\" instead of \"getAngularUrlPattern\"", - "fix": "Changed localhost → getAngularUrlPattern", - "tags": [ - "auto-detected", - "wrong-reference", - "ts" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-28T01:26:33.257Z" - }, - { - "id": "bug-043", - "timestamp": "2026-04-28T01:26:44.009Z", - "error_message": "Wrong reference: localhost should be getAngularUrlPattern", - "file": "Tests/AngularNetTutorial-Playwright/tests/navigation/routing.spec.ts", - "root_cause": "Used \"localhost\" instead of \"getAngularUrlPattern\"", - "fix": "Changed localhost → getAngularUrlPattern", - "tags": [ - "auto-detected", - "wrong-reference", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:26:44.009Z" - }, - { - "id": "bug-044", - "timestamp": "2026-04-28T01:26:55.903Z", - "error_message": "Wrong reference: localhost should be getAngularUrlPattern", - "file": "Tests/AngularNetTutorial-Playwright/tests/workflows/manager-daily-tasks.spec.ts", - "root_cause": "Used \"localhost\" instead of \"getAngularUrlPattern\"", - "fix": "Changed localhost → getAngularUrlPattern", - "tags": [ - "auto-detected", - "wrong-reference", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:26:55.903Z" - }, - { - "id": "bug-045", - "timestamp": "2026-04-28T01:27:06.245Z", - "error_message": "Wrong reference: localhost should be getAngularUrlPattern", - "file": "Tests/AngularNetTutorial-Playwright/tests/workflows/complete-employee-workflow.spec.ts", - "root_cause": "Used \"localhost\" instead of \"getAngularUrlPattern\"", - "fix": "Changed localhost → getAngularUrlPattern", - "tags": [ - "auto-detected", - "wrong-reference", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:27:06.245Z" - }, - { - "id": "bug-046", - "timestamp": "2026-04-28T01:27:16.938Z", - "error_message": "Wrong reference: localhost should be getAngularUrlPattern", - "file": "Tests/AngularNetTutorial-Playwright/tests/workflows/hradmin-operations.spec.ts", - "root_cause": "Used \"localhost\" instead of \"getAngularUrlPattern\"", - "fix": "Changed localhost → getAngularUrlPattern", - "tags": [ - "auto-detected", - "wrong-reference", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:27:16.938Z" - }, - { - "id": "bug-047", - "timestamp": "2026-04-28T01:48:42.968Z", - "error_message": "Incorrect value in code", - "file": "Tests/AngularNetTutorial-Playwright/fixtures/auth.fixtures.ts", - "root_cause": "Had 'networkidle'", - "fix": "Changed to 'domcontentloaded'", - "tags": [ - "auto-detected", - "wrong-value", - "ts" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-28T01:48:46.422Z" - }, - { - "id": "bug-048", - "timestamp": "2026-04-28T01:50:16.302Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/fixtures/auth.fixtures.ts", - "root_cause": "3 lines replaced/restructured", - "fix": "Rewrote 18→22 lines (3 removed)", - "tags": [ - "auto-detected", - "refactor", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:50:16.302Z" - }, - { - "id": "bug-049", - "timestamp": "2026-04-28T01:52:37.674Z", - "error_message": "Missing error handling in function", - "file": "Tests/AngularNetTutorial-Playwright/fixtures/auth.fixtures.ts", - "root_cause": "Code path had no error handling — exceptions would propagate uncaught", - "fix": "Added try/catch block", - "tags": [ - "auto-detected", - "error-handling", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:52:37.674Z" - }, - { - "id": "bug-050", - "timestamp": "2026-04-28T01:52:42.769Z", - "error_message": "Type error", - "file": "Tests/AngularNetTutorial-Playwright/tests/employee-management/employee-smoke.spec.ts", - "root_cause": "Missing or incorrect type annotation", - "fix": "Added type assertion/annotation", - "tags": [ - "auto-detected", - "type-fix", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:52:42.769Z" - }, - { - "id": "bug-051", - "timestamp": "2026-04-28T01:57:12.310Z", - "error_message": "Type error", - "file": "Tests/AngularNetTutorial-Playwright/fixtures/auth.fixtures.ts", - "root_cause": "Missing or incorrect type annotation", - "fix": "Added type assertion/annotation", - "tags": [ - "auto-detected", - "type-fix", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:57:12.310Z" - }, - { - "id": "bug-052", - "timestamp": "2026-04-28T01:57:33.003Z", - "error_message": "Significant refactor of ", - "file": "Tests/AngularNetTutorial-Playwright/tests/employee-management/employee-smoke.spec.ts", - "root_cause": "2 lines replaced/restructured", - "fix": "Rewrote 3→5 lines (2 removed)", - "tags": [ - "auto-detected", - "refactor", - "ts" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T01:57:33.003Z" - }, - { - "id": "bug-053", - "timestamp": "2026-04-28T01:58:28.569Z", - "error_message": "Type error", - "file": "Tests/AngularNetTutorial-Playwright/tests/employee-management/employee-smoke.spec.ts", - "root_cause": "Missing or incorrect type annotation", - "fix": "Added type assertion/annotation | Also: // test.fixme: The employee list table is populated from the; // (https://localhost:44378). Currently the API returns ERR_", - "tags": [ - "auto-detected", - "type-fix", - "ts" - ], - "related_bugs": [], - "occurrences": 2, - "last_seen": "2026-04-28T01:58:32.878Z" - }, - { - "id": "bug-054", - "timestamp": "2026-04-28T02:28:40.719Z", - "error_message": "Incorrect value in code", - "file": ".vscode/settings.json", - "root_cause": "Had \"playwright.configFile\"", - "fix": "Changed to \"playwright.workingDirectory\"", - "tags": [ - "auto-detected", - "wrong-value", - "json" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T02:28:40.719Z" - }, - { - "id": "bug-055", - "timestamp": "2026-04-28T02:35:14.419Z", - "error_message": "Incorrect value in code", - "file": ".vscode/settings.json", - "root_cause": "Had \"playwright.workingDirectory\"", - "fix": "Changed to \"playwright.configFile\"", - "tags": [ - "auto-detected", - "wrong-value", - "json" - ], - "related_bugs": [], - "occurrences": 1, - "last_seen": "2026-04-28T02:35:14.419Z" - } - ] -} \ No newline at end of file diff --git a/.wolf/cerebrum.md b/.wolf/cerebrum.md deleted file mode 100644 index 895349b..0000000 --- a/.wolf/cerebrum.md +++ /dev/null @@ -1,23 +0,0 @@ -# Cerebrum - -> OpenWolf's learning memory. Updated automatically as the AI learns from interactions. -> Do not edit manually unless correcting an error. -> Last updated: 2026-04-21 - -## User Preferences - - - -## Key Learnings - -- **Project:** angularnettutotial -- **Description:** This repository demonstrates the **Client, API Resource, and Token Service (CAT)** pattern using Angular and .NET technologies. - -## Do-Not-Repeat - - - - -## Decision Log - - diff --git a/.wolf/config.json b/.wolf/config.json deleted file mode 100644 index d2c76ae..0000000 --- a/.wolf/config.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "version": 1, - "openwolf": { - "enabled": true, - "anatomy": { - "auto_scan_on_init": true, - "rescan_interval_hours": 6, - "max_description_length": 100, - "max_files": 500, - "exclude_patterns": [ - "node_modules", - ".git", - "dist", - "build", - ".wolf", - ".next", - ".nuxt", - "coverage", - "__pycache__", - ".cache", - "target", - ".vscode", - ".idea", - ".turbo", - ".vercel", - ".netlify", - ".output", - "*.min.js", - "*.min.css" - ] - }, - "token_audit": { - "enabled": true, - "report_frequency": "weekly", - "waste_threshold_percent": 15, - "chars_per_token_code": 3.5, - "chars_per_token_prose": 4.0 - }, - "cron": { - "enabled": true, - "max_retry_attempts": 3, - "dead_letter_enabled": true, - "heartbeat_interval_minutes": 30, - "use_claude_p": true, - "api_key_env": null - }, - "memory": { - "consolidation_after_days": 7, - "max_entries_before_consolidation": 200 - }, - "cerebrum": { - "max_tokens": 2000, - "reflection_frequency": "weekly" - }, - "daemon": { - "port": 18790, - "log_level": "info" - }, - "dashboard": { - "enabled": true, - "port": 18791 - }, - "designqc": { - "enabled": true, - "viewports": [ - { "name": "desktop", "width": 1440, "height": 900 }, - { "name": "mobile", "width": 375, "height": 812 } - ], - "max_screenshots": 6, - "chrome_path": null - } - } -} diff --git a/.wolf/cron-manifest.json b/.wolf/cron-manifest.json deleted file mode 100644 index 7e2d7f3..0000000 --- a/.wolf/cron-manifest.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "version": 1, - "tasks": [ - { - "id": "anatomy-rescan", - "name": "Full anatomy rescan", - "schedule": "0 */6 * * *", - "description": "Re-scans project filesystem and reconciles anatomy.md", - "action": { "type": "scan_project" }, - "retry": { - "max_attempts": 3, - "backoff": "exponential", - "base_delay_seconds": 30 - }, - "failsafe": { - "on_failure": "log_and_continue", - "alert_after_consecutive_failures": 2, - "dead_letter": true - }, - "enabled": true - }, - { - "id": "memory-consolidation", - "name": "Consolidate old memory", - "schedule": "0 2 * * *", - "description": "Compress memory.md entries older than 7 days", - "action": { - "type": "consolidate_memory", - "params": { "older_than_days": 7 } - }, - "retry": { - "max_attempts": 2, - "backoff": "exponential", - "base_delay_seconds": 60 - }, - "failsafe": { - "on_failure": "skip_and_retry_next_cycle", - "dead_letter": false - }, - "enabled": true - }, - { - "id": "token-audit", - "name": "Token audit report", - "schedule": "0 0 * * 1", - "description": "Weekly waste pattern detection", - "action": { "type": "generate_token_report" }, - "retry": { - "max_attempts": 2, - "backoff": "linear", - "base_delay_seconds": 60 - }, - "failsafe": { "on_failure": "log_and_continue", "dead_letter": true }, - "enabled": true - }, - { - "id": "cerebrum-reflection", - "name": "Cerebrum reflection", - "schedule": "0 3 * * 0", - "description": "Weekly AI review of cerebrum.md — prune stale entries, consolidate duplicates", - "action": { - "type": "ai_task", - "params": { - "prompt": "Review this cerebrum.md. Remove duplicate preferences (keep newer). Remove Do-Not-Repeat entries older than 90 days if no longer relevant. Consolidate Key Learnings that overlap. Keep the file under 2000 tokens. Return the cleaned file content only.", - "context_files": [".wolf/cerebrum.md"] - } - }, - "retry": { - "max_attempts": 1, - "backoff": "none", - "base_delay_seconds": 0 - }, - "failsafe": { "on_failure": "log_and_continue", "dead_letter": false }, - "enabled": true - }, - { - "id": "project-suggestions", - "name": "AI suggestions", - "schedule": "0 4 * * 1", - "description": "Weekly AI analysis with project improvement suggestions", - "action": { - "type": "ai_task", - "params": { - "prompt": "Based on the recent memory entries and current project structure, provide: 1) Key achievements this week, 2) Code improvements to consider, 3) Logical next tasks, 4) Technical debt or risks. Be specific and actionable. Return as JSON: {achievements:[], improvements:[], next_tasks:[], risks:[]}", - "context_files": [".wolf/memory.md", ".wolf/anatomy.md"] - } - }, - "retry": { - "max_attempts": 1, - "backoff": "none", - "base_delay_seconds": 0 - }, - "failsafe": { "on_failure": "log_and_continue", "dead_letter": false }, - "enabled": true - } - ] -} diff --git a/.wolf/cron-state.json b/.wolf/cron-state.json deleted file mode 100644 index ce4dcb7..0000000 --- a/.wolf/cron-state.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "last_heartbeat": null, - "engine_status": "initialized", - "execution_log": [], - "dead_letter_queue": [], - "upcoming": [] -} diff --git a/.wolf/designqc-report.json b/.wolf/designqc-report.json deleted file mode 100644 index 8f658ee..0000000 --- a/.wolf/designqc-report.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "captured_at": null, - "captures": [], - "total_size_kb": 0, - "estimated_tokens": 0 -} \ No newline at end of file diff --git a/.wolf/hooks/_session.json b/.wolf/hooks/_session.json deleted file mode 100644 index fdffbbb..0000000 --- a/.wolf/hooks/_session.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "session_id": "session-2026-05-04-1507", - "started": "2026-05-04T19:07:01.457Z", - "files_read": {}, - "files_written": [], - "edit_counts": {}, - "anatomy_hits": 0, - "anatomy_misses": 0, - "repeated_reads_warned": 0, - "cerebrum_warnings": 0, - "stop_count": 0 -} \ No newline at end of file diff --git a/.wolf/hooks/package.json b/.wolf/hooks/package.json deleted file mode 100644 index 3dbc1ca..0000000 --- a/.wolf/hooks/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/.wolf/hooks/post-read.js b/.wolf/hooks/post-read.js deleted file mode 100644 index 5d22fbb..0000000 --- a/.wolf/hooks/post-read.js +++ /dev/null @@ -1,69 +0,0 @@ -import * as path from "node:path"; -import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, estimateTokens, readStdin, normalizePath } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - const hooksDir = path.join(wolfDir, "hooks"); - const sessionFile = path.join(hooksDir, "_session.json"); - const raw = await readStdin(); - let input; - try { - input = JSON.parse(raw); - } - catch { - process.exit(0); - return; - } - const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; - const content = input.tool_output?.content ?? ""; - if (!filePath) { - process.exit(0); - return; - } - const normalizedFile = normalizePath(filePath); - // Skip tracking for .wolf/ internal files — consistent with pre-read - const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd()); - const relToProject = normalizedFile.startsWith(projectDir) - ? normalizedFile.slice(projectDir.length).replace(/^\//, "") - : ""; - if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) { - process.exit(0); - return; - } - const ext = path.extname(filePath).toLowerCase(); - const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".rs", ".go", ".java", ".c", ".cpp", ".css", ".json", ".yaml", ".yml"]); - const proseExts = new Set([".md", ".txt", ".rst"]); - const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed"; - let tokens = content ? estimateTokens(content, type) : 0; - // Fallback: if tool_output had no content, use anatomy token estimate - if (tokens === 0) { - const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md")); - const sections = parseAnatomy(anatomyContent); - for (const [sectionKey, entries] of sections) { - for (const entry of entries) { - const entryRelPath = normalizePath(path.join(sectionKey, entry.file)); - if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) { - tokens = entry.tokens; - break; - } - } - if (tokens > 0) - break; - } - } - const session = readJSON(sessionFile, { files_read: {} }); - if (session.files_read[normalizedFile]) { - session.files_read[normalizedFile].tokens = tokens; - } - else { - session.files_read[normalizedFile] = { - count: 1, - tokens, - first_read: new Date().toISOString(), - }; - } - writeJSON(sessionFile, session); - process.exit(0); -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=post-read.js.map \ No newline at end of file diff --git a/.wolf/hooks/post-write.js b/.wolf/hooks/post-write.js deleted file mode 100644 index 9a5ce97..0000000 --- a/.wolf/hooks/post-write.js +++ /dev/null @@ -1,503 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as crypto from "node:crypto"; -import { getWolfDir, ensureWolfDir, readJSON, writeJSON, parseAnatomy, serializeAnatomy, extractDescription, estimateTokens, appendMarkdown, timeShort, readStdin, normalizePath } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - const hooksDir = path.join(wolfDir, "hooks"); - const sessionFile = path.join(hooksDir, "_session.json"); - const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd(); - const raw = await readStdin(); - let input; - try { - input = JSON.parse(raw); - } - catch { - process.exit(0); - return; - } - const toolName = input.tool_name ?? "Write"; - const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; - if (!filePath) { - process.exit(0); - return; - } - const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath); - // Skip processing for .wolf/ internal files to avoid slow self-referential updates - const relPath = normalizePath(path.relative(projectRoot, absolutePath)); - if (relPath.startsWith(".wolf/")) { - process.exit(0); - return; - } - // Never track .env files in anatomy — they contain secrets - const baseName = path.basename(absolutePath); - if (baseName === ".env" || baseName.startsWith(".env.")) { - process.exit(0); - return; - } - const oldStr = input.tool_input?.old_string ?? ""; - const newStr = input.tool_input?.new_string ?? ""; - // 1. Update anatomy.md - try { - const anatomyPath = path.join(wolfDir, "anatomy.md"); - let anatomyContent; - try { - anatomyContent = fs.readFileSync(anatomyPath, "utf-8"); - } - catch { - anatomyContent = "# anatomy.md\n\n> Auto-maintained by OpenWolf.\n"; - } - const sections = parseAnatomy(anatomyContent); - const relPathLocal = normalizePath(path.relative(projectRoot, absolutePath)); - const dir = path.dirname(relPathLocal); - const fileName = path.basename(relPathLocal); - const sectionKey = dir === "." ? "./" : dir + "/"; - let fileContent = ""; - try { - fileContent = fs.readFileSync(absolutePath, "utf-8"); - } - catch { - fileContent = input.tool_input?.content ?? ""; - } - const desc = extractDescription(absolutePath).slice(0, 100); - const ext = path.extname(absolutePath).toLowerCase(); - const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css"]); - const proseExts = new Set([".md", ".txt", ".rst"]); - const type = codeExts.has(ext) ? "code" : proseExts.has(ext) ? "prose" : "mixed"; - const tokens = estimateTokens(fileContent, type); - if (!sections.has(sectionKey)) - sections.set(sectionKey, []); - const entries = sections.get(sectionKey); - const idx = entries.findIndex((e) => e.file === fileName); - if (idx !== -1) { - entries[idx] = { file: fileName, description: desc, tokens }; - } - else { - entries.push({ file: fileName, description: desc, tokens }); - } - let fileCount = 0; - for (const [, list] of sections) - fileCount += list.length; - const serialized = serializeAnatomy(sections, { - lastScanned: new Date().toISOString(), - fileCount, - hits: 0, - misses: 0, - }); - const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"; - try { - fs.writeFileSync(tmp, serialized, "utf-8"); - fs.renameSync(tmp, anatomyPath); - } - catch { - try { - fs.writeFileSync(anatomyPath, serialized, "utf-8"); - } - catch { } - try { - fs.unlinkSync(tmp); - } - catch { } - } - } - catch { } - // 2. Append richer entry to memory.md - try { - const action = toolName === "Write" ? "Created" : toolName === "MultiEdit" ? "Multi-edited" : "Edited"; - const relFile = normalizePath(path.relative(projectRoot, absolutePath)); - const fileContent = input.tool_input?.content ?? ""; - const ext = path.extname(absolutePath).toLowerCase(); - const codeExts = new Set([".ts", ".js", ".tsx", ".jsx", ".py", ".json", ".yaml", ".yml", ".css"]); - const type = codeExts.has(ext) ? "code" : "mixed"; - const writeTokens = estimateTokens(fileContent || newStr, type); - let changeDesc = ""; - if (oldStr && newStr) { - changeDesc = summarizeEdit(oldStr, newStr, baseName); - } - const memoryPath = path.join(wolfDir, "memory.md"); - const outcome = changeDesc || "—"; - appendMarkdown(memoryPath, `| ${timeShort()} | ${action} ${relFile} | ${outcome} | ~${writeTokens} |\n`); - } - catch { } - // 3. Record in session tracker + track edit counts - try { - const session = readJSON(sessionFile, { files_written: [], edit_counts: {} }); - if (!session.edit_counts) - session.edit_counts = {}; - const normalizedFile = normalizePath(filePath); - const action = toolName === "Write" ? "create" : "edit"; - const fileContent = input.tool_input?.content ?? ""; - const tokens = estimateTokens(fileContent || newStr, "code"); - session.files_written.push({ - file: normalizedFile, - action, - tokens, - at: new Date().toISOString(), - }); - const editKey = normalizePath(path.relative(projectRoot, absolutePath)); - session.edit_counts[editKey] = (session.edit_counts[editKey] || 0) + 1; - writeJSON(sessionFile, session); - if (session.edit_counts[editKey] >= 3) { - process.stderr.write(`⚠️ OpenWolf: ${baseName} has been edited ${session.edit_counts[editKey]} times this session. If you're fixing a bug, remember to log it to .wolf/buglog.json.\n`); - } - } - catch { } - // 4. Auto-detect bug-fix patterns and log them - try { - if (oldStr && newStr) { - autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr); - } - } - catch { } - process.exit(0); -} -// ─── Edit Summarizer ───────────────────────────────────────────── -function summarizeEdit(oldStr, newStr, filename) { - const oldLines = oldStr.split("\n"); - const newLines = newStr.split("\n"); - const oldCount = oldLines.length; - const newCount = newLines.length; - const ext = path.extname(filename).toLowerCase(); - // --- Structural fixes --- - if (newStr.includes("try") && newStr.includes("catch") && !oldStr.includes("catch")) { - return "added error handling"; - } - if (newStr.includes("?.") && !oldStr.includes("?.")) - return "added optional chaining"; - if (newStr.includes("?? ") && !oldStr.includes("?? ")) - return "added nullish coalescing"; - // --- Deleted code --- - if (!newStr.trim() || newStr.trim().length < oldStr.trim().length * 0.2) { - return `removed ${oldCount} lines`; - } - // --- Import changes --- - const oldImports = oldLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length; - const newImports = newLines.filter(l => /^\s*(import|require|use |from )/.test(l)).length; - if (newImports > oldImports && Math.abs(newCount - oldCount) <= newImports - oldImports + 1) { - return `added ${newImports - oldImports} import(s)`; - } - // --- Value/string replacement (common bug fix: wrong value) --- - if (oldCount === 1 && newCount === 1) { - const o = oldStr.trim(); - const n = newStr.trim(); - // String literal change - const oStr = o.match(/['"`]([^'"`]+)['"`]/); - const nStr = n.match(/['"`]([^'"`]+)['"`]/); - if (oStr && nStr && oStr[1] !== nStr[1]) { - return `"${oStr[1].slice(0, 25)}" → "${nStr[1].slice(0, 25)}"`; - } - // Number change - const oNum = o.match(/\b(\d+\.?\d*)\b/); - const nNum = n.match(/\b(\d+\.?\d*)\b/); - if (oNum && nNum && oNum[1] !== nNum[1] && o.replace(oNum[1], "") === n.replace(nNum[1], "")) { - return `${oNum[1]} → ${nNum[1]}`; - } - return "inline fix"; - } - // --- Method/function call changes --- - const oldCalls = extractCalls(oldStr); - const newCalls = extractCalls(newStr); - const addedCalls = newCalls.filter(c => !oldCalls.includes(c)); - const removedCalls = oldCalls.filter(c => !newCalls.includes(c)); - if (removedCalls.length === 1 && addedCalls.length === 1) { - return `${removedCalls[0]}() → ${addedCalls[0]}()`; - } - // --- CSS/style changes --- - if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") { - const oldProps = (oldStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, "")); - const newProps = (newStr.match(/[\w-]+\s*:/g) || []).map(p => p.replace(/\s*:/, "")); - const changed = newProps.filter(p => !oldProps.includes(p)); - if (changed.length > 0 && changed.length <= 3) { - return `CSS: ${changed.join(", ")}`; - } - } - // --- Condition changes --- - const oldConds = (oldStr.match(/if\s*\(([^)]+)\)/g) || []); - const newConds = (newStr.match(/if\s*\(([^)]+)\)/g) || []); - if (newConds.length > oldConds.length) { - return `added ${newConds.length - oldConds.length} condition(s)`; - } - // --- Function modified --- - const fnMatch = newStr.match(/(?:function|def|fn|func|async\s+function)\s+(\w+)/); - if (fnMatch) { - return `modified ${fnMatch[1]}()`; - } - // --- Class/method context --- - const methodMatch = newStr.match(/(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/); - if (methodMatch) { - return `modified ${methodMatch[1]}()`; - } - // --- Size-based fallback --- - if (newCount > oldCount + 5) - return `expanded (+${newCount - oldCount} lines)`; - if (oldCount > newCount + 5) - return `reduced (-${oldCount - newCount} lines)`; - return `${oldCount}→${newCount} lines`; -} -function extractCalls(code) { - return [...new Set((code.match(/(\w+)\s*\(/g) || []) - .map(m => m.match(/(\w+)/)?.[1] || "") - .filter(n => n.length > 2 && !["if", "for", "while", "switch", "catch", "function", "return", "new", "typeof", "instanceof", "const", "let", "var"].includes(n)))]; -} -// ─── Auto Bug Detection ────────────────────────────────────────── -function autoDetectBugFix(wolfDir, absolutePath, projectRoot, oldStr, newStr) { - const bugLogPath = path.join(wolfDir, "buglog.json"); - const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] }); - const relFile = normalizePath(path.relative(projectRoot, absolutePath)); - const basename = path.basename(absolutePath); - const ext = path.extname(basename).toLowerCase(); - // Detect what kind of fix this is - const detection = detectFixPattern(oldStr, newStr, ext); - if (!detection) - return; - // Check for recent duplicate (same file + same category within 5 min) - const recentDupe = bugLog.bugs.find(b => { - if (path.basename(b.file) !== basename) - return false; - if (!b.tags.includes("auto-detected")) - return false; - if (!b.tags.includes(detection.category)) - return false; - const bugTime = new Date(b.last_seen).getTime(); - return (Date.now() - bugTime) < 5 * 60 * 1000; - }); - if (recentDupe) { - recentDupe.occurrences++; - recentDupe.last_seen = new Date().toISOString(); - // Append additional context - if (detection.context && !recentDupe.fix.includes(detection.context)) { - recentDupe.fix += ` | Also: ${detection.context}`; - } - writeJSON(bugLogPath, bugLog); - return; - } - const nextId = `bug-${String(bugLog.bugs.length + 1).padStart(3, "0")}`; - bugLog.bugs.push({ - id: nextId, - timestamp: new Date().toISOString(), - error_message: detection.summary, - file: relFile, - root_cause: detection.rootCause, - fix: detection.fix, - tags: ["auto-detected", detection.category, ext.replace(".", "") || "unknown"], - related_bugs: [], - occurrences: 1, - last_seen: new Date().toISOString(), - }); - writeJSON(bugLogPath, bugLog); -} -function detectFixPattern(oldStr, newStr, ext) { - const oldLines = oldStr.split("\n"); - const newLines = newStr.split("\n"); - // --- Error handling added --- - if (newStr.includes("catch") && !oldStr.includes("catch")) { - const fn = newStr.match(/(?:function|def|async)\s+(\w+)/)?.[1] || "unknown"; - return { - category: "error-handling", - summary: `Missing error handling in ${path.basename(fn)}`, - rootCause: "Code path had no error handling — exceptions would propagate uncaught", - fix: `Added try/catch block`, - context: extractChangedLines(oldStr, newStr), - }; - } - // --- Null/undefined safety --- - if ((newStr.includes("?.") && !oldStr.includes("?.")) || - (newStr.includes("?? ") && !oldStr.includes("?? ")) || - (/!==?\s*(null|undefined)/.test(newStr) && !/!==?\s*(null|undefined)/.test(oldStr))) { - return { - category: "null-safety", - summary: `Null/undefined access in ${path.basename(path.basename(""))}`, - rootCause: "Property access on potentially null/undefined value", - fix: `Added null safety (optional chaining or null check)`, - context: extractChangedLines(oldStr, newStr), - }; - } - // --- Guard clause / early return added --- - if (/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(newStr) && - !/if\s*\([^)]*\)\s*(return|throw|continue|break)/.test(oldStr)) { - const condition = newStr.match(/if\s*\(([^)]+)\)/)?.[1]?.trim().slice(0, 60) || "condition"; - return { - category: "guard-clause", - summary: `Missing guard clause`, - rootCause: `No early return/throw for edge case: ${condition}`, - fix: `Added guard clause: if (${condition.slice(0, 40)})`, - }; - } - // --- Wrong value / string fix (very common bug) --- - if (oldLines.length <= 3 && newLines.length <= 3) { - const oldJoined = oldStr.trim(); - const newJoined = newStr.trim(); - // String literal changed - const oStrs = oldJoined.match(/['"`]([^'"`]{2,})['"`]/g) || []; - const nStrs = newJoined.match(/['"`]([^'"`]{2,})['"`]/g) || []; - if (oStrs.length > 0 && nStrs.length > 0) { - for (let i = 0; i < Math.min(oStrs.length, nStrs.length); i++) { - if (oStrs[i] !== nStrs[i]) { - return { - category: "wrong-value", - summary: `Incorrect value in code`, - rootCause: `Had ${oStrs[i].slice(0, 50)}`, - fix: `Changed to ${nStrs[i].slice(0, 50)}`, - }; - } - } - } - // Variable name / method call changed - const oldTokens = tokenizeCode(oldJoined); - const newTokens = tokenizeCode(newJoined); - const changed = []; - for (let i = 0; i < Math.min(oldTokens.length, newTokens.length); i++) { - if (oldTokens[i] !== newTokens[i]) { - changed.push([oldTokens[i], newTokens[i]]); - } - } - if (changed.length === 1 && changed[0][0].length > 2) { - return { - category: "wrong-reference", - summary: `Wrong reference: ${changed[0][0]} should be ${changed[0][1]}`, - rootCause: `Used "${changed[0][0]}" instead of "${changed[0][1]}"`, - fix: `Changed ${changed[0][0]} → ${changed[0][1]}`, - }; - } - } - // --- Logic fix (condition changed) --- - const oldCond = oldStr.match(/if\s*\(([^)]+)\)/)?.[1]; - const newCond = newStr.match(/if\s*\(([^)]+)\)/)?.[1]; - if (oldCond && newCond && oldCond !== newCond && oldLines.length <= 5) { - return { - category: "logic-fix", - summary: `Wrong condition in logic`, - rootCause: `Condition was: if (${oldCond.slice(0, 50)})`, - fix: `Changed to: if (${newCond.slice(0, 50)})`, - }; - } - // --- Operator fix (=== vs ==, > vs >=, etc.) --- - const opChange = findOperatorChange(oldStr, newStr); - if (opChange) { - return { - category: "operator-fix", - summary: `Wrong operator: ${opChange.old} should be ${opChange.new}`, - rootCause: `Used "${opChange.old}" instead of "${opChange.new}"`, - fix: `Changed operator ${opChange.old} → ${opChange.new}`, - }; - } - // --- Missing import/require --- - const oldImports = new Set((oldStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []).map(m => m)); - const newImports = (newStr.match(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g) || []); - const addedImports = newImports.filter(i => !oldImports.has(i)); - if (addedImports.length > 0 && newLines.length - oldLines.length <= addedImports.length + 2) { - const modules = addedImports.map(i => i.match(/['"]([^'"]+)['"]/)?.[1] || "").filter(Boolean); - return { - category: "missing-import", - summary: `Missing import: ${modules.join(", ")}`, - rootCause: `Module(s) not imported: ${modules.join(", ")}`, - fix: `Added import(s) for ${modules.join(", ")}`, - }; - } - // --- Return value fix --- - const oldReturn = oldStr.match(/return\s+(.+)/)?.[1]?.trim(); - const newReturn = newStr.match(/return\s+(.+)/)?.[1]?.trim(); - if (oldReturn && newReturn && oldReturn !== newReturn && oldLines.length <= 5) { - return { - category: "return-value", - summary: `Wrong return value`, - rootCause: `Was returning: ${oldReturn.slice(0, 50)}`, - fix: `Now returns: ${newReturn.slice(0, 50)}`, - }; - } - // --- Async/await fix --- - if (newStr.includes("await ") && !oldStr.includes("await ")) { - return { - category: "async-fix", - summary: `Missing await`, - rootCause: `Async call without await — returned Promise instead of value`, - fix: `Added await to async call`, - context: extractChangedLines(oldStr, newStr), - }; - } - if (newStr.includes("async ") && !oldStr.includes("async ")) { - return { - category: "async-fix", - summary: `Function not marked async`, - rootCause: `Function uses await but wasn't declared async`, - fix: `Added async modifier`, - }; - } - // --- Type annotation/cast fix --- - if (ext === ".ts" || ext === ".tsx") { - if ((newStr.includes(" as ") && !oldStr.includes(" as ")) || - (newStr.includes(": ") && !oldStr.includes(": ") && oldLines.length <= 3)) { - return { - category: "type-fix", - summary: `Type error`, - rootCause: `Missing or incorrect type annotation`, - fix: `Added type assertion/annotation`, - context: extractChangedLines(oldStr, newStr), - }; - } - } - // --- CSS/style fix --- - if (ext === ".css" || ext === ".scss" || ext === ".vue" || ext === ".tsx" || ext === ".jsx") { - const oldProps = extractCSSProps(oldStr); - const newProps = extractCSSProps(newStr); - const changedProps = [...newProps.entries()].filter(([k, v]) => oldProps.get(k) !== v && oldProps.has(k)); - if (changedProps.length > 0 && changedProps.length <= 3) { - const desc = changedProps.map(([k, v]) => `${k}: ${oldProps.get(k)} → ${v}`).join("; "); - return { - category: "style-fix", - summary: `CSS fix: ${changedProps.map(([k]) => k).join(", ")}`, - rootCause: desc, - fix: `Changed ${desc}`, - }; - } - } - // --- Significant diff (catch-all for substantial edits) --- - const diffRatio = Math.abs(newStr.length - oldStr.length) / Math.max(oldStr.length, 1); - if (diffRatio > 0.3 && oldLines.length >= 3 && newLines.length >= 3) { - // Only log if there's meaningful structural change, not just additions - const removedLines = oldLines.filter(l => l.trim() && !newLines.some(nl => nl.trim() === l.trim())); - if (removedLines.length >= 2) { - return { - category: "refactor", - summary: `Significant refactor of ${path.basename("")}`, - rootCause: `${removedLines.length} lines replaced/restructured`, - fix: `Rewrote ${oldLines.length}→${newLines.length} lines (${removedLines.length} removed)`, - context: removedLines.slice(0, 2).map(l => l.trim().slice(0, 50)).join("; "), - }; - } - } - return null; -} -function extractChangedLines(oldStr, newStr) { - const oldLines = new Set(oldStr.split("\n").map(l => l.trim()).filter(Boolean)); - const newLines = newStr.split("\n").map(l => l.trim()).filter(Boolean); - const added = newLines.filter(l => !oldLines.has(l)); - return added.slice(0, 2).map(l => l.slice(0, 60)).join("; "); -} -function tokenizeCode(code) { - return code.replace(/[^\w$]/g, " ").split(/\s+/).filter(t => t.length > 0); -} -function findOperatorChange(oldStr, newStr) { - const operators = ["===", "!==", "==", "!=", ">=", "<=", ">>", "<<", "&&", "||", "??"]; - for (const op of operators) { - if (oldStr.includes(op) && !newStr.includes(op)) { - for (const op2 of operators) { - if (op2 !== op && newStr.includes(op2) && !oldStr.includes(op2)) { - return { old: op, new: op2 }; - } - } - } - } - return null; -} -function extractCSSProps(code) { - const props = new Map(); - const matches = code.matchAll(/([\w-]+)\s*:\s*([^;}\n]+)/g); - for (const m of matches) { - props.set(m[1].trim(), m[2].trim()); - } - return props; -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=post-write.js.map \ No newline at end of file diff --git a/.wolf/hooks/pre-read.js b/.wolf/hooks/pre-read.js deleted file mode 100644 index 6ba67d0..0000000 --- a/.wolf/hooks/pre-read.js +++ /dev/null @@ -1,80 +0,0 @@ -import * as path from "node:path"; -import { getWolfDir, ensureWolfDir, readJSON, writeJSON, readMarkdown, parseAnatomy, readStdin, normalizePath } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - const hooksDir = path.join(wolfDir, "hooks"); - const sessionFile = path.join(hooksDir, "_session.json"); - const raw = await readStdin(); - let input; - try { - input = JSON.parse(raw); - } - catch { - process.exit(0); - return; - } - const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; - if (!filePath) { - process.exit(0); - return; - } - const normalizedFile = normalizePath(filePath); - // Skip tracking for .wolf/ internal files — they're infrastructure, not project files. - // Counting them inflates anatomy miss rates since .wolf/ is excluded from anatomy scanning. - const projectDir = normalizePath(process.env.CLAUDE_PROJECT_DIR || process.cwd()); - const relToProject = normalizedFile.startsWith(projectDir) - ? normalizedFile.slice(projectDir.length).replace(/^\//, "") - : ""; - if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) { - process.exit(0); - return; - } - const session = readJSON(sessionFile, { - session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0, - repeated_reads_warned: 0, - }); - // Check if already read this session - if (session.files_read[normalizedFile]) { - const prev = session.files_read[normalizedFile]; - process.stderr.write(`⚡ OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tokens). Consider using your existing knowledge of this file.\n`); - session.files_read[normalizedFile].count++; - session.repeated_reads_warned++; - writeJSON(sessionFile, session); - process.exit(0); - return; - } - // Check anatomy.md for this file - const anatomyContent = readMarkdown(path.join(wolfDir, "anatomy.md")); - const sections = parseAnatomy(anatomyContent); - let found = false; - for (const [sectionKey, entries] of sections) { - for (const entry of entries) { - // Build the full relative path from the section key + filename for accurate matching - const entryRelPath = normalizePath(path.join(sectionKey, entry.file)); - if (normalizedFile.endsWith(entryRelPath) || normalizedFile.endsWith("/" + entryRelPath)) { - process.stderr.write(`📋 OpenWolf anatomy: ${entry.file} — ${entry.description} (~${entry.tokens} tok)\n`); - found = true; - break; - } - } - if (found) - break; - } - if (found) { - session.anatomy_hits++; - } - else { - session.anatomy_misses++; - } - // Record initial read entry (tokens will be updated in post-read) - session.files_read[normalizedFile] = { - count: 1, - tokens: 0, - first_read: new Date().toISOString(), - }; - writeJSON(sessionFile, session); - process.exit(0); -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=pre-read.js.map \ No newline at end of file diff --git a/.wolf/hooks/pre-write.js b/.wolf/hooks/pre-write.js deleted file mode 100644 index 2e84303..0000000 --- a/.wolf/hooks/pre-write.js +++ /dev/null @@ -1,121 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { getWolfDir, ensureWolfDir, readJSON, readMarkdown, readStdin } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - const raw = await readStdin(); - let input; - try { - input = JSON.parse(raw); - } - catch { - process.exit(0); - return; - } - // For Edit tool, the meaningful content is old_string + new_string - const content = input.tool_input?.content ?? ""; - const oldStr = input.tool_input?.old_string ?? ""; - const newStr = input.tool_input?.new_string ?? ""; - const filePath = input.tool_input?.file_path ?? input.tool_input?.path ?? ""; - const allContent = [content, oldStr, newStr].join("\n"); - if (!allContent.trim()) { - process.exit(0); - return; - } - // 1. Cerebrum Do-Not-Repeat check - checkCerebrum(wolfDir, allContent); - // 2. Bug log: search for similar past bugs when editing code - // This fires when Claude is about to edit a file — if the edit looks like a fix - // (changing error handling, modifying catch blocks, etc.), check the bug log - if (filePath && (oldStr || content)) { - checkBugLog(wolfDir, filePath, oldStr, newStr, content); - } - process.exit(0); -} -function checkCerebrum(wolfDir, content) { - const cerebrumContent = readMarkdown(path.join(wolfDir, "cerebrum.md")); - const doNotRepeatSection = cerebrumContent.split("## Do-Not-Repeat")[1]; - if (!doNotRepeatSection) - return; - const entries = doNotRepeatSection.split("## ")[0]; - const lines = entries.split("\n").filter((l) => l.trim().startsWith("[") || l.trim().startsWith("-")); - for (const line of lines) { - const trimmed = line.trim().replace(/^[-*]\s*/, "").replace(/^\[[\d-]+\]\s*/, ""); - if (!trimmed) - continue; - const patterns = []; - const quotedMatches = trimmed.match(/"([^"]+)"/g) || trimmed.match(/'([^']+)'/g) || trimmed.match(/`([^`]+)`/g); - if (quotedMatches) { - for (const qm of quotedMatches) { - patterns.push(qm.replace(/["'`]/g, "")); - } - } - const neverMatch = trimmed.match(/(?:never use|avoid|don't use|do not use)\s+(\w+)/i); - if (neverMatch) - patterns.push(neverMatch[1]); - for (const pattern of patterns) { - try { - const regex = new RegExp(`\\b${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i"); - if (regex.test(content)) { - process.stderr.write(`⚠️ OpenWolf cerebrum warning: "${trimmed}" — check your code before proceeding.\n`); - } - } - catch { } - } - } -} -// Common words that appear in most code — must be excluded from similarity matching -const STOP_WORDS = new Set([ - "error", "function", "return", "const", "this", "that", "with", "from", - "import", "export", "class", "interface", "type", "undefined", "null", - "true", "false", "string", "number", "object", "array", "value", - "file", "path", "name", "data", "response", "request", "result", - "should", "must", "does", "have", "been", "will", "would", "could", - "when", "then", "else", "each", "some", "every", "only", -]); -function checkBugLog(wolfDir, filePath, oldStr, newStr, content) { - const bugLogPath = path.join(wolfDir, "buglog.json"); - if (!fs.existsSync(bugLogPath)) - return; - const bugLog = readJSON(bugLogPath, { version: 1, bugs: [] }); - if (bugLog.bugs.length === 0) - return; - const basename = path.basename(filePath); - // ONLY surface bugs that match the SAME file being edited. - // Cross-file matching is too noisy and risks misdirecting Claude. - const fileMatches = bugLog.bugs.filter(b => { - const bugBasename = path.basename(b.file); - return bugBasename === basename; - }); - if (fileMatches.length === 0) - return; - // Further filter: require tag or error_message overlap with the edit content - const editText = (oldStr + " " + newStr + " " + content).toLowerCase(); - const editTokens = tokenize(editText); - const relevant = fileMatches.filter(bug => { - // Check if any bug tag appears in the edit content - const tagHit = bug.tags.some(t => editText.includes(t.toLowerCase())); - if (tagHit) - return true; - // Check meaningful word overlap (excluding stop words) - const bugTokens = tokenize(bug.error_message + " " + bug.root_cause); - const overlap = [...editTokens].filter(t => bugTokens.has(t)); - // Require at least 3 meaningful overlapping words - return overlap.length >= 3; - }); - if (relevant.length === 0) - return; - // Surface as a FYI, not a directive — Claude should evaluate, not blindly apply - process.stderr.write(`📋 OpenWolf buglog: ${relevant.length} past bug(s) found for ${basename} — review for context, do NOT apply blindly:\n`); - for (const bug of relevant.slice(0, 2)) { - process.stderr.write(` [${bug.id}] "${bug.error_message.slice(0, 70)}"\n Cause: ${bug.root_cause.slice(0, 80)}\n Fix: ${bug.fix.slice(0, 80)}\n`); - } -} -function tokenize(text) { - return new Set(text.replace(/[^\w\s]/g, " ").split(/\s+/) - .filter(w => w.length > 3 && !STOP_WORDS.has(w.toLowerCase())) - .map(w => w.toLowerCase())); -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=pre-write.js.map \ No newline at end of file diff --git a/.wolf/hooks/session-start.js b/.wolf/hooks/session-start.js deleted file mode 100644 index 7da38f3..0000000 --- a/.wolf/hooks/session-start.js +++ /dev/null @@ -1,77 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { getWolfDir, ensureWolfDir, writeJSON, appendMarkdown, readJSON, timestamp, timeShort } from "./shared.js"; -async function main() { - ensureWolfDir(); - const wolfDir = getWolfDir(); - // Clean up stale .tmp files left from failed atomic writes - try { - const files = fs.readdirSync(wolfDir); - for (const f of files) { - if (f.endsWith(".tmp")) { - try { - fs.unlinkSync(path.join(wolfDir, f)); - } - catch { } - } - } - } - catch { } - const hooksDir = path.join(wolfDir, "hooks"); - const sessionFile = path.join(hooksDir, "_session.json"); - const now = new Date(); - const sessionId = `session-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`; - // Create fresh session state - writeJSON(sessionFile, { - session_id: sessionId, - started: timestamp(), - files_read: {}, - files_written: [], - edit_counts: {}, - anatomy_hits: 0, - anatomy_misses: 0, - repeated_reads_warned: 0, - cerebrum_warnings: 0, - stop_count: 0, - }); - // Append session header to memory.md - const memoryPath = path.join(wolfDir, "memory.md"); - const header = `\n## Session: ${now.toISOString().slice(0, 10)} ${timeShort()}\n\n| Time | Action | File(s) | Outcome | ~Tokens |\n|------|--------|---------|---------|--------|\n`; - appendMarkdown(memoryPath, header); - // Check cerebrum freshness — remind Claude to learn - try { - const cerebrumPath = path.join(wolfDir, "cerebrum.md"); - const cerebrumContent = fs.readFileSync(cerebrumPath, "utf-8"); - const stat = fs.statSync(cerebrumPath); - const daysSinceUpdate = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24); - // Count actual entries (non-comment, non-empty lines in content sections) - const entryLines = cerebrumContent.split("\n").filter(l => { - const t = l.trim(); - return t.startsWith("- ") || t.startsWith("* ") || (t.startsWith("[") && t.includes("]")); - }); - if (entryLines.length < 3) { - process.stderr.write(`💡 OpenWolf: cerebrum.md has only ${entryLines.length} entries. Learn from this session — record user preferences, project conventions, and mistakes to .wolf/cerebrum.md.\n`); - } - else if (daysSinceUpdate > 3) { - process.stderr.write(`💡 OpenWolf: cerebrum.md hasn't been updated in ${Math.floor(daysSinceUpdate)} days. Look for opportunities to add learnings this session.\n`); - } - } - catch { } - // Check buglog — remind if empty - try { - const buglogPath = path.join(wolfDir, "buglog.json"); - const buglog = readJSON(buglogPath, { bugs: [] }); - if (buglog.bugs.length === 0) { - process.stderr.write(`📋 OpenWolf: buglog.json is empty. If you encounter or fix any bugs, errors, or failed tests this session, log them to .wolf/buglog.json.\n`); - } - } - catch { } - // Increment total_sessions in token-ledger - const ledgerPath = path.join(wolfDir, "token-ledger.json"); - const ledger = readJSON(ledgerPath, { version: 1, lifetime: { total_sessions: 0 } }); - ledger.lifetime.total_sessions++; - writeJSON(ledgerPath, ledger); - process.exit(0); -} -main().catch(() => process.exit(0)); -//# sourceMappingURL=session-start.js.map \ No newline at end of file diff --git a/.wolf/hooks/shared.js b/.wolf/hooks/shared.js deleted file mode 100644 index e402d75..0000000 --- a/.wolf/hooks/shared.js +++ /dev/null @@ -1,614 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as crypto from "node:crypto"; -export function getWolfDir() { - // Prefer CLAUDE_PROJECT_DIR so hooks work even if CWD changes during a session - const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); - return path.join(projectDir, ".wolf"); -} -/** - * Bail out silently if .wolf/ directory doesn't exist in the current project. - * Call this at the top of every hook to avoid crashes in non-OpenWolf projects. - */ -export function ensureWolfDir() { - const wolfDir = getWolfDir(); - if (!fs.existsSync(wolfDir)) { - process.exit(0); - } -} -export function readJSON(filePath, fallback) { - try { - return JSON.parse(fs.readFileSync(filePath, "utf-8")); - } - catch { - return fallback; - } -} -export function writeJSON(filePath, data) { - const dir = path.dirname(filePath); - if (!fs.existsSync(dir)) - fs.mkdirSync(dir, { recursive: true }); - const tmp = filePath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"; - try { - fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8"); - fs.renameSync(tmp, filePath); - } - catch { - // On Windows, rename can fail if another process holds a handle. - // Fall back to direct write and clean up the tmp file. - try { - fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); - } - catch { } - try { - fs.unlinkSync(tmp); - } - catch { } - } -} -export function readMarkdown(filePath) { - try { - return fs.readFileSync(filePath, "utf-8"); - } - catch { - return ""; - } -} -export function appendMarkdown(filePath, line) { - const dir = path.dirname(filePath); - if (!fs.existsSync(dir)) - fs.mkdirSync(dir, { recursive: true }); - fs.appendFileSync(filePath, line, "utf-8"); -} -export function parseAnatomy(content) { - const sections = new Map(); - let currentSection = ""; - for (const line of content.split("\n")) { - const sm = line.match(/^## (.+)/); - if (sm) { - currentSection = sm[1].trim(); - if (!sections.has(currentSection)) - sections.set(currentSection, []); - continue; - } - if (!currentSection) - continue; - const em = line.match(/^- `([^`]+)`(?:\s+—\s+(.+?))?\s*\(~(\d+)\s+tok\)$/); - if (em) { - sections.get(currentSection).push({ - file: em[1], - description: em[2] || "", - tokens: parseInt(em[3], 10), - }); - } - } - return sections; -} -export function serializeAnatomy(sections, metadata) { - const lines = [ - "# anatomy.md", - "", - `> Auto-maintained by OpenWolf. Last scanned: ${metadata.lastScanned}`, - `> Files: ${metadata.fileCount} tracked | Anatomy hits: ${metadata.hits} | Misses: ${metadata.misses}`, - "", - ]; - const keys = [...sections.keys()].sort(); - for (const key of keys) { - lines.push(`## ${key}`); - lines.push(""); - const entries = sections.get(key).sort((a, b) => a.file.localeCompare(b.file)); - for (const e of entries) { - const desc = e.description ? ` — ${e.description}` : ""; - lines.push(`- \`${e.file}\`${desc} (~${e.tokens} tok)`); - } - lines.push(""); - } - return lines.join("\n"); -} -export function extractDescription(filePath) { - const MAX_DESC = 150; - const basename = path.basename(filePath); - const ext = path.extname(basename).toLowerCase(); - const known = { - "package.json": "Node.js package manifest", - "tsconfig.json": "TypeScript configuration", - ".gitignore": "Git ignore rules", - "README.md": "Project documentation", - "composer.json": "PHP package manifest", - "requirements.txt": "Python dependencies", - "schema.sql": "Database schema", - "Dockerfile": "Docker container definition", - "docker-compose.yml": "Docker Compose services", - "Cargo.toml": "Rust package manifest", - "go.mod": "Go module definition", - "Gemfile": "Ruby dependencies", - "pubspec.yaml": "Dart/Flutter package manifest", - }; - if (known[basename]) - return known[basename]; - let content; - try { - const fd = fs.openSync(filePath, "r"); - const buf = Buffer.alloc(12288); // 12KB - const n = fs.readSync(fd, buf, 0, 12288, 0); - fs.closeSync(fd); - content = buf.subarray(0, n).toString("utf-8"); - } - catch { - return ""; - } - if (!content.trim()) - return ""; - const cap = (s) => s.length <= MAX_DESC ? s : s.slice(0, MAX_DESC - 3) + "..."; - // Markdown heading - if (ext === ".md" || ext === ".mdx") { - const m = content.match(/^#{1,2}\s+(.+)$/m); - if (m) - return cap(m[1].trim()); - } - // HTML title - if (ext === ".html" || ext === ".htm") { - const m = content.match(/]*>([^<]+)<\/title>/i); - if (m) - return cap(m[1].trim()); - } - // JSDoc / PHPDoc / Javadoc — first meaningful line - const jm = content.match(/\/\*\*\s*\n?\s*\*?\s*(.+)/); - if (jm) { - const l = jm[1].replace(/\*\/$/, "").trim(); - if (l && !l.startsWith("@") && l.length > 5) - return cap(l); - } - // Python docstring - if (ext === ".py") { - const dm = content.match(/^(?:#[^\n]*\n)*\s*(?:"""(.+?)"""|'''(.+?)''')/s); - if (dm) { - const first = (dm[1] || dm[2]).split("\n")[0].trim(); - if (first && first.length > 3) - return cap(first); - } - } - // Rust doc comments - if (ext === ".rs") { - const lines = content.split("\n"); - for (const line of lines.slice(0, 20)) { - const m = line.match(/^\s*(?:\/\/\/|\/\/!)\s*(.+)/); - if (m && m[1].length > 5) - return cap(m[1].trim()); - } - } - // Go package comment - if (ext === ".go") { - const m = content.match(/\/\/\s*Package\s+\w+\s+(.*)/); - if (m) - return cap(m[1].trim()); - } - // C# XML doc - if (ext === ".cs") { - const m = content.match(/\s*([\s\S]*?)\s*<\/summary>/); - if (m) { - const text = m[1].replace(/\/\/\/\s*/g, "").replace(/\s+/g, " ").trim(); - if (text.length > 5) - return cap(text); - } - } - // Elixir @moduledoc - if (ext === ".ex" || ext === ".exs") { - const m = content.match(/@moduledoc\s+"""\s*\n\s*(.*)/); - if (m) - return cap(m[1].trim()); - } - // Header comment (skip generic ones) - const hdrLines = content.split("\n"); - for (const line of hdrLines.slice(0, 15)) { - const t = line.trim(); - if (!t || t === " 5 && !lower.startsWith("copyright") && !lower.startsWith("license") && !lower.startsWith("@") && !lower.startsWith("strict") && !lower.startsWith("generated") && !lower.startsWith("eslint-") && !lower.startsWith("nolint")) { - return cap(text); - } - } - if (!t.startsWith("//") && !t.startsWith("#") && !t.startsWith("/*") && !t.startsWith("*") && !t.startsWith("--")) - break; - } - // ─── PHP / Laravel ─────────────────────────────────────── - if (ext === ".php") { - if (basename.endsWith(".blade.php")) { - const ext2 = content.match(/@extends\(\s*['"]([^'"]+)['"]\s*\)/); - const sections = (content.match(/@section\(\s*['"](\w+)['"]/g) || []).map(s => s.match(/['"](\w+)['"]/)?.[1]).filter(Boolean); - const parts = []; - if (ext2) - parts.push(`extends ${ext2[1]}`); - if (sections.length) - parts.push(`sections: ${sections.join(", ")}`); - return cap(parts.length ? `Blade: ${parts.join(", ")}` : "Blade template"); - } - const classM = content.match(/class\s+(\w+)(?:\s+extends\s+(\w+))?/); - const className = classM?.[1] || ""; - const parent = classM?.[2] || ""; - const pubMethods = (content.match(/public\s+function\s+(\w+)/g) || []) - .map(m => m.match(/public\s+function\s+(\w+)/)?.[1]) - .filter(n => n && n !== "__construct" && n !== "middleware"); - if (basename.endsWith("Controller.php") || parent === "Controller") { - if (pubMethods.length > 0) { - const display = pubMethods.slice(0, 5).join(", "); - return cap(pubMethods.length > 5 ? `${display} + ${pubMethods.length - 5} more` : display); - } - } - if (parent === "Model" || parent === "Authenticatable") { - const parts = []; - const tbl = content.match(/\$table\s*=\s*['"]([^'"]+)['"]/); - if (tbl) - parts.push(`table: ${tbl[1]}`); - const fill = content.match(/\$fillable\s*=\s*\[([^\]]*)\]/s); - if (fill) { - const c = (fill[1].match(/['"]/g) || []).length / 2; - parts.push(`${Math.floor(c)} fields`); - } - const rels = (content.match(/\$this->(hasMany|hasOne|belongsTo|belongsToMany|morphMany|morphTo)\(/g) || []).length; - if (rels) - parts.push(`${rels} rels`); - return cap(parts.length ? `Model — ${parts.join(", ")}` : `Model: ${className}`); - } - if (basename.match(/^\d{4}_\d{2}_\d{2}/)) { - const create = content.match(/Schema::create\(\s*['"]([^'"]+)['"]/); - if (create) - return `Migration: create ${create[1]} table`; - const alter = content.match(/Schema::table\(\s*['"]([^'"]+)['"]/); - if (alter) - return `Migration: alter ${alter[1]} table`; - return "Database migration"; - } - if (className && pubMethods.length > 0) { - const display = pubMethods.slice(0, 4).join(", "); - return cap(pubMethods.length > 4 ? `${className}: ${display} + ${pubMethods.length - 4} more` : `${className}: ${display}`); - } - } - // ─── TS/JS/React/Next.js ───────────────────────────────── - if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") { - // React component - if (ext === ".tsx" || ext === ".jsx") { - const comp = content.match(/(?:export\s+(?:default\s+)?)?(?:function|const)\s+(\w+)/); - const parts = []; - if (comp) - parts.push(comp[1]); - const renders = []; - if (/<(?:form|Form)/i.test(content)) - renders.push("form"); - if (/<(?:table|Table|DataTable)/i.test(content)) - renders.push("table"); - if (/<(?:dialog|Dialog|Modal|Drawer)/i.test(content)) - renders.push("modal"); - if (renders.length) - parts.push(`renders ${renders.join(", ")}`); - if (parts.length) - return cap(parts.join(" — ")); - } - // Next.js conventions - if (basename === "page.tsx" || basename === "page.js") - return "Next.js page component"; - if (basename === "layout.tsx" || basename === "layout.js") - return "Next.js layout"; - if (basename === "route.ts" || basename === "route.js") { - const methods = [...new Set((content.match(/export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)/g) || []) - .map(m => m.match(/(GET|POST|PUT|PATCH|DELETE)/)?.[1]))].filter(Boolean); - return methods.length ? `Next.js API route: ${methods.join(", ")}` : "Next.js API route"; - } - // Express/Fastify routes - const routeHits = content.match(/\.(get|post|put|patch|delete)\s*\(\s*['"`]/g); - if (routeHits && routeHits.length > 0) { - const methods = [...new Set(routeHits.map(r => r.match(/\.(get|post|put|patch|delete)/)?.[1]?.toUpperCase()))]; - return cap(`API routes: ${methods.join(", ")} (${routeHits.length} endpoints)`); - } - // tRPC router - if (content.includes("createTRPCRouter") || content.includes("publicProcedure")) { - const procs = (content.match(/\.(query|mutation|subscription)\s*\(/g) || []).length; - return procs ? `tRPC router: ${procs} procedures` : "tRPC router"; - } - // Zod schemas - if (content.includes("z.object") || content.includes("z.string")) { - const schemas = (content.match(/(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*z\./g) || []) - .map(s => s.match(/(?:const|let)\s+(\w+)/)?.[1]).filter(Boolean); - if (schemas.length) - return cap(`Zod schemas: ${schemas.slice(0, 4).join(", ")}${schemas.length > 4 ? ` + ${schemas.length - 4} more` : ""}`); - } - // Exports summary - const exports = (content.match(/export\s+(?:async\s+)?(?:function|class|const|interface|type|enum)\s+(\w+)/g) || []) - .map(e => e.match(/(\w+)$/)?.[1]).filter(Boolean); - if (exports.length > 0 && exports.length <= 5) - return `Exports ${exports.join(", ")}`; - if (exports.length > 5) - return cap(`Exports ${exports.slice(0, 4).join(", ")} + ${exports.length - 4} more`); - } - // ─── Python / Django / FastAPI / Flask ──────────────────── - if (ext === ".py") { - // Django model - if (content.includes("models.Model")) { - const cls = content.match(/class\s+(\w+)\(.*models\.Model\)/); - const fields = (content.match(/^\s+\w+\s*=\s*models\.\w+/gm) || []).length; - return cap(`Model: ${cls?.[1] || "unknown"}, ${fields} fields`); - } - // FastAPI/Flask routes - if (content.includes("@router.") || content.includes("@app.")) { - const routes = (content.match(/@(?:router|app)\.(get|post|put|patch|delete)\s*\(/g) || []); - return cap(routes.length ? `API: ${routes.length} endpoints` : "API router"); - } - // Pydantic - if (content.includes("BaseModel") && content.includes("Field(")) { - const cls = content.match(/class\s+(\w+)\(.*BaseModel\)/); - return cls ? `Pydantic: ${cls[1]}` : "Pydantic model"; - } - // Celery - if (content.includes("@shared_task") || content.includes("@app.task")) { - const tasks = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); - return cap(tasks.length ? `Celery tasks: ${tasks.join(", ")}` : "Celery task"); - } - // Generic - const pyClass = content.match(/class\s+(\w+)/); - const funcs = (content.match(/def\s+(\w+)/g) || []).map(f => f.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); - if (pyClass && funcs.length > 0) - return cap(funcs.length > 4 ? `${pyClass[1]}: ${funcs.slice(0, 4).join(", ")} + ${funcs.length - 4} more` : `${pyClass[1]}: ${funcs.join(", ")}`); - if (funcs.length > 0) - return cap(funcs.slice(0, 4).join(", ")); - } - // ─── Go ────────────────────────────────────────────────── - if (ext === ".go") { - const handlers = (content.match(/func\s+(\w+)\s*\(\s*\w+\s+http\.ResponseWriter/g) || []) - .map(m => m.match(/func\s+(\w+)/)?.[1]).filter(Boolean); - if (handlers.length) - return cap(`HTTP handlers: ${handlers.slice(0, 5).join(", ")}`); - const iface = content.match(/type\s+(\w+)\s+interface\s*\{/); - if (iface) - return `Interface: ${iface[1]}`; - const structM = content.match(/type\s+(\w+)\s+struct\s*\{/); - if (structM) - return `Struct: ${structM[1]}`; - const funcs = (content.match(/^func\s+(\w+)/gm) || []).map(m => m.match(/func\s+(\w+)/)?.[1]).filter(n => n && n[0] === n[0].toUpperCase()); - if (funcs.length) - return cap(funcs.slice(0, 5).join(", ")); - } - // ─── Rust ──────────────────────────────────────────────── - if (ext === ".rs") { - const structM = content.match(/pub\s+struct\s+(\w+)/); - if (structM) { - const methods = (content.match(/pub\s+(?:async\s+)?fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean); - return cap(methods.length ? `${structM[1]}: ${methods.slice(0, 4).join(", ")}` : `Struct: ${structM[1]}`); - } - const traitM = content.match(/pub\s+trait\s+(\w+)/); - if (traitM) - return `Trait: ${traitM[1]}`; - const enumM = content.match(/pub\s+enum\s+(\w+)/); - if (enumM) - return `Enum: ${enumM[1]}`; - const fns = (content.match(/pub\s+(?:async\s+)?fn\s+(\w+)/g) || []).map(m => m.match(/fn\s+(\w+)/)?.[1]).filter(Boolean); - if (fns.length) - return cap(fns.slice(0, 5).join(", ")); - } - // ─── Java / Spring ─────────────────────────────────────── - if (ext === ".java") { - const cls = content.match(/(?:public\s+)?class\s+(\w+)/); - const className = cls?.[1] || basename.replace(".java", ""); - const annotations = (content.match(/@(RestController|Controller|Service|Repository|Component|Entity|Configuration)/g) || []).map(a => a.slice(1)); - const mappings = (content.match(/@(?:Get|Post|Put|Patch|Delete|Request)Mapping/g) || []).length; - if (mappings) - return cap(`${annotations[0] || "Spring"}: ${className} (${mappings} endpoints)`); - if (annotations.length) - return `${annotations[0]}: ${className}`; - if (content.includes("@Entity")) - return `Entity: ${className}`; - const methods = (content.match(/public\s+(?:static\s+)?(?:\w+(?:<[\w,\s]+>)?)\s+(\w+)\s*\(/g) || []) - .map(m => m.match(/(\w+)\s*\(/)?.[1]).filter(n => n && n !== className); - if (methods.length) - return cap(`${className}: ${methods.slice(0, 4).join(", ")}`); - return className ? `Class: ${className}` : ""; - } - // ─── Kotlin ────────────────────────────────────────────── - if (ext === ".kt" || ext === ".kts") { - const cls = content.match(/(?:data\s+)?class\s+(\w+)/); - if (content.match(/data\s+class/)) - return `Data class: ${cls?.[1] || basename.replace(/\.kts?$/, "")}`; - if (content.includes("routing {")) - return "Ktor routing"; - const fns = (content.match(/fun\s+(\w+)/g) || []).map(m => m.match(/fun\s+(\w+)/)?.[1]).filter(Boolean); - if (cls && fns.length) - return cap(`${cls[1]}: ${fns.slice(0, 4).join(", ")}`); - if (fns.length) - return cap(fns.slice(0, 5).join(", ")); - } - // ─── C# / .NET ─────────────────────────────────────────── - if (ext === ".cs") { - const cls = content.match(/(?:public\s+)?(?:partial\s+)?class\s+(\w+)(?:\s*:\s*(\w+))?/); - const className = cls?.[1] || basename.replace(".cs", ""); - const parent = cls?.[2] || ""; - if (parent === "Controller" || parent === "ControllerBase" || content.includes("[ApiController]")) { - const actions = (content.match(/\[Http(Get|Post|Put|Patch|Delete)\]/g) || []).map(a => a.match(/Http(\w+)/)?.[1]).filter(Boolean); - return cap(actions.length ? `API Controller: ${className} (${[...new Set(actions)].join(", ")})` : `Controller: ${className}`); - } - if (parent === "DbContext" || content.includes("DbSet<")) { - const sets = (content.match(/DbSet<(\w+)>/g) || []).map(s => s.match(/<(\w+)>/)?.[1]).filter(Boolean); - return cap(sets.length ? `DbContext: ${sets.join(", ")}` : `DbContext: ${className}`); - } - return className ? `Class: ${className}` : ""; - } - // ─── Ruby / Rails ──────────────────────────────────────── - if (ext === ".rb") { - const cls = content.match(/class\s+(\w+)(?:\s*<\s*(\w+(?:::\w+)?))?/); - const className = cls?.[1] || ""; - const parent = cls?.[2] || ""; - if (parent?.includes("Controller")) { - const actions = (content.match(/def\s+(index|show|new|create|edit|update|destroy|\w+)/g) || []) - .map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); - return cap(actions.length ? `Controller: ${actions.join(", ")}` : `Controller: ${className}`); - } - if (parent === "ApplicationRecord" || parent === "ActiveRecord::Base") - return `Model: ${className}`; - if (basename.match(/^\d{14}_/)) { - const create = content.match(/create_table\s+:(\w+)/); - return create ? `Migration: create ${create[1]}` : "Database migration"; - } - const methods = (content.match(/def\s+(\w+)/g) || []).map(m => m.match(/def\s+(\w+)/)?.[1]).filter(n => n && !n.startsWith("_")); - if (cls && methods.length) - return cap(`${className}: ${methods.slice(0, 4).join(", ")}`); - } - // ─── Swift ─────────────────────────────────────────────── - if (ext === ".swift") { - if (content.includes(": View") || content.includes("some View")) { - const name = content.match(/struct\s+(\w+)\s*:\s*View/); - return name ? `SwiftUI view: ${name[1]}` : "SwiftUI view"; - } - const proto = content.match(/protocol\s+(\w+)/); - if (proto) - return `Protocol: ${proto[1]}`; - const struct = content.match(/(?:public\s+)?struct\s+(\w+)/); - const cls = content.match(/(?:public\s+)?class\s+(\w+)/); - const name = struct?.[1] || cls?.[1] || ""; - if (name) - return `${struct ? "Struct" : "Class"}: ${name}`; - } - // ─── Dart / Flutter ────────────────────────────────────── - if (ext === ".dart") { - if (content.includes("StatefulWidget") || content.includes("StatelessWidget")) { - const name = content.match(/class\s+(\w+)\s+extends\s+(?:Stateful|Stateless)Widget/); - return name ? `${content.includes("StatefulWidget") ? "Stateful" : "Stateless"} widget: ${name[1]}` : "Flutter widget"; - } - const cls = content.match(/class\s+(\w+)/); - if (cls) - return `Class: ${cls[1]}`; - } - // ─── Vue / Svelte / Astro ──────────────────────────────── - if (ext === ".vue") { - const name = content.match(/name:\s*['"]([^'"]+)['"]/); - const setup = content.includes(" + diff --git a/ApiResources/TalentManagement-API b/ApiResources/TalentManagement-API deleted file mode 160000 index 5909eea..0000000 --- a/ApiResources/TalentManagement-API +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5909eeab6262be8860d553e3831ba88eb571b3b8 diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index a0f1c10..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,708 +0,0 @@ -# OpenWolf - -@.wolf/OPENWOLF.md - -This project uses OpenWolf for context management. Read and follow .wolf/OPENWOLF.md every session. Check .wolf/cerebrum.md before generating code. Check .wolf/anatomy.md before reading files. - - -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Repository Overview - -This is a **tutorial repository** demonstrating the **CAT (Client, API Resource, Token Service)** pattern using Git submodules. Each component is a separate repository that can be developed independently. - -**Tutorial Repository**: https://github.com/workcontrolgit/AngularNetTutorial.git - -## Architecture: CAT Pattern with Git Submodules - -### Three-Tier Architecture with E2E Testing - -``` -AngularNetTutorial/ -├── Clients/TalentManagement-Angular-Material/ # Git submodule -├── ApiResources/TalentManagement-API/ # Git submodule -├── TokenService/Duende-IdentityServer/ # Git submodule -└── Tests/AngularNetTutorial-Playwright/ # Git submodule -``` - -Each folder is a **git submodule** pointing to its own repository: -- `Clients/`: Angular 20 + Material Design client (ng-matero template) -- `ApiResources/`: .NET 10 Web API with Clean Architecture -- `TokenService/`: Duende IdentityServer 7.0 for OAuth 2.0/OIDC -- `Tests/`: Playwright end-to-end tests for the full stack - -### Authentication Flow - -1. User visits Angular app (`http://localhost:4200`) -2. Login redirects to IdentityServer (`https://localhost:44310`) -3. IdentityServer authenticates user, issues ID token + access token -4. Angular stores tokens, attaches access token to API requests -5. API validates token against IdentityServer, returns protected data - -## Running the Full Stack - -**Start all three services in this order:** - -```bash -# Terminal 1: IdentityServer (must start first) -cd TokenService/Duende-IdentityServer/src/Duende.STS.Identity -dotnet run - -# Terminal 2: API (needs IdentityServer running) -cd ApiResources/TalentManagement-API -dotnet run - -# Terminal 3: Angular Client -cd Clients/TalentManagement-Angular-Material/talent-management -npm start -``` - -**Application URLs:** -- Angular: `http://localhost:4200` -- API: `https://localhost:44378` -- IdentityServer: `https://localhost:44310` -- IdentityServer Admin: `https://localhost:44303` -- IdentityServer Admin API: `https://localhost:44302` - -## Running End-to-End Tests - -**Prerequisites:** All three services must be running (IdentityServer, API, Angular). - -```bash -# Navigate to Playwright tests -cd Tests/AngularNetTutorial-Playwright - -# Install dependencies (first time only) -npm install - -# Run tests headless -npx playwright test - -# Run tests with UI -npx playwright test --ui - -# Run tests in headed mode (see browser) -npx playwright test --headed - -# Run specific test file -npx playwright test tests/auth.spec.ts - -# View test report -npx playwright show-report -``` - -**Common Playwright Commands:** -- `npx playwright codegen http://localhost:4200` - Generate tests by recording interactions -- `npx playwright test --debug` - Run tests in debug mode -- `npx playwright test --project=chromium` - Run tests on specific browser - -## Working with Git Submodules - -### Initial Clone - -```bash -# Clone with all submodules -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git - -# Or initialize submodules after cloning -git submodule update --init --recursive -``` - -### Making Changes in a Submodule - -**Critical**: Submodules have their own Git history. Changes must be committed in the submodule first, then the parent. - -```bash -# 1. Navigate to submodule and make changes -cd Clients/TalentManagement-Angular-Material -git checkout develop # or appropriate branch -# ... make your changes ... -git add . -git commit -m "Your changes" -git push - -# 2. Return to parent and update reference -cd ../.. -git add Clients/TalentManagement-Angular-Material -git commit -m "Update Angular client submodule" -git push -``` - -### Pulling Latest Changes - -```bash -# Pull parent repo changes -git pull - -# Update all submodules to their referenced commits -git submodule update --init --recursive - -# OR pull latest from submodule's remote branch -git submodule update --remote --merge -``` - -### Check Submodule Status - -```bash -git submodule status -# Shows current commit hash for each submodule -``` - -### Common Submodule Issues - -**Submodule shows modified but you didn't change anything:** -- Submodule is on a different commit than parent expects -- Navigate to submodule: `cd Clients/TalentManagement-Angular-Material` -- Check status: `git status` and `git log` -- Reset to parent's expected commit or commit the change - -**Submodule folder is empty:** -```bash -git submodule update --init --recursive -``` - -## Configuration Dependencies - -### IdentityServer Configuration - -**File**: `TokenService/Duende-IdentityServer/src/Duende.Admin/identityserverdata.json` - -Key configuration for Angular client: -```json -{ - "ClientId": "TalentManagement", - "AllowedScopes": [ - "openid", - "email", - "profile", - "roles", - "app.api.talentmanagement.read", - "app.api.talentmanagement.write" - ], - "RedirectUris": ["http://localhost:4200/callback"], - "PostLogoutRedirectUris": ["http://localhost:4200"] -} -``` - -### Angular Environment Configuration - -**File**: `Clients/TalentManagement-Angular-Material/talent-management/src/environments/environment.ts` - -Must match IdentityServer configuration: -```typescript -identityServerUrl: 'https://localhost:44310' -clientId: 'TalentManagement' -scope: 'openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write' -``` - -### API Configuration - -**File**: `ApiResources/TalentManagement-API/appsettings.json` - -Must trust IdentityServer: -```json -{ - "IdentityServer": { - "Authority": "https://localhost:44310" - } -} -``` - -## Development Workflow - -### Branching Strategy - -Parent repository tracks submodule commits, not branches. Each submodule has its own branch strategy: -- Angular: Uses `develop` and `master` branches -- API: Check submodule for branch strategy -- IdentityServer: Check submodule for branch strategy - -### Testing Changes Across Multiple Submodules - -When changes span multiple components (e.g., new API endpoint + Angular UI): - -1. Make changes in API submodule, commit, push -2. Make changes in Angular submodule, commit, push -3. Update E2E tests in Playwright submodule if needed, commit, push -4. Update parent repo to reference all new commits -5. Run E2E tests to verify integration before pushing parent - -### Port Conflicts - -If ports are already in use: -- **IdentityServer**: Edit `Properties/launchSettings.json` -- **API**: Edit `Properties/launchSettings.json` -- **Angular**: Use `ng serve --port 4201` or edit `angular.json` - -## Component-Specific Documentation - -Each submodule has its own documentation: - -### Angular Client Documentation -- `Clients/TalentManagement-Angular-Material/docs/claude-code-guide.md` - Comprehensive development guide -- `Clients/TalentManagement-Angular-Material/docs/` - Feature plans, implementation guides - -### API Documentation -- Check `ApiResources/TalentManagement-API/` for API-specific documentation - -### IdentityServer Documentation -- Check `TokenService/Duende-IdentityServer/` for IdentityServer configuration guides - -### Playwright E2E Tests Documentation -- Check `Tests/AngularNetTutorial-Playwright/` for test documentation and test organization - -## Common Development Tasks - -### Adding a New API Scope - -1. Update `TokenService/.../identityserverdata.json` with new scope -2. Restart IdentityServer -3. Update Angular `environment.ts` scope string -4. Update API to protect endpoints with `[Authorize]` requiring the scope - -### Troubleshooting Authentication Issues - -Common issue: **"invalid_scope" error** -- Cause: Angular requests a scope not in IdentityServer's `AllowedScopes` -- Fix: Ensure `environment.ts` scope matches `identityserverdata.json` exactly - -Common issue: **Angular stuck at login page after successful auth** -- Cause: Auth guard using wrong authentication service -- Fix: Verify `auth-guard.ts` uses `OidcAuthService.isAuthenticated()` - -Common issue: **CORS errors** -- Cause: IdentityServer URL mismatch -- Fix: Ensure `environment.ts` identityServerUrl matches running IdentityServer URL - -### Verifying Full Stack Integration - -**Manual Testing:** -1. Start all three services -2. Navigate to `http://localhost:4200` -3. Click login → should redirect to IdentityServer -4. Login with test credentials (`ashtyn1` / `Pa$$word123`) -5. Should redirect back to Angular dashboard -6. API calls should work (check Network tab for 200 responses with Bearer token) - -**Automated Testing:** -7. Run Playwright E2E tests to verify critical user flows: - ```bash - cd Tests/AngularNetTutorial-Playwright - npx playwright test - ``` - -**Admin UI Access:** -- URL: https://localhost:44303 -- Credentials: `admin` / `Pa$$word123` - ---- - -## Git Commit Guidelines - -When asked to commit code changes to the repository, follow these guidelines: - -### Commit Message Format - -**Write concise, descriptive commit messages without AI co-authorship attribution:** - -```bash -# Good examples -git commit -m "Add blogs folder with Medium.com template and git submodule article" -git commit -m "Fix broken image link in git submodule blog post" -git commit -m "Update Angular environment configuration for production" -git commit -m "Refactor authentication service to use OIDC" - -# Bad examples (avoid these) -git commit -m "Updated files" # Too vague -git commit -m "Fixed stuff" # Not descriptive -git commit -m "🤖 Generated with Claude Code" # No AI attribution -``` - -### Commit Message Guidelines - -* **Be descriptive but concise** — 50-72 characters max for the subject line -* **Use imperative mood** — "Add feature" not "Added feature" or "Adds feature" -* **Focus on what changed** — Describe the change, not the process -* **No AI attribution** — Do not reference Claude, AI assistance, or co-authorship -* **Group related changes** — Combine logically related file changes into one commit - -### Standard Commit Workflow - -When user requests "check in code" or "commit changes": - -```bash -# 1. Stage all changes -git add . - -# 2. Commit with descriptive message (no AI attribution) -git commit -m "Brief description of changes" - -# 3. Push to remote -git push -``` - -### Multi-File Commit Examples - -**When adding new features:** -```bash -git commit -m "Add authentication guard and login component" -``` - -**When updating documentation:** -```bash -git commit -m "Update README with deployment instructions" -``` - -**When fixing bugs:** -```bash -git commit -m "Fix token refresh logic in auth interceptor" -``` - -**When refactoring:** -```bash -git commit -m "Refactor employee service to use RxJS operators" -``` - -### What NOT to Include - -* ❌ AI tool references ("Generated by Claude", "AI-assisted commit") -* ❌ Co-author attributions to AI assistants -* ❌ Workflow descriptions ("Used Claude Code to...", "Asked AI to...") -* ❌ Excessive detail in commit message (save that for PR descriptions) -* ❌ Emoji or special characters (unless project convention) - ---- - -## Writing Medium.com Compatible Blog Posts - -When creating blog posts or documentation for Medium.com, follow these guidelines to ensure proper formatting and compatibility. - -### Medium.com Formatting Rules - -**CRITICAL: Medium.com does NOT support tables.** All content must use alternative formatting. - -### Replace Tables With Lists - -**❌ DON'T use tables:** -```markdown -| Tool | Version | Purpose | -|------|---------|---------| -| .NET | 10.0+ | Backend | -``` - -**✅ DO use bullet lists with em dashes (—):** -```markdown -* **.NET SDK 10.0+** — Build and run .NET applications -* **Node.js 20.x LTS** — Run Angular development server -* **Git (Latest)** — Version control and submodules -``` - -### Technology Stack Formatting - -**❌ DON'T use tables for tech stacks:** -```markdown -| Technology | Version | Purpose | -|------------|---------|---------| -| Angular | 20.x | Frontend | -``` - -**✅ DO use descriptive bullets:** -```markdown -**Technology Stack:** - -* **Angular 20** — Frontend framework -* **Angular Material 20** — UI component library -* **TypeScript 5.x** — Type-safe JavaScript -* **RxJS 7.x** — Reactive programming -``` - -### API Endpoints Formatting - -**❌ DON'T use tables for API endpoints:** -```markdown -| Method | Endpoint | Auth | -|--------|----------|------| -| GET | /api/employees | read | -``` - -**✅ DO use descriptive bullets:** -```markdown -**API Endpoints (Employees):** - -* **GET /api/v1/employees** — Get all employees (requires `read` scope) -* **GET /api/v1/employees/{id}** — Get employee by ID (requires `read` scope) -* **POST /api/v1/employees** — Create employee (requires `write` scope) -* **PUT /api/v1/employees/{id}** — Update employee (requires `write` scope) -* **DELETE /api/v1/employees/{id}** — Delete employee (requires `write` scope) -``` - -### Comparison/Reference Formatting - -**❌ DON'T use tables for comparisons:** -```markdown -| Aspect | Value | -|--------|-------| -| Format | JWT | -| Lifetime | 1 hour | -``` - -**✅ DO use definition-style formatting:** -```markdown -**Access Token:** -* **Purpose:** Grant access to protected resources (APIs) -* **Format:** JWT or reference token -* **Lifetime:** Short (typically 1 hour) -* **Validated by:** Resource server (API) -* **Contains:** Scopes, client ID, user claims -``` - -### URL/Port Listings - -**❌ DON'T use tables for URLs:** -```markdown -| Component | URL | Description | -|-----------|-----|-------------| -| Angular | http://localhost:4200 | Main UI | -``` - -**✅ DO use colon-separated bullets:** -```markdown -**Application URLs:** - -* **Angular Client:** http://localhost:4200 — Main application UI -* **Web API:** https://localhost:44378 — RESTful API endpoints -* **Swagger UI:** https://localhost:44378/swagger — API documentation -* **IdentityServer:** https://localhost:44310 — Authentication server -``` - -### Problem/Solution Formatting - -**❌ DON'T use tables for troubleshooting:** -```markdown -| Issue | Cause | Solution | -|-------|-------|----------| -| 401 Error | Token invalid | Restart IdentityServer | -``` - -**✅ DO use problem/solution structure:** -```markdown -**Common Issues:** - -**IdentityServer won't start** -* **Problem:** Port 44310 already in use -* **Solution:** Kill process using the port or change port in `Properties/launchSettings.json` - -**API returns 401 Unauthorized** -* **Problem:** IdentityServer not running or URL mismatch -* **Solution:** Verify IdentityServer is running at https://localhost:44310 - -**Angular shows "invalid_scope" error** -* **Problem:** Scope mismatch between Angular config and IdentityServer -* **Solution:** Verify `environment.ts` scope matches `identityserverdata.json` -``` - -### Section Headers with Emojis - -Use emojis to make sections more visually appealing and scannable: - -```markdown -## 📚 What You'll Learn -## 🎯 What is the CAT Pattern? -## 🚀 Getting Started -## 🔐 Key Security Features -## 📦 Component Deep Dive -## 💡 Benefits of the CAT Pattern -## 📖 Tutorial Series Roadmap -## 🎓 Next Steps -## 🔗 Learning Resources -## 🤝 Support and Contribution -## 🎉 Conclusion -``` - -### Bold and Emphasis - -Use bold effectively for scannability: - -```markdown -**Why First?** The API and Angular client both depend on IdentityServer. - -**Wait for:** `Now listening on: https://localhost:44310` - -**Verify:** Open browser to check the application loads correctly. -``` - -### Code Block Best Practices - -Keep code blocks concise and focused: - -```markdown -**Good:** -```typescript -export const environment = { - apiUrl: 'https://localhost:44378/api/v1', - identityServerUrl: 'https://localhost:44310', -}; -``` -``` - -**Avoid:** Including entire files or excessive comments - -### Nested Lists for Structure - -Use nested bullets for hierarchical information: - -```markdown -**Key Features:** - -* **Authentication & Authorization** - * OIDC authentication with automatic token refresh - * HTTP interceptor adds Bearer tokens automatically - * Route guards protect authenticated routes - * Role-based UI rendering using ngx-permissions - -* **UI Components** - * Material Design components (buttons, forms, tables) - * Responsive layouts (mobile, tablet, desktop) - * Data tables with sorting and filtering - * Form validation with reactive forms -``` - -### Checkmarks for Benefits - -Use checkmarks (✅) for positive points: - -```markdown -## Benefits - -✅ **Security** — Industry-standard OAuth 2.0/OIDC authentication - -✅ **Scalability** — Independent scaling of each component - -✅ **Maintainability** — Clear separation of concerns - -✅ **Flexibility** — Technology-agnostic architecture -``` - -### Hero Images - -Always include a placeholder at the top of blog posts: - -```markdown -# Your Title Here - -## Subtitle - -Brief introduction paragraph. - -![Architecture Diagram](https://via.placeholder.com/800x400?text=Your+Image+Description) - ---- - -## First Section -``` - -### Tags at Bottom - -End blog posts with relevant tags: - -```markdown ---- - -**📌 Tags:** #angular #dotnet #oauth2 #openidconnect #identityserver #webdevelopment #authentication #security #cleanarchitecture #typescript #csharp #enterpriseapplications #fullstack #spa #jwt -``` - -### Creating Medium-Optimized Content - -When asked to create Medium.com blog posts: - -1. **Start from scratch** or use existing content as reference -2. **Remove ALL tables** — convert to lists, sections, or prose -3. **Add emoji section headers** for visual appeal -4. **Use bold liberally** for scannability -5. **Keep paragraphs short** (2-3 sentences max) -6. **Use nested bullets** for hierarchical info -7. **Add checkmarks (✅)** for benefits/features -8. **Include hero image placeholder** at top -9. **Add relevant tags** at bottom -10. **Test by copying to Medium** editor before finalizing - -### Example: Converting a Tutorial Section - -**Before (with tables):** -```markdown -## Prerequisites - -| Tool | Version | Download | -|------|---------|----------| -| .NET SDK | 10.0+ | [Link](https://dotnet.microsoft.com) | -| Node.js | 20.x | [Link](https://nodejs.org/) | - -| Feature | Description | -|---------|-------------| -| OIDC | Authentication protocol | -| JWT | Token format | -``` - -**After (Medium-optimized):** -```markdown -## 🚀 Getting Started - -### Prerequisites - -**Tools you'll need:** - -* **.NET SDK 10.0+** — Build and run .NET applications — [Download](https://dotnet.microsoft.com/download) -* **Node.js 20.x LTS** — Run Angular development server — [Download](https://nodejs.org/) -* **npm 10+** — Package manager for Node.js — Included with Node.js -* **Git (Latest)** — Version control and submodules — [Download](https://git-scm.com/) - -### Key Technologies - -**Authentication & Security:** -* **OIDC (OpenID Connect)** — Industry-standard authentication protocol -* **JWT (JSON Web Tokens)** — Secure token format for API authorization -* **OAuth 2.0** — Authorization framework for delegated access -* **PKCE** — Security extension for single-page applications -``` - -### Quick Reference: Medium.com Do's and Don'ts - -**DO:** -* ✅ Use bullet lists with em dashes (—) -* ✅ Use emoji section headers (📚, 🎯, 🚀) -* ✅ Use bold for emphasis and key terms -* ✅ Keep paragraphs short (2-3 sentences) -* ✅ Use nested bullets for structure -* ✅ Use checkmarks (✅) for benefits -* ✅ Include hero image placeholder -* ✅ Add tags at bottom - -**DON'T:** -* ❌ Use tables (not supported) -* ❌ Use complex ASCII diagrams (simplify them) -* ❌ Use relative internal links -* ❌ Include file system paths excessively -* ❌ Use overly technical jargon without explanation -* ❌ Write long paragraphs (hard to scan) -* ❌ Use excessive nested headings (keep hierarchy flat) - -### Publishing Workflow - -1. **Create content** following Medium guidelines -2. **Save as `*-MEDIUM.md`** to distinguish from regular docs -3. **Copy entire content** to clipboard -4. **Paste into Medium editor** (medium.com/new-story) -5. **Replace placeholder image** with actual diagram -6. **Preview** to check formatting -7. **Add publication tags** from bottom of article -8. **Publish or save as draft** - -### File Naming Convention - -* Regular documentation: `TUTORIAL.md`, `README.md` -* Medium-optimized version: `TUTORIAL-MEDIUM.md` -* Part-specific blogs: `01-introduction-MEDIUM.md`, `02-authentication-MEDIUM.md` - -This ensures clear separation between comprehensive technical documentation and reader-friendly Medium content. diff --git a/Clients/TalentManagement-Angular-Material b/Clients/TalentManagement-Angular-Material deleted file mode 160000 index a25e859..0000000 --- a/Clients/TalentManagement-Angular-Material +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a25e8596381ec3c9cdaffeb631c9f36475fc5c6f diff --git a/README.md b/README.md deleted file mode 100644 index 1495b31..0000000 --- a/README.md +++ /dev/null @@ -1,226 +0,0 @@ -# CAT Pattern Tutorial - Angular/.NET Stack - -This repository demonstrates the **Client, API Resource, and Token Service (CAT)** pattern using Angular and .NET technologies. - -**Repository**: https://github.com/workcontrolgit/AngularNetTutorial.git - -## Documentation - -- **[Complete Tutorial Series](docs/TUTORIAL.md)** - Comprehensive guide covering architecture, features, benefits, and detailed component documentation -- **[Developer Guide](CLAUDE.md)** - Technical reference for working with this codebase -- **[Setup Submodules Guide](SETUP-SUBMODULES.md)** - Git submodules setup instructions - -## Repository Structure - -``` -AngularNetTutorial/ -├── Clients/ -│ └── TalentManagement-Angular-Material/ # Angular Material client -├── ApiResources/ -│ └── TalentManagement-API/ # ASP.NET Core Web API -└── TokenService/ - └── Duende-IdentityServer/ # Duende IdentityServer -``` - -## Technology Stack - -| Component | Technology | Version | Repository | -|-----------|------------|---------|------------| -| Client | Angular + Material Design | 20.x | [TalentManagement-Angular-Material](https://github.com/workcontrolgit/TalentManagement-Angular-Material) | -| API | ASP.NET Core Web API | .NET 10 | [TalentManagement-API](https://github.com/workcontrolgit/TalentManagement-API) | -| Identity | Duende IdentityServer | 7.0 | [Duende-IdentityServer](https://github.com/workcontrolgit/Duende-IdentityServer) | - -## Getting Started - -### Prerequisites - -- [.NET 10 SDK](https://dotnet.microsoft.com/download) -- [Node.js 20+](https://nodejs.org/) and npm -- [Git](https://git-scm.com/) -- [Visual Studio Code](https://code.visualstudio.com/) or your preferred IDE - -### Clone with Submodules - -This repository uses Git submodules to manage the individual components. Clone everything with: - -```bash -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git -cd AngularNetTutorial -``` - -Or if you've already cloned without submodules: - -```bash -git clone https://github.com/workcontrolgit/AngularNetTutorial.git -cd AngularNetTutorial -git submodule update --init --recursive -``` - -### Running the Application - -#### 1. Start the Token Service (Identity Server) - -```bash -cd TokenService/Duende-IdentityServer/src/Duende.STS.Identity -dotnet restore -dotnet run -``` - -**URL**: `https://localhost:44310` - -#### 2. Start the API Resource - -```bash -cd ApiResources/TalentManagement-API -dotnet restore -dotnet run -``` - -**URL**: `https://localhost:44378` - -#### 3. Start the Angular Client - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management -npm install -npm start -``` - -**URL**: `http://localhost:4200` - -### Test Credentials - -**Angular Application User:** -- **Username:** `ashtyn1` -- **Password:** `Pa$$word123` - -**IdentityServer Admin UI:** -- **Username:** `admin` -- **Password:** `Pa$$word123` -- **URL:** `https://localhost:44303` - -## Application URLs - -| Component | URL | Description | -|-----------|-----|-------------| -| **Angular Client** | `http://localhost:4200` | Main application UI | -| **Web API** | `https://localhost:44378` | RESTful API endpoints | -| **IdentityServer** | `https://localhost:44310` | Authentication & Authorization | -| **IdentityServer Admin** | `https://localhost:44303` | IdentityServer admin panel | -| **IdentityServer Admin API** | `https://localhost:44302` | IdentityServer admin API | - -## Architecture Overview - -### CAT Pattern - -The **CAT (Client, API Resource, Token Service)** pattern separates concerns: - -- **Client**: User interface (Angular with Material Design) -- **API Resource**: Business logic and data access (.NET Core Web API) -- **Token Service**: Authentication and authorization (Duende IdentityServer) - -### Authentication Flow - -1. User authenticates via the Angular client -2. Client redirects to IdentityServer for login -3. IdentityServer issues tokens (ID token, access token) -4. Client uses access token to call API -5. API validates token with IdentityServer - -## Project Structure Details - -### Clients Folder - -Contains different client implementations. Currently includes: -- **TalentManagement-Angular-Material**: Angular application with Material Design components - -Future additions may include: -- React client -- Blazor WebAssembly client - -### ApiResources Folder - -Contains the backend API that serves data to clients. The API: -- Protects endpoints using JWT bearer authentication -- Validates tokens issued by IdentityServer -- Implements business logic and data access - -### TokenService Folder - -Contains the identity and authentication server: -- Issues tokens (JWT) -- Manages user authentication -- Handles OAuth 2.0 and OpenID Connect flows -- Manages API scopes and client configurations - -## Configuration - -Each component has its own configuration: - -- **IdentityServer**: `appsettings.json` - Configure clients, API resources, and identity resources -- **API**: `appsettings.json` - Configure database connections and IdentityServer authority -- **Angular Client**: `environment.ts` - Configure API and IdentityServer URLs - -## Development Workflow - -1. Make changes in the appropriate submodule -2. Test locally -3. Commit changes within the submodule -4. Push submodule changes to its remote repository -5. Update the parent repository to reference the new submodule commit - -```bash -# In the submodule directory -cd ApiResources/TalentManagement-API -git add . -git commit -m "Your changes" -git push - -# In the parent directory -cd ../.. -git add ApiResources/TalentManagement-API -git commit -m "Update API submodule" -git push -``` - -## Troubleshooting - -### Submodule Issues - -If submodules aren't initialized: -```bash -git submodule update --init --recursive -``` - -To pull latest changes from all submodules: -```bash -git submodule update --remote --merge -``` - -### Port Conflicts - -If default ports are in use, update the configuration: -- IdentityServer: `Properties/launchSettings.json` -- API: `Properties/launchSettings.json` -- Angular: `angular.json` or use `ng serve --port ` - -## Learning Resources - -- [OAuth 2.0 and OpenID Connect](https://oauth.net/2/) -- [Duende IdentityServer Documentation](https://docs.duendesoftware.com/) -- [ASP.NET Core Documentation](https://docs.microsoft.com/aspnet/core/) -- [Angular Documentation](https://angular.dev/) - -## License - -[Specify your license here] - -## Contributing - -Found an issue or want to improve the tutorials? - -1. **Report issues:** Open an issue in the repository -2. **Suggest improvements:** Submit a pull request -3. **Share feedback:** Let us know what's helpful or confusing - -We welcome contributions to improve documentation, fix bugs, add features, or enhance existing components. diff --git a/SETUP-SUBMODULES.md b/SETUP-SUBMODULES.md deleted file mode 100644 index c5b7b89..0000000 --- a/SETUP-SUBMODULES.md +++ /dev/null @@ -1,216 +0,0 @@ -# Setting Up Git Submodules - -This guide will help you convert the existing repositories into proper Git submodules. - -**Tutorial Repository**: https://github.com/workcontrolgit/AngularNetTutorial.git - -## Current Situation - -You have: -- A parent Git repository initialized at `c:\apps\AngularNetTutorial` -- Three component folders with their own Git repositories (or content) - -## Steps to Set Up Submodules - -### Important: Close VS Code First - -Before running these commands, **close VS Code** or any other applications that might have the folders open. This prevents file locking issues. - -### 1. Remove Existing Content - -Open PowerShell or Git Bash and navigate to your project: - -```bash -cd c:\apps\AngularNetTutorial -``` - -Remove the existing folders (they have their .git directories removed already): - -```bash -# Remove existing folders -rm -rf Clients/TalentManagement-Angular-Material -rm -rf ApiResources/TalentManagement-API -rm -rf TokenService/Duende-IdentityServer -``` - -### 2. Add Submodules - -Add the Angular client as a submodule: - -```bash -git submodule add https://github.com/workcontrolgit/TalentManagement-Angular-Material.git Clients/TalentManagement-Angular-Material -``` - -Add the API as a submodule: - -```bash -git submodule add https://github.com/workcontrolgit/TalentManagement-API.git ApiResources/TalentManagement-API -``` - -Add IdentityServer as a submodule: - -```bash -git submodule add https://github.com/workcontrolgit/Duende-IdentityServer.git TokenService/Duende-IdentityServer -``` - -### 3. Initialize and Update Submodules - -```bash -git submodule update --init --recursive -``` - -### 4. Commit the Parent Repository - -```bash -git add .gitmodules Clients/ ApiResources/ TokenService/ README.md .gitignore -git commit -m "Add submodules for Angular client, API, and IdentityServer" -``` - -### 5. Push to GitHub Repository - -Push your changes to the tutorial repository: - -```bash -git remote add origin https://github.com/workcontrolgit/AngularNetTutorial.git -git branch -M main -git push -u origin main -``` - -**Note**: If the remote already exists, just push: -```bash -git push -``` - -## Verifying Setup - -Check submodule status: - -```bash -git submodule status -``` - -You should see something like: -``` - Clients/TalentManagement-Angular-Material (heads/main) - ApiResources/TalentManagement-API (heads/main) - TokenService/Duende-IdentityServer (heads/main) -``` - -## Working with Submodules - -### Cloning the Repository - -Anyone cloning your tutorial should use: - -```bash -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git -``` - -### Updating Submodules - -To get latest changes from all submodules: - -```bash -git submodule update --remote --merge -``` - -### Making Changes in a Submodule - -1. Navigate to the submodule: - ```bash - cd ApiResources/TalentManagement-API - ``` - -2. Create a branch and make changes: - ```bash - git checkout -b feature/my-feature - # Make your changes - git add . - git commit -m "Add feature" - git push origin feature/my-feature - ``` - -3. Update parent repository to reference new commit: - ```bash - cd ../.. - git add ApiResources/TalentManagement-API - git commit -m "Update API submodule to latest" - git push - ``` - -## All Components as Submodules - -All three components (Angular client, API, and IdentityServer) are set up as Git submodules. This means: - -- Each component maintains its own Git history -- Changes to each component are tracked in their respective repositories -- The parent repository only tracks which commit of each submodule to use -- Students can work on individual components independently - -## Troubleshooting - -### "fatal: destination path exists and is not an empty directory" - -The folder still has content. Remove it completely: -```bash -rm -rf ApiResources/TalentManagement-API -``` - -### Submodule shows modified but you didn't change anything - -The submodule might be on a different commit. Navigate to it and check: -```bash -cd ApiResources/TalentManagement-API -git status -git log -``` - -Either commit the changes or reset to the desired commit. - -### Need to change submodule URL - -Edit `.gitmodules` file and update the URL, then: -```bash -git submodule sync -git submodule update --init --recursive -``` - -## Alternative: Manual Setup Script - -Create a file `setup-submodules.sh`: - -```bash -#!/bin/bash - -echo "Setting up Git submodules for CAT Pattern Tutorial..." - -# Remove existing folders -echo "Removing existing folders..." -rm -rf Clients/TalentManagement-Angular-Material -rm -rf ApiResources/TalentManagement-API -rm -rf TokenService/Duende-IdentityServer - -# Add submodules -echo "Adding Angular client submodule..." -git submodule add https://github.com/workcontrolgit/TalentManagement-Angular-Material.git Clients/TalentManagement-Angular-Material - -echo "Adding API submodule..." -git submodule add https://github.com/workcontrolgit/TalentManagement-API.git ApiResources/TalentManagement-API - -echo "Adding IdentityServer submodule..." -git submodule add https://github.com/workcontrolgit/Duende-IdentityServer.git TokenService/Duende-IdentityServer - -# Initialize -echo "Initializing submodules..." -git submodule update --init --recursive - -echo "Done! Don't forget to commit the changes:" -echo "git add ." -echo "git commit -m 'Add submodules for Angular client, API, and IdentityServer'" -``` - -Make it executable and run: -```bash -chmod +x setup-submodules.sh -./setup-submodules.sh -``` diff --git a/Tests/AngularNetTutorial-Playwright b/Tests/AngularNetTutorial-Playwright deleted file mode 160000 index d0578e8..0000000 --- a/Tests/AngularNetTutorial-Playwright +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d0578e8f23d6eb57f421dd2b47b30fe1d8582f14 diff --git a/TokenService/Duende-IdentityServer b/TokenService/Duende-IdentityServer deleted file mode 160000 index d79dd1e..0000000 --- a/TokenService/Duende-IdentityServer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d79dd1e29468cd1587f7fc4ca1742aaa95b0dd7e diff --git a/blogs/BLOG-SERIES-PLAN.md b/blogs/BLOG-SERIES-PLAN.md deleted file mode 100644 index a9261a6..0000000 --- a/blogs/BLOG-SERIES-PLAN.md +++ /dev/null @@ -1,325 +0,0 @@ -# AngularNetTutorial Blog Series Plan - -**Series Title:** Full-Stack Development with Angular, .NET, and OAuth 2.0 -**Publication:** [Scrum and Coke on Medium](https://medium.com/scrum-and-coke) -**Tutorial Repository:** https://github.com/workcontrolgit/AngularNetTutorial - -**Status Key:** -- [ ] Not started -- [~] In progress / Draft exists -- [x] Published - ---- - -## 📚 Published Articles - -- [x] **Series Kickoff** - - **Title:** Building Modern Web Applications with Angular, .NET, and OAuth 2.0 - - **Subtitle:** A Complete Tutorial Series Using the CAT Pattern — Client, API Resource, and Token Service - - **Published:** https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56 - - **File:** *(external — no local file)* - ---- - -## 📝 Series 0: Architecture & Workspace - -- [~] **Article 0.1 — Git Submodules as Workspace** - - **Title:** Stop Juggling Multiple Repos: Manage Your Full-Stack App Like a Workspace - - **Subtitle:** How Git Submodules Transform Multi-Repository Projects into a Unified Development Experience - - **File:** `blogs/series-0-architecture/0.1-git-submodule-workspace.md` - - **Notes:** Draft complete. Ready to publish. - -- [~] **Article 0.2 — Playwright Overview** - - **Title:** End-to-End Testing Made Simple: How Playwright Transforms Testing for Angular Applications - - **Subtitle:** Comprehensive E2E Testing for Modern Full-Stack Applications - - **File:** `blogs/series-0-architecture/0.2-playwright-testing.md` - - **Notes:** Draft complete. QA done. Ready to publish. - ---- - -## 🔐 Series 1: Authentication & Security - -- [~] **Article 1.1 — OAuth 2.0 PKCE Flow** - - **Title:** Why Your Angular App Needs PKCE: OAuth 2.0 Explained with a Working Demo - - **Subtitle:** Follow a Login Request from Browser to IdentityServer and Back — No Theory, Just Code - - **File:** `blogs/series-1-authentication/1.1-oauth2-pkce-flow.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 1.2 — Angular Route Guards** - - **Title:** Lock Down Your Angular Routes: Auth Guards with OIDC in 5 Minutes - - **Subtitle:** How to Protect Pages, Redirect Unauthenticated Users, and Verify It Works with Playwright - - **File:** `blogs/series-1-authentication/1.2-angular-route-guards.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 1.3 — HTTP Interceptor** - - **Title:** Never Forget a Bearer Token Again: Angular's HTTP Interceptor Explained - - **Subtitle:** How One File Automatically Secures Every API Request in Your Angular App - - **File:** `blogs/series-1-authentication/1.3-angular-http-interceptor.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 1.4 — Role-Based UI** - - **Title:** Show the Right Buttons to the Right People: Role-Based UI in Angular - - **Subtitle:** How *appHasRole and ngx-permissions Control What Each User Sees — Without Cluttering Your Components - - **File:** `blogs/series-1-authentication/1.4-angular-role-based-ui.md` - - **Notes:** Draft complete. Ready for review. - ---- - -## 🔧 Series 2: .NET 10 Web API - -- [~] **Article 2.1 — Clean Architecture** - - **Title:** How to Structure a .NET 10 API So It Doesn't Become a Mess - - **Subtitle:** A Walking Tour of Clean Architecture: Domain, Application, Infrastructure, and WebApi Layers - - **File:** `blogs/series-2-dotnet-api/2.1-dotnet-clean-architecture.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 2.2 — JWT Token Validation** - - **Title:** How Your .NET API Knows to Trust Angular — JWT Validation Explained - - **Subtitle:** Connecting IdentityServer, Access Tokens, and API Authorization in One Clear Flow - - **File:** `blogs/series-2-dotnet-api/2.2-dotnet-jwt-validation.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 2.3 — API Versioning** - - **Title:** Future-Proof Your .NET API: Add Versioning Without Breaking Existing Clients - - **Subtitle:** Why `/api/v1/` Matters and How to Implement Versioning the Right Way - - **File:** `blogs/series-2-dotnet-api/2.3-dotnet-api-versioning.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 2.4 — Swagger with JWT** - - **Title:** Test Your Secured .NET API Without Writing a Single Line of Frontend Code - - **Subtitle:** Configuring Swagger to Accept Bearer Tokens for Interactive API Exploration - - **File:** `blogs/series-2-dotnet-api/2.4-dotnet-swagger-jwt.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 2.5 — Response Caching with EasyCaching** - - **Title:** Speed Up Your Dashboard: Easy Response Caching in .NET 10 With EasyCaching - - **Subtitle:** How In-Memory Caching, Domain Event Invalidation, and Diagnostic Headers Transform API Performance - - **File:** `blogs/series-2-dotnet-api/2.5-dotnet-easycaching.md` - - **Notes:** Draft complete. Ready for review. - ---- - -## 🎨 Series 3: Angular Material UI - -- [~] **Article 3.1 — Data Tables** - - **Title:** Build a Production-Ready Data Table in Angular Material: Sort, Filter, Page - - **Subtitle:** From Zero to a Fully Functional Employee List with MatTable, MatPaginator, and Server-Side Filtering - - **File:** `blogs/series-3-angular-material/3.1-angular-material-datatable.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 3.2 — Reactive Forms** - - **Title:** Reactive Forms Done Right: Validation Patterns Every Angular Developer Should Know - - **Subtitle:** Required Fields, Email Validation, and Min Value Rules with Angular Material — One Form for Create and Edit - - **File:** `blogs/series-3-angular-material/3.2-angular-reactive-forms.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 3.3 — Dialogs** - - **Title:** The Right Way to Ask "Are You Sure?" — Angular Material Dialogs for Confirm Actions - - **Subtitle:** Building a Delete Confirmation Dialog with MatDialog, Proper Result Handling, and Reuse Across the App - - **File:** `blogs/series-3-angular-material/3.3-angular-material-dialogs.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 3.4 — ng-matero Admin Shell** - - **Title:** Why We Didn't Build the Admin Shell from Scratch: ng-matero Explained - - **Subtitle:** How a Starter Template Provides the Responsive Layout, Theme Customizer, and Date Adapter — So You Can Focus on Features - - **File:** `blogs/series-3-angular-material/3.4-ng-matero-admin-shell.md` - - **Notes:** Draft complete. Ready for review. - ---- - -## 🎭 Series 4: Playwright Testing Deep Dives - -- [~] **Article 4.1 — First Playwright Test** - - **Title:** Your First Playwright Test for an Angular App — From Zero to Green - - **Subtitle:** Step-by-Step: Install Playwright, Write a Login Test, and Run It Against a Real OAuth 2.0 App - - **File:** `blogs/series-4-playwright-testing/4.1-playwright-first-test.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 4.2 — Page Object Model** - - **Title:** Stop Copy-Pasting Selectors: The Page Object Model for Angular Material - - **Subtitle:** How BaseListPage and BaseFormPage Eliminate Selector Duplication Across Your Entire Test Suite - - **File:** `blogs/series-4-playwright-testing/4.2-playwright-page-object-model.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 4.3 — Role-Based Testing** - - **Title:** One Feature, Three Users: Testing Role-Based Access Control with Playwright - - **Subtitle:** How to Verify That Employees See Read-Only Views, Managers Can Create, and HRAdmins Can Delete — Without Guessing - - **File:** `blogs/series-4-playwright-testing/4.3-playwright-role-based-testing.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 4.4 — JWT Token Testing** - - **Title:** How to Extract and Verify JWT Tokens in Playwright Tests - - **Subtitle:** Decode the Token, Check the Claims, and Confirm the Right Scopes Are Granted - - **File:** `blogs/series-4-playwright-testing/4.4-playwright-jwt-token-testing.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 4.5 — API Testing** - - **Title:** Skip the UI: Test Your .NET API Directly with Playwright's Request Fixture - - **Subtitle:** Authenticate via Browser, Extract the Token, and Call API Endpoints Programmatically - - **File:** `blogs/series-4-playwright-testing/4.5-playwright-api-testing.md` - - **Notes:** Draft complete. Ready for review. - ---- - -## 🛠️ Series 5: DevOps & Data - -- [~] **Article 5.1 — Database Seeding** - - **Title:** 1,000 Test Employees in 3 Seconds: Database Seeding for Development and Testing - - **Subtitle:** How the Bogus Library Generates Reproducible Fake Data That Makes Your App Look Real From Day One - - **File:** `blogs/series-5-devops-data/5.1-database-seeding.md` - - **Notes:** Draft complete. Ready for review. - -- [~] **Article 5.2 — CI/CD with GitHub Actions** - - **Title:** Run Your Playwright Tests Automatically: CI/CD for a Full-Stack Angular/.NET App - - **Subtitle:** GitHub Actions Workflow That Runs All Three Browsers, Publishes JUnit Reports, and Comments Results on Every Pull Request - - **File:** `blogs/series-5-devops-data/5.2-cicd-github-actions.md` - - **Notes:** Draft complete. Ready for review. - -### ☁️ Series 5 Azure Deployment Sub-Series - -*Target audience: developers new to Azure with a Visual Studio Professional subscription ($50/month credit). Each article ships with real, runnable Bicep templates and GitHub Actions workflows.* - -- [ ] **Article 5.3 — Azure Subscription Setup** - - **Title:** Your First Azure Deployment: Setting Up a Visual Studio Subscription - - **Subtitle:** Activate Your $50 Monthly Credit, Install the Azure CLI, and Understand What Fits in Your Budget - - **File:** `blogs/series-5-devops-data/5.3-azure-subscription-setup.md` - - **Branch:** `feature/5.3-5.8-azure-deployment-series` - - **Notes:** Blog only — no code files. Setup guide for new Azure users. - -- [ ] **Article 5.4 — Bicep Infrastructure** - - **Title:** Infrastructure as Code: Provision All Azure Resources with One Bicep Command - - **Subtitle:** From Empty Subscription to a Running App Service Plan, SQL Server, and Static Web App in Minutes - - **File:** `blogs/series-5-devops-data/5.4-azure-bicep-infrastructure.md` - - **Code:** `infra/main.bicep`, `infra/modules/*.bicep`, `infra/parameters/dev.bicepparam` - - **Branch:** `feature/5.3-5.8-azure-deployment-series` - - **Notes:** Writes real Bicep templates that can be run against an actual Azure subscription. - -- [ ] **Article 5.5 — OIDC GitHub Actions Setup** - - **Title:** Secure CI/CD: Connect GitHub Actions to Azure Without Storing Passwords - - **Subtitle:** How Federated Identity Credentials Replace Long-Lived Secrets With Short-Lived OIDC Tokens - - **File:** `blogs/series-5-devops-data/5.5-azure-oidc-github-actions.md` - - **Code:** Walkthrough of `infra/scripts/setup-oidc.sh` (already written) - - **Branch:** `feature/5.3-5.8-azure-deployment-series` - - **Notes:** Explains the existing setup-oidc.sh script step by step; covers 4 GitHub Secrets. - -- [ ] **Article 5.6 — Deploy .NET Apps** - - **Title:** Deploy .NET API and IdentityServer to Azure App Service with GitHub Actions - - **Subtitle:** Restore, Build, Test, Publish, and Deploy — Automatically on Every Push to Main - - **File:** `blogs/series-5-devops-data/5.6-azure-deploy-dotnet-apps.md` - - **Code:** `.github/workflows/deploy-api.yml`, `.github/workflows/deploy-identityserver.yml` - - **Branch:** `feature/5.3-5.8-azure-deployment-series` - - **Notes:** Covers deployment order (IdentityServer first), App Service config, EF Core migrations. - -- [ ] **Article 5.7 — Deploy Angular** - - **Title:** Deploy Angular to Azure Static Web Apps: Zero Cost, Global CDN, Auto PR Previews - - **Subtitle:** Inject Environment URLs at Build Time and Let GitHub Actions Handle the Rest - - **File:** `blogs/series-5-devops-data/5.7-azure-deploy-angular-swa.md` - - **Code:** `.github/workflows/deploy-angular.yml`, `staticwebapp.config.json` - - **Branch:** `feature/5.3-5.8-azure-deployment-series` - - **Notes:** Covers environment URL injection at build time and SPA fallback routing config. - -- [ ] **Article 5.8 — Post-Deployment Configuration and Validation** - - **Title:** Connect the Stack: Post-Deployment Configuration and Validation - - **Subtitle:** Wire Up IdentityServer Redirect URIs, CORS, and Validate the Full Login Flow on Azure - - **File:** `blogs/series-5-devops-data/5.8-azure-post-deployment-config.md` - - **Branch:** `feature/5.3-5.8-azure-deployment-series` - - **Notes:** Blog only — configuration checklist, common failure patterns, end-to-end validation steps. - ---- - -## 🤖 Series 6: AI App Features - -- [ ] **Article 6.1 — .NET AI Foundation** - - **Title:** Run a Local LLM in Your .NET 10 API with Ollama - - **Subtitle:** How Microsoft.Extensions.AI Makes Your API AI-Ready Without Locking You Into One Provider - - **File:** `blogs/series-6-ai-app-features/6.1-dotnet-ai-foundation.md` - - **Branch:** `feature/6.1-dotnet-ai-foundation` - - **Notes:** Not started. - -- [ ] **Article 6.2 — HR AI Assistant** - - **Title:** Build an HR AI Assistant That Knows Your Data - - **Subtitle:** Feed Real Employee Metrics Into a Local LLM and Get Meaningful Insights Back - - **File:** `blogs/series-6-ai-app-features/6.2-dotnet-ai-hr-assistant.md` - - **Branch:** `feature/6.2-dotnet-ai-hr-assistant` - - **Notes:** Not started. - -- [ ] **Article 6.3 — Angular AI Chat Widget** - - **Title:** Add an AI Chat Widget to Angular with Streaming - - **Subtitle:** Build a Real-Time Chat UI with SSE Streaming, Angular Signals, and Angular Material - - **File:** `blogs/series-6-ai-app-features/6.3-angular-ai-chat-widget.md` - - **Branch:** `feature/6.3-angular-ai-chat-widget` - - **Notes:** Not started. - -- [ ] **Article 6.4 — AI Dashboard Insights** - - **Title:** AI-Generated Dashboard Insights in Angular Material - - **Subtitle:** How to Prompt an LLM With Live Metrics and Display Smart Summaries on the Dashboard - - **File:** `blogs/series-6-ai-app-features/6.4-angular-ai-dashboard-insights.md` - - **Branch:** `feature/6.4-angular-ai-dashboard-insights` - - **Notes:** Not started. - -- [ ] **Article 6.5 — Natural Language Search** - - **Title:** Natural Language Employee Search with LLM Query Parsing - - **Subtitle:** Replace Keyword Filters With Plain-English Queries — The LLM Translates Intent Into API Parameters - - **File:** `blogs/series-6-ai-app-features/6.5-dotnet-natural-language-search.md` - - **Branch:** `feature/6.5-natural-language-search` - - **Notes:** Not started. - -- [ ] **Article 6.6 — AI Response Caching** - - **Title:** Cache Your AI Responses: Save Time and API Costs - - **Subtitle:** How EasyCaching Cache-Aside Eliminates Duplicate LLM Calls and Adds Cache Observability Headers - - **File:** `blogs/series-6-ai-app-features/6.6-dotnet-ai-response-caching.md` - - **Branch:** `feature/6.6-ai-response-caching` - - **Notes:** Not started. - ---- - -## 🛠️ Series 7: Developer Productivity AI - -- [ ] **Article 7.1 — Claude Code Workflow** - - **Title:** How We Built 22 Articles with Claude Code - - **Subtitle:** The Prompting Patterns, CLAUDE.md Conventions, and Session Workflows That Made It Possible - - **File:** `blogs/series-7-developer-productivity-ai/7.1-claude-code-workflow.md` - - **Branch:** `feature/7.1-claude-code-workflow` - - **Notes:** Not started. - -- [ ] **Article 7.2 — Copilot for Clean Architecture** - - **Title:** GitHub Copilot for .NET Clean Architecture - - **Subtitle:** Prompt Patterns for Generating CQRS Handlers, FluentValidation Rules, and Mapster Mappings - - **File:** `blogs/series-7-developer-productivity-ai/7.2-copilot-clean-architecture.md` - - **Branch:** `feature/7.2-copilot-clean-architecture` - - **Notes:** Not started. - -- [ ] **Article 7.3 — AI-Generated Playwright Tests** - - **Title:** Generate Playwright Tests from User Stories with AI - - **Subtitle:** From Requirements to Running Tests — How to Prompt Your Way to a Full E2E Test Suite - - **File:** `blogs/series-7-developer-productivity-ai/7.3-ai-generated-playwright-tests.md` - - **Branch:** `feature/7.3-ai-generated-playwright-tests` - - **Notes:** Not started. - -- [ ] **Article 7.4 — AI Code Review in GitHub Actions** - - **Title:** AI Code Review in GitHub Actions - - **Subtitle:** Add an Automated AI Reviewer to Every Pull Request Using the Anthropic API - - **File:** `blogs/series-7-developer-productivity-ai/7.4-ai-code-review-github-actions.md` - - **Branch:** `feature/7.4-ai-code-review-ci` - - **Notes:** Not started. - ---- - -## 📊 Publication Tracker - -**Total articles planned:** 37 -**Published:** 1 -**Draft ready:** 22 -**Not started:** 16 (Series 5 Azure sub-series × 6, Series 6 × 6, Series 7 × 4) - ---- - -## 📋 Writing Guidelines - -- Follow `BLOG-TEMPLATE.md` for structure -- Save drafts as `[N.N]-[topic].md` in the appropriate `blogs/series-N-*/` folder -- All code examples must come from the actual tutorial repo -- Test all code examples before publishing -- No tables — use bullet lists with em dashes (—) -- Add Series Navigation section at bottom of each article -- Update this plan when an article is published (change `[ ]` to `[x]`) diff --git a/blogs/BLOG-TEMPLATE.md b/blogs/BLOG-TEMPLATE.md deleted file mode 100644 index ec15c52..0000000 --- a/blogs/BLOG-TEMPLATE.md +++ /dev/null @@ -1,155 +0,0 @@ -# [Catchy Title: Focus on Developer Benefit] - -## [Compelling Subtitle: What Problem Does This Solve?] - -[2-3 sentence introduction that hooks the reader. Explain the pain point this tutorial addresses. Make it relatable.] - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article [explain what this article adds or builds on from the series].** - ---- - -## 🎓 What You'll Learn - -* **[Concept 1]** — Brief description of what the reader gains -* **[Concept 2]** — Key skill or pattern covered -* **[Concept 3]** — Practical implementation takeaway -* **[Unique Insight]** — The standout idea in this article - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **[Prerequisite 1]** — e.g., Angular/.NET tutorial stack running locally -* **[Prerequisite 2]** — e.g., Basic understanding of TypeScript/C# -* **[Prerequisite 3]** — e.g., Node.js 20+ and .NET 10 SDK installed - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem - -[Describe the common challenge developers face. 2-3 paragraphs max. Use concrete examples.] - -**Common pain points:** - -* **[Challenge 1]** — Brief description of frustration -* **[Challenge 2]** — Another common issue -* **[Challenge 3]** — Third problem this article solves - ---- - -## 💡 The Solution - -[Introduce the approach or feature this article covers. Explain how it solves the problem above.] - -**Key benefits:** - -* ✅ **[Benefit 1]** — Specific improvement or time saved -* ✅ **[Benefit 2]** — Developer productivity gain -* ✅ **[Benefit 3]** — Code quality or maintainability win - ---- - -## 🚀 How It Works - -[Step-by-step explanation with code examples. Reference actual files from the tutorial repo.] - -### Step 1: [Action Verb + What You're Doing] - -[Brief explanation of this step. Reference the actual file: `path/to/file.ts`] - -```bash -# Example command -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git -``` - -**What this does:** [Explain the command/code in plain language] - -### Step 2: [Next Action] - -[Continue with subsequent steps. Include code snippets from the actual project.] - -```typescript -// path/to/actual/file.ts -export const config = { - setting: 'value' -}; -``` - -**Key point:** [Explain why this matters or what to watch for] - -### Step 3: [Final Step] - -[Wrap up the implementation steps.] - ---- - -## 💻 Try It Yourself - -**Start all three services** — See [Quick Start Setup](../QUICKSTART.md) for startup commands, application URLs, and test credentials. - -[Add any article-specific steps here, e.g. Playwright commands, browser DevTools observations, etc.] - ---- - -## 📊 Real-World Impact - -**Before this approach:** - -* ❌ [Pain point 1] -* ❌ [Pain point 2] -* ❌ [Pain point 3] - -**After this approach:** - -* ✅ [Improvement 1] -* ✅ [Improvement 2] -* ✅ [Improvement 3] - ---- - -## 🌟 Why This Matters - -[1-2 paragraphs on broader impact. How does this improve your development workflow? What skills transfer to other projects?] - -**Transferable skills:** - -* **[Skill 1]** — Applicable to [other scenarios] -* **[Skill 2]** — Useful for [related technologies] -* **[Skill 3]** — Foundation for [advanced topics] - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -## 📖 Series Navigation - -**AngularNetTutorial Blog Series:** - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial -* [Stop Juggling Multiple Repos: Manage Your Full-Stack App Like a Workspace](#) — Git Submodules -* [End-to-End Testing Made Simple: How Playwright Transforms Testing](#) — Playwright Overview -* **[This Article]** — [Title] -* *More articles coming soon...* - ---- - -**📌 Tags:** #angular #dotnet #[topic-specific-tag] #oauth2 #openidconnect #identityserver #webdevelopment #authentication #cleanarchitecture #typescript #csharp #fullstack #spa #playwright #[topic-specific-tag2] diff --git a/blogs/QUICKSTART.md b/blogs/QUICKSTART.md deleted file mode 100644 index 2a819d8..0000000 --- a/blogs/QUICKSTART.md +++ /dev/null @@ -1,83 +0,0 @@ -# Quick Start: Running the AngularNetTutorial Stack - -This guide covers the startup commands, application URLs, and test credentials used throughout the AngularNetTutorial blog series. - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -## Start All Services - -Start the three services in this order. Each must be running before you proceed. - -**Terminal 1: IdentityServer (start first — others depend on it)** - -```bash -cd TokenService/Duende-IdentityServer/src/Duende.STS.Identity -dotnet run -``` - -**Wait for:** `Now listening on: https://localhost:44310` - -**Terminal 2: API** - -```bash -cd ApiResources/TalentManagement-API/TalentManagementAPI.WebApi -dotnet run -``` - -**Wait for:** `Now listening on: https://localhost:44378` - -**Terminal 3: Angular Client** - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management -npm start -``` - -**Wait for:** `Angular Live Development Server is listening on localhost:4200` - ---- - -## Application URLs - -* **Angular Client:** http://localhost:4200 — Main application UI -* **Web API:** https://localhost:44378 — RESTful API endpoints -* **Swagger UI:** https://localhost:44378/swagger — Interactive API documentation -* **IdentityServer:** https://localhost:44310 — OAuth 2.0/OIDC authentication server -* **Admin UI:** https://localhost:44303 — IdentityServer management console - ---- - -## Test Credentials - -* **Manager:** `rosamond33` / `Pa$$word123` -* **HRAdmin:** `ashtyn1` / `Pa$$word123` -* **Employee:** `antoinette16` / `Pa$$word123` -* **Admin (IdentityServer Admin UI):** `admin` / `Pa$$word123` - ---- - -## Playwright Tests (E2E Testing Articles) - -After all three services are running, install and run the Playwright test suite: - -```bash -cd Tests/AngularNetTutorial-Playwright -npm install -npx playwright install -npx playwright test -``` - -**Useful Playwright commands:** - -```bash -npx playwright test --ui # Interactive UI mode for debugging -npx playwright test --headed # Watch tests run in the browser -npx playwright test --project=chromium # Run on specific browser only -npx playwright show-report # View detailed HTML report -``` - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](SERIES-NAVIGATION-TOC.md) diff --git a/blogs/README.md b/blogs/README.md deleted file mode 100644 index fd22f2d..0000000 --- a/blogs/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Blogs - -This folder contains blog posts for publication on Medium.com covering the AngularNetTutorial full-stack series (Angular 20, .NET 10, OAuth 2.0). - -## Navigation - -📖 **[Series Navigation TOC](SERIES-NAVIGATION-TOC.md)** — Quick links to all articles - -📋 **[Blog Series Plan](BLOG-SERIES-PLAN.md)** — Full plan with status, subtitles, and file paths - -## Folder Structure - -``` -blogs/ -├── README.md ← This file -├── SERIES-NAVIGATION-TOC.md ← Article navigation — link all readers back here -├── BLOG-SERIES-PLAN.md ← Master plan with status tracking -├── BLOG-TEMPLATE.md ← Template for new articles -│ -├── series-0-architecture/ -│ ├── 0.1-git-submodule-workspace.md -│ └── 0.2-playwright-testing.md -│ -├── series-1-authentication/ -│ ├── 1.1-oauth2-pkce-flow.md -│ ├── 1.2-angular-route-guards.md -│ ├── 1.3-angular-http-interceptor.md -│ └── 1.4-angular-role-based-ui.md -│ -├── series-2-dotnet-api/ -│ ├── 2.1-dotnet-clean-architecture.md -│ ├── 2.2-dotnet-jwt-validation.md -│ ├── 2.3-dotnet-api-versioning.md -│ └── 2.4-dotnet-swagger-jwt.md -│ -├── series-3-angular-material/ -│ ├── 3.1-angular-material-datatable.md -│ ├── 3.2-angular-reactive-forms.md -│ ├── 3.3-angular-material-dialogs.md -│ └── 3.4-ng-matero-admin-shell.md -│ -├── series-4-playwright-testing/ ← Coming soon -└── series-5-devops/ ← Coming soon -``` - -## File Naming Convention - -* **Blog posts:** `[N.N]-[topic].md` inside the appropriate `series-N-*/` folder -* **Series plan:** `BLOG-SERIES-PLAN.md` — track status with `[ ]` / `[~]` / `[x]` -* **Template:** `BLOG-TEMPLATE.md` — copy this when starting a new article - -## Medium.com Compatibility Rules - -* ❌ NO tables (Medium.com doesn't support them) -* ✅ Bullet lists with em dashes (—) -* ✅ Emoji section headers (📚 🎯 🚀 🔐) -* ✅ Bold text for scannability -* ✅ Short paragraphs (2-3 sentences max) -* ✅ Hero image placeholder at top -* ✅ Single TOC back-reference at bottom (replaces per-article navigation lists) -* ✅ Tags at the very bottom - -## Publishing Workflow - -1. Write article in `series-N-*/N.N-[topic].md` following `BLOG-TEMPLATE.md` -2. Update `BLOG-SERIES-PLAN.md` status from `[ ]` to `[~]` -3. Update `SERIES-NAVIGATION-TOC.md` with the published Medium URL when live -4. Copy entire file content to clipboard -5. Paste into Medium editor (medium.com/new-story) -6. Replace placeholder image with actual diagram -7. Preview, adjust formatting, add tags -8. Publish — then update `BLOG-SERIES-PLAN.md` status to `[x]` with the published URL - -## Published Articles - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial - -## Main Tutorial Repository - -📖 [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) diff --git a/blogs/SERIES-NAVIGATION-TOC.md b/blogs/SERIES-NAVIGATION-TOC.md deleted file mode 100644 index aa23512..0000000 --- a/blogs/SERIES-NAVIGATION-TOC.md +++ /dev/null @@ -1,96 +0,0 @@ -# AngularNetTutorial — Series Navigation - -**Full-Stack Development with Angular 20, .NET 10, and OAuth 2.0 with Duende IdentityServer** - -📖 **Main Tutorial:** [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) - -📁 **Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -## 🌐 Live Demo URLs - -### Azure -* **Angular UI (Static Web App):** https://mango-flower-0ced4011e.4.azurestaticapps.net -* **Web API:** https://app-talent-api-dev.azurewebsites.net -* **IdentityServer STS:** https://app-talent-ids-dev.azurewebsites.net -* **IdentityServer Admin:** https://app-talent-admin-dev.azurewebsites.net - -### GitHub Pages -* **Angular UI:** https://workcontrolgit.github.io/AngularNetTutorial/ - ---- - -## 📝 Series 0: Architecture & Workspace - -* [0.1 — Stop Juggling Multiple Repos: Manage Your Full-Stack App Like a Workspace](series-0-architecture/0.1-git-submodule-workspace.md) — Git Submodules -* [0.2 — End-to-End Testing Made Simple: How Playwright Transforms Testing](series-0-architecture/0.2-playwright-testing.md) — Playwright Overview - ---- - -## 🔐 Series 1: Authentication & Security - -* [1.1 — Why Your Angular App Needs PKCE: OAuth 2.0 Explained with a Working Demo](series-1-authentication/1.1-oauth2-pkce-flow.md) — OAuth 2.0 PKCE Flow -* [1.2 — Lock Down Your Angular Routes: Auth Guards with OIDC in 5 Minutes](series-1-authentication/1.2-angular-route-guards.md) — Route Guards -* [1.3 — Never Forget a Bearer Token Again: Angular's HTTP Interceptor Explained](series-1-authentication/1.3-angular-http-interceptor.md) — HTTP Interceptor -* [1.4 — Show the Right Buttons to the Right People: Role-Based UI in Angular](series-1-authentication/1.4-angular-role-based-ui.md) — Role-Based UI - ---- - -## 🔧 Series 2: .NET 10 Web API - -* [2.1 — How to Structure a .NET 10 API So It Doesn't Become a Mess](series-2-dotnet-api/2.1-dotnet-clean-architecture.md) — Clean Architecture -* [2.2 — How Your .NET API Knows to Trust Angular: JWT Validation Explained](series-2-dotnet-api/2.2-dotnet-jwt-validation.md) — JWT Validation -* [2.3 — Future-Proof Your .NET API: Add Versioning Without Breaking Existing Clients](series-2-dotnet-api/2.3-dotnet-api-versioning.md) — API Versioning -* [2.4 — Test Your Secured .NET API Without Writing a Single Line of Frontend Code](series-2-dotnet-api/2.4-dotnet-swagger-jwt.md) — Swagger with JWT -* [2.5 — Speed Up Your Dashboard: Easy Response Caching in .NET 10 With EasyCaching](series-2-dotnet-api/2.5-dotnet-easycaching.md) — Response Caching - ---- - -## 🎨 Series 3: Angular Material UI - -* [3.1 — Build a Production-Ready Data Table in Angular Material: Sort, Filter, Page](series-3-angular-material/3.1-angular-material-datatable.md) — Data Tables -* [3.2 — Reactive Forms Done Right: Validation Patterns Every Angular Developer Should Know](series-3-angular-material/3.2-angular-reactive-forms.md) — Reactive Forms -* [3.3 — The Right Way to Ask "Are You Sure?" — Angular Material Dialogs for Confirm Actions](series-3-angular-material/3.3-angular-material-dialogs.md) — Dialogs -* [3.4 — Why We Didn't Build the Admin Shell from Scratch: ng-matero Explained](series-3-angular-material/3.4-ng-matero-admin-shell.md) — Admin Shell - ---- - -## 🎭 Series 4: Playwright Testing Deep Dives - -* [4.1 — Your First Playwright Test for an Angular App — From Zero to Green](series-4-playwright-testing/4.1-playwright-first-test.md) — First Test -* [4.2 — Stop Copy-Pasting Selectors: The Page Object Model for Angular Material](series-4-playwright-testing/4.2-playwright-page-object-model.md) — Page Object Model -* [4.3 — One Feature, Three Users: Testing Role-Based Access Control with Playwright](series-4-playwright-testing/4.3-playwright-role-based-testing.md) — Role-Based Testing -* [4.4 — How to Extract and Verify JWT Tokens in Playwright Tests](series-4-playwright-testing/4.4-playwright-jwt-token-testing.md) — JWT Token Testing -* [4.5 — Skip the UI: Test Your .NET API Directly with Playwright's Request Fixture](series-4-playwright-testing/4.5-playwright-api-testing.md) — API Testing - ---- - -## 🛠️ Series 5: DevOps & Data - -* [5.1 — 1,000 Test Employees in 3 Seconds: Database Seeding for Development and Testing](series-5-devops-data/5.1-database-seeding.md) — Database Seeding -* [5.2 — Run Your Playwright Tests Automatically: CI/CD for a Full-Stack Angular/.NET App](series-5-devops-data/5.2-cicd-github-actions.md) — CI/CD with GitHub Actions -* [5.3 — Your First Azure Deployment: Setting Up a Visual Studio Subscription](series-5-devops-data/5.3-azure-subscription-setup.md) — Azure Subscription Setup -* [5.4 — Infrastructure as Code: Provision All Azure Resources with One Bicep Command](series-5-devops-data/5.4-azure-bicep-infrastructure.md) — Azure Bicep Infrastructure -* [5.5 — Secure CI/CD: Connect GitHub Actions to Azure Without Passwords](series-5-devops-data/5.5-azure-oidc-github-actions.md) — OIDC Federated Identity -* [5.6 — Deploy .NET API and IdentityServer to Azure App Service](series-5-devops-data/5.6-azure-deploy-dotnet-apps.md) — Deploy .NET Apps -* [5.7 — Deploy Angular to Azure Static Web Apps](series-5-devops-data/5.7-azure-deploy-angular-swa.md) — Deploy Angular SWA -* [5.8 — Connect the Stack: Post-Deployment Configuration and Validation](series-5-devops-data/5.8-azure-post-deployment-config.md) — Post-Deployment Config -* [5.9 — Zero Secrets in Config: Azure Key Vault for App Service and .NET Code](series-5-devops-data/5.9-azure-key-vault-secrets.md) — Key Vault Secrets Management - ---- - -## 🤖 Series 6: AI App Features - -* [6.1 — Run a Local LLM in Your .NET 10 API with Ollama](series-6-ai-app-features/6.1-dotnet-ai-foundation.md) — .NET AI Foundation -* [6.2 — Build an HR AI Assistant That Knows Your Data](series-6-ai-app-features/6.2-dotnet-ai-hr-assistant.md) — HR AI Assistant -* [6.3 — Build a Dedicated AI Section in Angular with Submenu Navigation](series-6-ai-app-features/6.3-angular-ai-chat-widget.md) — AI Submenu Refactor -* [6.4 — Natural Language Employee Search in Angular Material](series-6-ai-app-features/6.4-angular-ai-nl-search.md) — NL Search Angular -* [6.5 — Natural Language Employee Search with LLM Query Parsing](series-6-ai-app-features/6.5-dotnet-natural-language-search.md) — NL Search .NET Backend -* [6.6 — Semantic Position Search with Vector Embeddings in Angular Material](series-6-ai-app-features/6.6-angular-ai-vector-search.md) — Vector Search Angular -* [6.7 — Semantic Position Search with SQL Server Native Vector Search](series-6-ai-app-features/6.7-dotnet-mssql-vector-search.md) — Vector Search .NET Backend -* [6.8 — Cache Your AI Responses: Save Time and API Costs](series-6-ai-app-features/6.8-dotnet-ai-response-caching.md) — AI Response Caching - ---- - -*Articles without links are not yet written. Links will be updated to Medium URLs when published.* diff --git a/blogs/series-0-architecture/0.1-git-submodule-workspace.md b/blogs/series-0-architecture/0.1-git-submodule-workspace.md deleted file mode 100644 index 5104632..0000000 --- a/blogs/series-0-architecture/0.1-git-submodule-workspace.md +++ /dev/null @@ -1,217 +0,0 @@ -# Stop Juggling Multiple Repos: Manage Your Full-Stack App Like a Workspace - -## How Git Submodules Transform Multi-Repository Projects into a Unified Development Experience - -Managing a modern full-stack application means juggling multiple repositories—one for your frontend, another for your API, and yet another for authentication. You're constantly switching directories, checking out branches, and hoping everything stays in sync. Sound familiar? - -There's a better way. This tutorial demonstrates how to use **git submodules** to link separate repositories into a single, cohesive workspace—giving you the independence of microservices with the convenience of a monorepo. - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Git submodules are used to manage a multi-repo full-stack workspace.** - ---- - -## 🎓 What You'll Learn - -* **Git Submodule Mastery** — Clone, update, commit, and troubleshoot submodules like a pro -* **Full-Stack Architecture** — Implement the CAT (Client, API, Token Service) pattern -* **OAuth 2.0 / OIDC** — Production-ready authentication with IdentityServer -* **Angular 20 + Material Design** — Modern SPA development with reactive forms, route guards, HTTP interceptors -* **.NET 10 Web API** — Clean Architecture, JWT validation, Swagger documentation -* **Version Alignment** — Ensure frontend, API, and auth server always use compatible commits - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **.NET SDK 10.0+** — Build and run .NET applications — [Download](https://dotnet.microsoft.com/download) -* **Node.js 20.x LTS** — Run Angular development server — [Download](https://nodejs.org/) -* **Git (Latest)** — Version control and submodule support — [Download](https://git-scm.com/) -* **Basic Git knowledge** — Familiarity with clone, commit, push, and pull - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem - -**The traditional approach to full-stack development creates friction at every step.** - -You've separated your concerns beautifully. Your Angular frontend lives in one repo, your .NET API in another, and your authentication service in a third. Each team can work independently, deployment is isolated, and versioning is clean. Perfect architecture, right? - -**But the developer experience is painful:** - -* **Context switching hell** — Opening three terminal windows, three IDE instances, tracking which repo has uncommitted changes -* **Branch synchronization nightmares** — Frontend dev creates a feature branch, but forgets to create matching branches in the API and auth repos -* **Integration testing chaos** — "Works on my machine" because you're testing frontend v2.1 against API v2.0 while your teammate uses v2.2 -* **Onboarding bottlenecks** — New developers spend hours cloning repos, reading scattered README files, and asking "Which version of the API do I need?" -* **Documentation drift** — README in the frontend repo says "use API v2.1," but the API repo's README says it requires auth v3.0 - -**The root cause?** You've optimized for deployment architecture while sacrificing developer productivity. - ---- - -## 💡 The Solution - -**Git submodules let you combine multiple repositories into a unified workspace—without sacrificing repository independence.** - -Think of it like this: Your main repository becomes a "project workspace" that references specific commits from each component repository. You get: - -* **One clone command** to pull all three repositories -* **One directory structure** to navigate all your code -* **Explicit version locking** so everyone uses compatible versions -* **Independent repository histories** so teams can still work autonomously -* **Cross-component refactoring** because all the code is right there - -**Key benefits:** - -* ✅ **Single clone, instant setup** — New developers get the entire working stack with one command -* ✅ **Version alignment** — The parent repo locks specific commits, ensuring compatible versions work together -* ✅ **IDE-friendly structure** — Open one workspace folder and see all your code in the file tree -* ✅ **Cross-repo refactoring** — Change an API endpoint and update the Angular service call in the same IDE session -* ✅ **Centralized documentation** — One README at the top level explains how everything fits together -* ✅ **Integration testing confidence** — You're always testing against the exact versions that deploy together - ---- - -## 🚀 How It Works - -This tutorial implements the **CAT (Client, API, Token Service)** pattern using three separate repositories linked via git submodules: - -``` -AngularNetTutorial/ # Parent workspace repo -├── Clients/TalentManagement-Angular/ # Submodule → separate repo -├── ApiResources/TalentManagement-API/ # Submodule → separate repo -└── TokenService/Duende-IdentityServer/ # Submodule → separate repo -``` - -### Step 1: Clone the Entire Workspace - -**Single command gets you all three repositories:** - -```bash -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git -cd AngularNetTutorial -``` - -**What this does:** Clones the parent repository and automatically initializes all three submodules at their pinned commits. No manual setup, no "oops, I forgot to clone the API repo." - -The parent repo stores exact commit SHAs for each submodule: - -```bash -# Parent repo internally tracks: -Clients/TalentManagement-Angular-Material → commit abc123def456789012345678901234567890abcd -ApiResources/TalentManagement-API → commit 789ghi012jkl345678901234567890123456abcd -TokenService/Duende-IdentityServer → commit 345mno678pqr901234567890123456789012abcd -``` - -**Key point:** Everyone on your team gets **identical code** across all three repositories. No version mismatches. - -### Step 2: Make Changes Across Repositories - -**Here's where submodules shine: cross-component changes become trivial.** - -```bash -# 1. Make changes in the API submodule -cd ApiResources/TalentManagement-API -git checkout -b feature/new-endpoint -git commit -m "Add GET /api/v1/employees/{id}/reports endpoint" -git push origin feature/new-endpoint - -# 2. Make changes in the Angular submodule -cd ../../Clients/TalentManagement-Angular-Material -git checkout -b feature/reports-ui -git commit -m "Add employee reports component" -git push origin feature/reports-ui - -# 3. Update parent repo references -cd ../.. -git add ApiResources/TalentManagement-API -git add Clients/TalentManagement-Angular-Material -git commit -m "Update submodules: Add employee reports feature" -git push -``` - -**Key point:** The parent repo now guarantees that the API reports endpoint and the Angular reports UI are always tested and deployed together. - -### Step 3: Sync Team Changes - -**When a teammate pushes changes, you sync everything at once:** - -```bash -# Pull parent repo changes -git pull - -# Update all submodules to their new referenced commits -git submodule update --init --recursive -``` - -**No more "It works on my machine but not yours."** If the parent repo says API commit `abc123` goes with Angular commit `def456`, that's what everyone gets — guaranteed. - ---- - -## 💻 Try It Yourself - -**Start all three services** — See [Quick Start Setup](../QUICKSTART.md) for startup commands, application URLs, and test credentials. - ---- - -## 📊 Real-World Impact - -**Before using git submodules:** - -* ❌ **30+ minutes** to onboard a new developer (clone three repos, read three READMEs, figure out versions) -* ❌ **Weekly integration bugs** because someone tested old API with new frontend -* ❌ **10+ Slack messages per day** asking "Which branch should I use for the API?" -* ❌ **Context switching tax** from jumping between three terminal windows, three IDE instances -* ❌ **Merge conflicts in documentation** when two repos' README files disagree on setup steps - -**After adopting git submodule workspace:** - -* ✅ **5 minutes** to onboard a new developer (one clone command, one README, instant compatibility) -* ✅ **Zero integration bugs** from version mismatches (parent repo enforces correct pairings) -* ✅ **Self-service setup** because the repository structure is self-documenting -* ✅ **Single IDE workspace** with all code visible in the file tree -* ✅ **Centralized documentation** that can't drift because there's only one source of truth - ---- - -## 🌟 Why This Matters - -**The submodule workspace pattern isn't just about convenience—it fundamentally changes how you think about microservice development.** - -Traditional microservices prioritize deployment independence, which is critical for production operations. But during development, that same independence creates friction. You're constantly asking: "Which version of the API should I use? Is this frontend branch compatible with that auth server commit?" - -**Git submodules bridge the gap.** You get deployment independence (each service is still a separate repo with its own release cycle) while maintaining development cohesion (the workspace repo locks compatible versions together). - -**Transferable skills:** - -* **Repository orchestration** — Applicable to any multi-repo project (microservices, microfrontends, monorepo migrations) -* **Version alignment strategies** — Useful for managing dependencies across teams and services -* **Workspace-based development** — Foundation for understanding tools like Nx, Turborepo, and Bazel - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover this approach! -* 🐛 **Issue reports** — Found a bug or have a suggestion? Let us know -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements and fixes are always appreciated - -**Found this helpful?** Share it with your team! If your organization struggles with multi-repo coordination, this pattern could save hours every week. - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #git #gitsubmodules #microservices #fullstack #angular #dotnet #oauth2 #identityserver #developerproductivity #bestpractices #webdevelopment #softwarearchitecture #monorepo #typescript #csharp #devops #enterpriseapplications diff --git a/blogs/series-0-architecture/0.2-playwright-testing.md b/blogs/series-0-architecture/0.2-playwright-testing.md deleted file mode 100644 index 064945d..0000000 --- a/blogs/series-0-architecture/0.2-playwright-testing.md +++ /dev/null @@ -1,215 +0,0 @@ -# End-to-End Testing Made Simple: How Playwright Transforms Testing for Angular Applications - -## Comprehensive E2E Testing for Modern Full-Stack Applications - -Testing complex applications is hard. Testing applications with OAuth authentication, role-based access control, and API integrations is even harder. That's where **Playwright** comes in — a modern, powerful testing framework that makes comprehensive E2E testing not just possible, but actually enjoyable. - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This Playwright article picks up where that tutorial left off** — showing you how to write automated end-to-end tests that verify the entire stack works correctly together. - ---- - -## 🎓 What You'll Learn - -* **Playwright fundamentals** — Why it outperforms older frameworks like Selenium for modern SPAs -* **Testing architecture** — How to structure tests across auth, API, CRUD, and workflow categories -* **Page Object Model** — Reusable UI abstractions that survive UI changes -* **Authentication testing** — Test full OAuth 2.0 / OIDC flows with role-based access -* **API testing** — Validate JWT tokens and make authenticated HTTP requests -* **Cross-browser testing** — Run the same tests on Chromium, Firefox, and WebKit - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **AngularNetTutorial stack running locally** — All three services (Angular, API, IdentityServer) must be up -* **Node.js 20.x LTS** — Required to install and run Playwright — [Download](https://nodejs.org/) -* **Basic TypeScript knowledge** — Tests are written in TypeScript -* **Git with submodules** — The Playwright tests live in a separate submodule - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem - -Our tutorial project follows the **CAT Pattern** — Client (Angular), API Resource (.NET), and Token Service (IdentityServer). This architecture presents unique testing challenges: - -**Authentication complexity:** - -* OAuth 2.0 / OpenID Connect flows with browser redirects -* Token acquisition, storage, and validation across requests -* Session management and token expiry handling - -**Multi-tier architecture:** - -* Angular SPA on port 4200 -* .NET API on port 44378 -* IdentityServer on port 44310 - -**Role-based access control:** - -* Employee — Read-only access -* Manager — Create/edit permissions -* HRAdmin — Full administrative access - -Traditional testing approaches struggle with this complexity. Manually testing three services, three roles, and dozens of user flows before every release is error-prone and unsustainable. - ---- - -## 💡 The Solution - -**Playwright** is Microsoft's open-source testing framework designed specifically for modern web applications. - -Unlike older tools like Selenium, Playwright was built from the ground up for single-page applications (SPAs): - -* ✅ **Auto-wait** — No more flaky tests due to timing issues -* ✅ **Cross-browser** — Test on Chromium, Firefox, and WebKit with a single codebase -* ✅ **Network interception** — Mock APIs, test offline scenarios, validate requests -* ✅ **TypeScript-first** — Excellent IntelliSense and type safety -* ✅ **All-in-one** — E2E, API testing, and visual regression in a single tool - -**Our Playwright testing suite** lives as a separate Git submodule under `Tests/AngularNetTutorial-Playwright/` — allowing independent development, versioning, and CI/CD pipelines. - ---- - -## 🚀 How It Works - -### Step 1: Understand the Test Structure - -The test suite is organized into focused categories: - -**tests/** — Organized by concern: - -* **auth/** — Login, logout, OIDC redirect flows, token validation -* **api/** — Direct API endpoint tests with Bearer token authentication -* **employee-management/** — CRUD operations per role (Employee, Manager, HRAdmin) -* **workflows/** — Multi-step end-to-end user scenarios - -**page-objects/** — UI abstractions (BaseListPage, BaseFormPage, per-entity subclasses) - -**fixtures/** — Reusable helpers: - -* **auth.fixtures.ts** — Login helpers and token management -* **data.fixtures.ts** — Test data factories -* **api.fixtures.ts** — Authenticated HTTP request helpers - -**What this does:** Separating tests by concern keeps each file focused and makes failures easy to diagnose. - -### Step 2: Use the Page Object Model - -Page Objects abstract UI interactions into reusable classes, so tests stay readable when the UI changes. - -```typescript -// page-objects/employee-list.page.ts -export class EmployeeListPage extends BaseListPage { - constructor(page: Page) { - super(page, '/position-management/employees'); - } -} - -// Usage in test -const employeeList = new EmployeeListPage(page); -await employeeList.goto(); -await employeeList.search('Smith'); -expect(await employeeList.getRowCount()).toBeGreaterThan(0); -``` - -**Key point:** When selectors change, you update one place — the Page Object — not every test. - -### Step 3: Use Fixtures for Authentication - -The `loginAsRole()` helper handles the full OIDC browser redirect flow, so tests stay focused on behavior: - -```typescript -// fixtures/auth.fixtures.ts -await loginAsRole(page, 'manager'); // Logs in as Manager -await loginAsRole(page, 'hradmin'); // Logs in as HRAdmin -await loginAsRole(page, 'employee'); // Logs in as Employee -``` - -Each role maps to credentials in `config/test-users.json`, keeping secrets out of test code. - ---- - -## 💻 Try It Yourself - -**Start all three services** — See [Quick Start Setup](../QUICKSTART.md) for startup commands, application URLs, and test credentials. - -**Install and run Playwright:** - -```bash -cd Tests/AngularNetTutorial-Playwright -npm install -npx playwright install -npx playwright test -``` - -**Useful Playwright commands:** - -```bash -npx playwright test --ui # Interactive UI mode for debugging -npx playwright test --headed # Watch tests in the browser -npx playwright test --project=chromium # Run on specific browser -npx playwright show-report # View detailed HTML report -``` - ---- - -## 📊 Real-World Impact - -**Before Playwright:** - -* ❌ Manual testing of three user roles before every release -* ❌ No automated validation of OAuth redirect flows -* ❌ API integration bugs discovered in staging, not locally -* ❌ No cross-browser coverage (tests only in Chrome) -* ❌ New developers don't know expected behavior for each role - -**After Playwright:** - -* ✅ Automated tests cover all three roles in under 5 minutes -* ✅ Full OIDC authentication flow tested on every commit -* ✅ API tests catch integration issues before they reach staging -* ✅ Chromium, Firefox, and WebKit tested in parallel -* ✅ Tests serve as living documentation of expected behavior - ---- - -## 🌟 Why This Matters - -E2E tests for a full-stack app with OAuth authentication are notoriously hard to write. Most teams skip them and pay the price in production bugs. **Playwright makes them practical** — auto-wait eliminates flakiness, the fixture system handles login boilerplate, and the Page Object Model keeps tests maintainable as the UI evolves. - -The skills you build here transfer beyond this tutorial. Any Angular app, any .NET API, any OAuth provider — the same patterns apply. - -**Transferable skills:** - -* **Page Object Model** — Applicable to any UI testing framework (Cypress, WebdriverIO, etc.) -* **Token-based API testing** — Useful for any JWT-protected REST API -* **OIDC test automation** — Foundation for testing SSO and federated identity flows - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #playwright #angular #dotnet #testing #e2etesting #oauth2 #openidconnect #identityserver #webdevelopment #authentication #typescript #csharp #fullstack #spa #automation #devops diff --git a/blogs/series-1-authentication/1.1-oauth2-pkce-flow.md b/blogs/series-1-authentication/1.1-oauth2-pkce-flow.md deleted file mode 100644 index 234fbc0..0000000 --- a/blogs/series-1-authentication/1.1-oauth2-pkce-flow.md +++ /dev/null @@ -1,572 +0,0 @@ -# Why Your Angular App Needs PKCE: OAuth 2.0 Explained with a Working Demo - -## Follow a Login Request from Browser to IdentityServer and Back — No Theory, Just Code - -You've built a beautiful Angular app. Now you need to add authentication. You search "Angular login OAuth2" and suddenly you're drowning in acronyms — PKCE, OIDC, code_challenge, code_verifier, access tokens, identity tokens. Most tutorials throw a diagram at you and call it done. - -This article is different. We'll follow a real login request step by step through actual working code, from the moment a user clicks "Login" to the moment they see their dashboard — with a Playwright test that proves it works. - -![IdentityServer Login Page — the OAuth 2.0 PKCE flow in action](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/identityserver/identityserver-login-admin.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how the authentication layer works under the hood.** - ---- - -📦 **Library credit:** The authentication layer is built on [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) by Manfred Steyer — the most widely used Angular library for OAuth 2.0 and OIDC. It handles the PKCE code flow, token storage, automatic refresh, and silent renew out of the box. Everything this article covers — the `AuthConfig`, `initCodeFlow()`, token callbacks, and the HTTP interceptor — is powered by this library. - ---- - -## 🎓 What You'll Learn - -* **Why PKCE exists** — The security problem it solves for single-page applications -* **The complete OIDC flow** — Every redirect, every token, in plain English -* **How Angular is configured** — The exact `auth.config.ts` settings and what each one does -* **How the token reaches your API** — The HTTP interceptor that adds `Authorization: Bearer` automatically -* **How to verify it works** — A Playwright test that walks through the full login sequence - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **AngularNetTutorial running locally** — Clone the repo and start all three services (see setup below) -* **Basic Angular knowledge** — Components, services, and dependency injection -* **Familiarity with HTTP** — You know what a request/response looks like - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem: Why Not Just Use Username/Password? - -Imagine your Angular app handles authentication like a traditional web app — a login form that sends a username and password directly to your API, which checks a database and returns a session cookie. - -**This works, but it has serious problems for modern multi-client architectures:** - -* **Your Angular app handles passwords** — If your app has an XSS vulnerability, an attacker steals every user's password -* **Every client reinvents auth** — Your mobile app, Angular app, and CLI tool each manage their own credential logic -* **No single sign-on** — Users log in separately to every application -* **No consent model** — Users can't control what each app is allowed to access - -**The OAuth 2.0 solution:** Don't let your Angular app touch credentials at all. Delegate authentication to a dedicated service — IdentityServer — that specialises in exactly this problem. - -**But OAuth had a vulnerability for SPAs:** The original "Implicit Flow" put the access token directly in the browser URL. Malicious scripts on the same domain could read it. **PKCE (Proof Key for Code Exchange)** fixes this. - ---- - -## 💡 What is PKCE and Why Does it Matter? - -**PKCE is a security extension to OAuth 2.0 that prevents authorization code interception attacks.** - -Here is how it works in plain English: - -**1. Before redirecting to IdentityServer, Angular generates a secret:** - -* Creates a random string called the `code_verifier` (stored in memory) -* Hashes it using SHA-256 to produce the `code_challenge` -* Sends only the `code_challenge` to IdentityServer (the hash, not the secret) - -**2. IdentityServer stores the `code_challenge` and returns an authorization code** - -**3. Angular sends the authorization code back to IdentityServer along with the original `code_verifier`** - -**4. IdentityServer hashes the `code_verifier` and compares it to the stored `code_challenge`** - -* If they match → IdentityServer issues access token and identity token -* If they don't match → Request rejected - -**Why this prevents attacks:** Even if someone intercepts the authorization code mid-flight, they cannot exchange it for tokens without knowing the `code_verifier` — which was never sent over the network. - ---- - -## 🗺️ The OIDC/PKCE Flow at a Glance - -Before diving into code, here is the complete flow mapped out: - -``` - Browser / Angular IdentityServer .NET API - (port 4200) (port 44310) (port 44378) - │ │ │ - [clicks Login] │ │ - generate code_verifier (secret) │ │ - compute code_challenge (hash) │ │ - │ │ │ - │── 1. GET /authorize ───────►│ │ - │ ?response_type=code │ │ - │ &code_challenge=abc123 │ │ - │ &client_id=TalentMgmt │ │ - │ │ │ - │◄── 2. Show login page ──────│ │ - │ │ │ - [enters username + password] │ │ - │── 3. POST credentials ─────►│ │ - │ [validates user] │ - │◄── 4. Redirect to callback ─│ │ - │ /callback?code=xyz789 │ │ - │ │ │ - │── 5. POST /connect/token ──►│ │ - │ code=xyz789 │ │ - │ code_verifier=secret │ hash(secret)==abc123? │ - │ │ ✅ match → issue tokens │ - │◄── 6. id_token ─────────────│ │ - │ + access_token │ │ - │ │ │ - [load user info + permissions] │ │ - │ │ │ - │── 7. GET /api/v1/employees ─────────────────────────►│ - │ Authorization: Bearer │ - │ │ [verify JWT] │ - │ │ [check scopes] │ - │◄───────────────────────────────── 200 OK + JSON ──────│ -``` - -**The PKCE security guarantee:** The `code_verifier` (the secret) is generated in the browser and **never sent to IdentityServer directly** — only its hash (`code_challenge`) is. When Angular later exchanges the code for tokens, it sends the original secret. IdentityServer rehashes it and compares — if someone intercepted the authorization code in Step 4, they cannot use it without the secret. - ---- - -## 🚀 The Complete Login Flow: Step by Step - -Here's exactly what happens when a user clicks "Login" in our Angular app. - -### Step 1: User Clicks the Login Button - -The user menu is rendered by `user-button.ts`. When the user clicks Login: - -```typescript -// src/app/theme/widgets/user-button.ts -login() { - // Redirect directly to IdentityServer for authentication - this.oidcAuth.login(); -} -``` - -This calls into `OidcAuthService`: - -```typescript -// src/app/core/authentication/oidc-auth.service.ts -login(targetUrl?: string): void { - if (targetUrl) { - this.oauthService.initCodeFlow(targetUrl); - } else { - this.oauthService.initCodeFlow(); - } -} -``` - -`initCodeFlow()` is from the `angular-oauth2-oidc` library. It generates the PKCE `code_verifier` and `code_challenge`, then redirects the browser to IdentityServer. - -### Step 2: Browser Redirects to IdentityServer - -The browser navigates to a URL like this: - -``` -https://localhost:44310/connect/authorize - ?response_type=code - &client_id=TalentManagement - &redirect_uri=http://localhost:4200/callback - &scope=openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write - &code_challenge=abc123xyz... - &code_challenge_method=S256 - &state=random-state-value -``` - -All of this is configured once in `auth.config.ts`: - -```typescript -// src/app/config/auth.config.ts -import { AuthConfig } from 'angular-oauth2-oidc'; -import { environment } from '../../environments/environment'; - -export const authConfig: AuthConfig = { - // Duende IdentityServer URL - issuer: environment.identityServerUrl, - - // Where IdentityServer sends the user after login - redirectUri: window.location.origin + '/callback', - - // The client ID registered in IdentityServer - clientId: environment.clientId, - - // What the app is requesting access to - scope: environment.scope, - - // Authorization Code Flow with PKCE (most secure for SPAs) - responseType: 'code', - - // Allow automatic token renewal before expiry - useSilentRefresh: true, - silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', -}; -``` - -And the environment values: - -```typescript -// src/environments/environment.ts -export const environment = { - production: false, - apiUrl: 'https://localhost:44378/api/v1', - identityServerUrl: 'https://localhost:44310', - clientId: 'TalentManagement', - scope: 'openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write', -}; -``` - -### Step 3: User Logs in at IdentityServer - -IdentityServer shows its own login page at `https://localhost:44310`. The Angular app is completely out of the picture — it never sees the username or password. - -The user enters their credentials (e.g., `rosamond33` / `Pa$$word123`) and IdentityServer validates them against its user store. - -### Step 4: IdentityServer Redirects Back with Authorization Code - -After successful login, IdentityServer redirects back to Angular: - -``` -http://localhost:4200/callback - ?code=abc123authcode... - &state=random-state-value - &session_state=xyz789... -``` - -This lands on the `CallbackComponent`: - -```typescript -// src/app/routes/sessions/callback/callback.ts -export class CallbackComponent implements OnInit { - private authService = inject(OidcAuthService); - private router = inject(Router); - - async ngOnInit() { - // initAuth() calls tryLogin() which exchanges the code for tokens - const isAuthenticated = await this.authService.initAuth(); - - if (isAuthenticated) { - this.router.navigate(['/dashboard']); - } else { - // Anonymous access allowed — go to dashboard as Guest - this.router.navigate(['/dashboard']); - } - } -} -``` - -### Step 5: Angular Exchanges the Code for Tokens - -`initAuth()` in `OidcAuthService` calls `tryLogin()` from the library: - -```typescript -// src/app/core/authentication/oidc-auth.service.ts -async initAuth(): Promise { - try { - // Load IdentityServer's metadata (endpoints, public keys, etc.) - await this.oauthService.loadDiscoveryDocument(); - - // Exchange the authorization code for tokens (PKCE verification happens here) - await this.oauthService.tryLogin(); - - if (this.oauthService.hasValidAccessToken()) { - await this.handleSuccessfulLogin(); - return true; - } - return false; - } catch (error) { - console.error('Error during auth initialization:', error); - return false; - } -} -``` - -Behind the scenes, `tryLogin()` sends a POST to IdentityServer's token endpoint: - -``` -POST https://localhost:44310/connect/token - grant_type=authorization_code - code=abc123authcode... - redirect_uri=http://localhost:4200/callback - client_id=TalentManagement - code_verifier=original-random-secret ← PKCE verification -``` - -IdentityServer hashes the `code_verifier` and compares it to the `code_challenge` from Step 2. They match → tokens are issued. - -### Step 6: Tokens are Stored and User State is Updated - -```typescript -// src/app/core/authentication/oidc-auth.service.ts -private async handleSuccessfulLogin(): Promise { - // Extract claims from the identity token (name, email, roles) - const claims = this.oauthService.getIdentityClaims() as UserInfo; - this.userInfoSubject.next(claims); - this.isAuthenticatedSubject.next(true); - - // Notify StartupService to load role-based permissions - this.permissionsChangeSubject.next(); -} -``` - -The `StartupService` listens for this event and loads the user's permissions: - -```typescript -// src/app/core/bootstrap/startup.service.ts -constructor() { - this.oidcAuth.permissionsChange$.subscribe(() => { - this.setPermissions(); - }); -} - -setPermissions() { - const roles = this.oidcAuth.getUserRoles(); - - this.rolesService.flushRoles(); - - if (roles.includes('HRAdmin')) { - this.rolesService.addRoles({ HRAdmin: ['canAdd', 'canDelete', 'canEdit', 'canRead'] }); - } - if (roles.includes('Manager')) { - this.rolesService.addRoles({ Manager: ['canAdd', 'canEdit', 'canRead'] }); - } - if (roles.includes('Employee')) { - this.rolesService.addRoles({ Employee: ['canRead'] }); - } -} -``` - -### Step 7: Every API Request Gets the Bearer Token Automatically - -Once the user is logged in, every HTTP request to the API automatically includes the access token. This is handled by the HTTP interceptor: - -```typescript -// src/app/core/interceptors/auth-token-interceptor.ts -export const authTokenInterceptor: HttpInterceptorFn = (req, next) => { - const authService = inject(OidcAuthService); - - if (!authService.isAuthenticated()) { - return next(req); - } - - const token = authService.getAccessToken(); - - // Clone the request and add the Authorization header - const authReq = req.clone({ - setHeaders: { - Authorization: `Bearer ${token}`, - }, - }); - - return next(authReq); -}; -``` - -The interceptor is registered once in `app.config.ts` and applies to every HTTP call automatically — no manual token management needed anywhere in your components. - ---- - -## 🔑 The Two Tokens: What Each One Does - -After a successful login, Angular holds two tokens. You can see both in action on the Profile page — it displays the user's name, email, and roles decoded from the identity token: - -![Profile Overview Page — identity token claims displayed in the Angular app](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/profile-overview-page.png) - -**Identity Token (id_token)** -* **Purpose:** Proves who the user is (authentication) -* **Contains:** User ID (`sub`), name, email, roles -* **Used by:** Angular — to display user info and set permissions -* **Lifetime:** Short (minutes) - -**Access Token (access_token)** -* **Purpose:** Grants access to protected API resources (authorization) -* **Contains:** Scopes (`app.api.talentmanagement.read`, `.write`), expiry -* **Used by:** .NET Web API — to validate the request -* **Lifetime:** Short (typically 1 hour) -* **Sent as:** `Authorization: Bearer ` header on every API request - ---- - -## 🔄 Automatic Token Renewal: Silent Refresh - -The access token expires after ~1 hour. Rather than forcing the user to log in again, Angular silently renews it using a hidden iframe: - -```typescript -// auth.config.ts -useSilentRefresh: true, -silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', -timeoutFactor: 0.75, // Renew when 75% of token lifetime has passed -``` - -The `silent-refresh.html` file at the app root handles the iframe callback: - -```html - - -``` - -The library calls `setupAutomaticSilentRefresh()` which handles all the timing automatically. - ---- - -## 🧪 Verify It Works: Playwright Test - -Here is a Playwright test that walks through the complete login flow and verifies every step: - -```typescript -// tests/auth/login-flow.spec.ts -import { test, expect } from '@playwright/test'; -import { loginAsRole } from '../../fixtures/auth.fixtures'; - -test.describe('OAuth 2.0 PKCE Login Flow', () => { - - test('should redirect to IdentityServer when clicking Login', async ({ page }) => { - await page.goto('/'); - - // Click the user icon in the toolbar - await page.locator('button[aria-label="User menu"]').click(); - - // Click Login option - await page.locator('button:has-text("Login")').click(); - - // Verify we are redirected to IdentityServer (PKCE flow started) - await page.waitForURL(/localhost:44310/); - await expect(page).toHaveURL(/connect\/authorize/); - - // Verify PKCE parameters are present in the URL - const url = new URL(page.url()); - expect(url.searchParams.get('response_type')).toBe('code'); - expect(url.searchParams.get('client_id')).toBe('TalentManagement'); - expect(url.searchParams.get('code_challenge')).toBeTruthy(); - expect(url.searchParams.get('code_challenge_method')).toBe('S256'); - }); - - test('should complete full login and reach dashboard', async ({ page }) => { - // loginAsRole handles the complete OIDC flow - await loginAsRole(page, 'manager'); - - // Verify we are back in the Angular app - await expect(page).toHaveURL(/localhost:4200/); - - // Verify the user is shown as authenticated (not Guest) - await page.locator('button[aria-label="User menu"]').click(); - await expect(page.locator('text=rosamond33').or(page.locator('text=Manager'))).toBeVisible(); - }); - - test('should attach Bearer token to API requests after login', async ({ page }) => { - await loginAsRole(page, 'manager'); - - // Intercept API calls and verify Authorization header - let capturedToken = ''; - await page.route('**/api/v1/employees**', route => { - const headers = route.request().headers(); - capturedToken = headers['authorization'] ?? ''; - route.continue(); - }); - - // Navigate to employees to trigger API call - await page.goto('/employees'); - - // Verify the Bearer token was sent - expect(capturedToken).toMatch(/^Bearer .+/); - }); - - test('should logout successfully', async ({ page }) => { - await loginAsRole(page, 'manager'); - - // Click user menu and logout - await page.locator('button[aria-label="User menu"]').click(); - await page.locator('button:has-text("Logout")').or( - page.locator('mat-icon:has-text("exit_to_app")').locator('..') - ).click(); - - // IdentityServer shows logout page — click return link - await page.waitForURL(/localhost:44310/); - const returnLink = page.locator('a:has-text("click here")'); - if (await returnLink.isVisible()) { - await returnLink.click(); - } - - // Verify back on Angular app as Guest - await page.waitForURL(/localhost:4200/); - await page.locator('button[aria-label="User menu"]').click(); - await expect(page.locator('text=Guest')).toBeVisible(); - }); - -}); -``` - -**Run the tests:** - -```bash -cd Tests/AngularNetTutorial-Playwright -npx playwright test tests/auth/login-flow.spec.ts --ui -``` - ---- - -## 💻 Try It Yourself - -**Start all three services** — See [Quick Start Setup](../QUICKSTART.md) for startup commands, application URLs, and test credentials. - -**What to observe in browser DevTools:** - -1. Open DevTools → Network tab before clicking Login -2. Click Login and watch the redirect to `localhost:44310/connect/authorize` -3. Check the URL parameters for `code_challenge` and `response_type=code` -4. After login, watch the POST to `localhost:44310/connect/token` -5. Watch subsequent API calls to `localhost:44378/api/v1/...` — each should have `Authorization: Bearer ...` - ---- - -## 📊 Real-World Impact - -**Before OAuth 2.0 / PKCE:** - -* ❌ Angular app stores and transmits user passwords -* ❌ XSS attack → all user passwords compromised -* ❌ No single sign-on — users log into every app separately -* ❌ Custom auth logic duplicated across every client app -* ❌ No standard for what each app is allowed to access - -**After OAuth 2.0 / PKCE:** - -* ✅ Angular app never sees passwords — IdentityServer handles it -* ✅ XSS attack can only compromise the current session's short-lived token -* ✅ One login works across all your applications -* ✅ Standard library (`angular-oauth2-oidc`) handles all auth logic -* ✅ Scopes define exactly what each app can access - ---- - -## 🌟 Why This Matters - -The PKCE flow demonstrated here is the **industry standard for securing single-page applications**. It's not Angular-specific — the same pattern applies to React, Vue, Svelte, and any other SPA framework. - -Understanding PKCE makes you a better full-stack developer because: - -**Transferable skills:** - -* **OAuth 2.0 is everywhere** — Google, GitHub, Microsoft, AWS all use it. Understanding PKCE lets you integrate with any of them -* **Security-first thinking** — Knowing why PKCE exists helps you make better decisions in your own applications -* **Token-based architecture** — JWT access tokens are the lingua franca of modern microservices; this is the foundation - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #angular #oauth2 #pkce #openidconnect #identityserver #webdevelopment #authentication #security #typescript #angularmaterial #spa #jwt #fullstack #dotnet #playwright diff --git a/blogs/series-1-authentication/1.2-angular-route-guards.md b/blogs/series-1-authentication/1.2-angular-route-guards.md deleted file mode 100644 index 68bb682..0000000 --- a/blogs/series-1-authentication/1.2-angular-route-guards.md +++ /dev/null @@ -1,588 +0,0 @@ -# Lock Down Your Angular Routes: Auth Guards with OIDC in 5 Minutes - -## How to Protect Pages, Redirect Unauthenticated Users, and Verify It Works with Playwright - -You've added OAuth 2.0 login to your Angular app. But login alone doesn't protect your pages. An unauthenticated user can still type `/employees/create` in the address bar and land on your form. A logged-in Employee can still navigate directly to `/positions/create` — a page reserved for HRAdmin only. - -Route guards fix this. In two small files, you can protect every authenticated route, enforce role-based access, and redirect unauthorised users automatically — with zero boilerplate in your components. - -![Anonymous user view — the dashboard before login](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/application-dashboard-anonymous.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article covers the Angular route guard layer — what runs before a page loads to decide whether the user is allowed in.** - ---- - -## 🎓 What You'll Learn - -* **How `authGuard` works** — The function that checks OIDC authentication before every protected route -* **How role guards work** — `managerGuard` and `hrAdminGuard` enforce permissions beyond authentication -* **How guards are applied in routes** — A single `canActivate` or `canActivateChild` on the parent protects all children -* **The anonymous access escape hatch** — How `environment.allowAnonymousAccess` lets you develop without logging in -* **How to verify it with Playwright** — Tests that prove guards redirect and block as expected - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **AngularNetTutorial running locally** — All three services started (IdentityServer, API, Angular) -* **Read the PKCE article** — [Why Your Angular App Needs PKCE](#) — this article builds on the auth service explained there -* **Basic Angular routing knowledge** — You know what `Routes` and `canActivate` are - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem: Login Alone Doesn't Protect Routes - -After implementing OIDC login, many developers assume their pages are protected. They're not. - -**Without guards, any user can:** - -* **Type a URL directly** — Navigating to `http://localhost:4200/employees/create` loads the form regardless of login state -* **Bookmark a deep link** — A bookmarked URL bypasses the login flow entirely -* **Escalate privileges** — An Employee role user can access Manager-only or HRAdmin-only forms - -Angular's router runs before any component renders. This is exactly where guards belong — they intercept navigation, check conditions, and either allow or redirect. - ---- - -## 🗺️ Guard Decision Flow - -Here's how the two-layer guard system works in this app: - -``` -User navigates to /employees/create - │ - ▼ - ┌─────────────────────┐ - │ canActivateChild: │ ← on AdminLayout parent route - │ [authGuard] │ - └─────────────────────┘ - │ - isAuthenticated()? - / \ - YES NO - │ │ - ▼ ▼ - ┌──────────┐ oidcAuth.login(targetUrl) - │ canActivate│ → redirect to IdentityServer - │[managerGuard]│ (returns to this URL after login) - └──────────┘ - │ - isManager() || isHRAdmin()? - / \ - YES NO - │ │ - ▼ ▼ - Route router.navigate(['/403']) - renders → Error 403 Forbidden page -``` - -**Two separate concerns, two separate files:** - -* **`auth-guard.ts`** — Are you logged in? If not, go to IdentityServer -* **`role.guard.ts`** — Are you authorised for this specific action? If not, go to `/403` - ---- - -## 🚀 How It Works: The Code - -### The Authentication Guard - -The `authGuard` function is the gatekeeper for all authenticated routes: - -```typescript -// src/app/core/authentication/auth-guard.ts -import { inject } from '@angular/core'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { OidcAuthService } from './oidc-auth.service'; -import { environment } from '../../../environments/environment'; - -export const authGuard = (route?: ActivatedRouteSnapshot, state?: RouterStateSnapshot) => { - const oidcAuth = inject(OidcAuthService); - - // Allow anonymous access if configured in environment - if (environment.allowAnonymousAccess) { - return true; - } - - // Check if user is authenticated via OIDC - if (oidcAuth.isAuthenticated()) { - return true; - } - - // Redirect to IdentityServer — pass target URL so user returns here after login - oidcAuth.login(state?.url); - return false; -}; -``` - -**Three things to notice:** - -* **`environment.allowAnonymousAccess`** — A development flag. Set it to `true` in `environment.ts` and you skip all authentication during local development. Never `true` in production -* **`oidcAuth.isAuthenticated()`** — Calls `oauthService.hasValidAccessToken()` under the hood. If the access token has expired, this returns `false` and the user is sent back to IdentityServer to re-authenticate -* **`oidcAuth.login(state?.url)`** — Passes the current URL as the target. After IdentityServer login completes, the user lands back on the page they tried to visit — not the home page - -### The Role Guards - -Role guards add a second layer on top of authentication: - -```typescript -// src/app/core/authentication/role.guard.ts -import { inject } from '@angular/core'; -import { CanActivateFn, ActivatedRouteSnapshot, Router } from '@angular/router'; -import { OidcAuthService } from './oidc-auth.service'; - -// Manager operations: employees and departments create/edit -export const managerGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { - const authService = inject(OidcAuthService); - const router = inject(Router); - - if (!authService.isAuthenticated()) { - authService.login(); - return false; - } - - if (authService.isManager() || authService.isHRAdmin()) { - return true; - } - - // User is authenticated but lacks the required role - router.navigate(['/403']); - return false; -}; - -// HRAdmin operations: positions and salary ranges create/edit -export const hrAdminGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { - const authService = inject(OidcAuthService); - const router = inject(Router); - - if (!authService.isAuthenticated()) { - authService.login(); - return false; - } - - if (authService.isHRAdmin()) { - return true; - } - - router.navigate(['/403']); - return false; -}; -``` - -**Key difference from `authGuard`:** Role guards redirect to `/403` (Forbidden) — not to IdentityServer. The user *is* authenticated, they just don't have the right role. Sending them to re-login would be confusing and wrong. - -There is also a generic `roleGuard` for declarative configuration: - -```typescript -// Usage: configure required roles via route data -{ - path: 'admin', - canActivate: [roleGuard], - data: { roles: ['HRAdmin'] } -} -``` - -### Applying Guards in the Routes - -The real power comes from how guards are applied in `app.routes.ts`: - -```typescript -// src/app/app.routes.ts -export const routes: Routes = [ - { - path: '', - component: AdminLayout, - canActivate: [authGuard], - canActivateChild: [authGuard], // ← protects ALL child routes - children: [ - { path: 'dashboard', component: Dashboard }, - - // All authenticated users can view lists - { path: 'employees', component: EmployeeListComponent }, - { path: 'employees/:id', component: EmployeeDetailComponent }, - - // Manager or HRAdmin only — create/edit - { path: 'employees/create', component: EmployeeFormComponent, canActivate: [managerGuard] }, - { path: 'employees/edit/:id', component: EmployeeFormComponent, canActivate: [managerGuard] }, - - // HRAdmin only — positions create/edit - { path: 'positions/create', component: PositionFormComponent, canActivate: [hrAdminGuard] }, - { path: 'positions/edit/:id', component: PositionFormComponent, canActivate: [hrAdminGuard] }, - - // HRAdmin only — salary ranges create/edit - { path: 'salary-ranges/create', component: SalaryRangeFormComponent, canActivate: [hrAdminGuard] }, - { path: 'salary-ranges/edit/:id', component: SalaryRangeFormComponent, canActivate: [hrAdminGuard] }, - - // Error pages - { path: '403', component: Error403 }, - ], - }, - // Public routes — no guards - { path: 'callback', component: CallbackComponent }, - { path: 'auth/register', component: Register }, -]; -``` - -**The `canActivateChild` pattern is the key architectural decision:** - -* Placing `canActivate: [authGuard]` and `canActivateChild: [authGuard]` on the `AdminLayout` parent means every child route is automatically protected -* You never forget to add `authGuard` to a new route — if it's a child of `AdminLayout`, it's already covered -* Role guards are only added to the specific routes that need them — list views are accessible to all authenticated users; create/edit forms are not - -### Route Protection Summary - -**Protected by `authGuard` only (all authenticated users):** - -* `/dashboard` — Dashboard -* `/employees` — Employee list -* `/employees/:id` — Employee detail -* `/departments` — Department list -* `/departments/:id` — Department detail -* `/positions` — Position list -* `/salary-ranges` — Salary range list -* `/profile/overview` — User profile - -**Protected by `managerGuard` (Manager + HRAdmin):** - -* `/employees/create` — Create employee -* `/employees/edit/:id` — Edit employee -* `/departments/create` — Create department -* `/departments/edit/:id` — Edit department - -**Protected by `hrAdminGuard` (HRAdmin only):** - -* `/positions/create` — Create position -* `/positions/edit/:id` — Edit position -* `/salary-ranges/create` — Create salary range -* `/salary-ranges/edit/:id` — Edit salary range - -**No guard (public):** - -* `/callback` — OIDC callback (must be public — IdentityServer redirects here) -* `/auth/register` — Registration page - ---- - -## 🧪 Verify It Works: Playwright Tests - -Here are Playwright tests that verify the guard behaviour for each role: - -```typescript -// Tests/AngularNetTutorial-Playwright/tests/auth/role-based-access.spec.ts -import { test, expect } from '@playwright/test'; -import { loginAsRole } from '../../fixtures/auth.fixtures'; - -test.describe('Route Guard — Authentication', () => { - - test('should redirect unauthenticated user to IdentityServer', async ({ page }) => { - // Try to access a protected route without logging in - await page.goto('/employees'); - - // Guard fires → redirected to IdentityServer - await page.waitForURL(/localhost:44310/); - await expect(page).toHaveURL(/connect\/authorize/); - }); - - test('should redirect back to target URL after login', async ({ page }) => { - // Try to access a specific protected page - await page.goto('/employees'); - await page.waitForURL(/localhost:44310/); - - // Complete login at IdentityServer - await page.fill('input[name="Input.Username"]', 'rosamond33'); - await page.fill('input[name="Input.Password"]', 'Pa$$word123'); - await page.click('button[type="submit"]'); - - // Should land back on /employees, not /dashboard - await page.waitForURL(/localhost:4200/); - expect(page.url()).toContain('/employees'); - }); - -}); - -test.describe('Route Guard — Employee Role (read-only)', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'employee'); - }); - - test('should allow Employee to view employee list', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const employeeTable = page.locator('table, mat-table'); - await expect(employeeTable.first()).toBeVisible({ timeout: 5000 }); - }); - - test('should block Employee from accessing create form directly', async ({ page }) => { - // Try to bypass UI and navigate directly to the create route - await page.goto('/employees/create'); - await page.waitForLoadState('networkidle'); - - // managerGuard fires → redirected to /403 - const isOnCreatePage = page.url().includes('employees/create'); - expect(isOnCreatePage).toBe(false); - }); - -}); - -test.describe('Route Guard — Manager Role', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'manager'); - }); - - test('should allow Manager to access employee create form', async ({ page }) => { - await page.goto('/employees/create'); - await page.waitForLoadState('networkidle'); - - // managerGuard passes → form renders - expect(page.url()).toContain('employees/create'); - }); - - test('should block Manager from accessing hrAdmin-only route', async ({ page }) => { - await page.goto('/positions/create'); - await page.waitForLoadState('networkidle'); - - // hrAdminGuard fires → redirected to /403 - const isOnCreatePage = page.url().includes('positions/create'); - expect(isOnCreatePage).toBe(false); - }); - -}); - -test.describe('Route Guard — HRAdmin Role (full access)', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'hrAdmin'); - }); - - test('should allow HRAdmin to access positions create form', async ({ page }) => { - await page.goto('/positions/create'); - await page.waitForLoadState('networkidle'); - - // hrAdminGuard passes → form renders - expect(page.url()).toContain('positions/create'); - }); - - test('should allow HRAdmin to access salary range create form', async ({ page }) => { - await page.goto('/salary-ranges/create'); - await page.waitForLoadState('networkidle'); - - expect(page.url()).toContain('salary-ranges/create'); - }); - -}); -``` - -**Run the tests:** - -```bash -cd Tests/AngularNetTutorial-Playwright -npx playwright test tests/auth/role-based-access.spec.ts --ui -``` - ---- - -## 🗑️ What About Delete? Route Guards Don't Cover It - -You may notice there is no `/employees/delete/:id` route in `app.routes.ts`. That's intentional — Delete is not a page navigation. It's a button action on the list page, and it requires a different protection strategy. - -The screenshot below shows the employee list as seen by an HRAdmin user — all three action buttons (view, edit, delete) are visible: - -![Employee CRUD operations — delete button visible to HRAdmin](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-crud-operations.png) - -**Delete is protected by three layers working together:** - -### Layer 1 — The Button Is Never Rendered for Non-HRAdmin Users - -The delete button uses a custom structural directive `*appHasRole` that removes the element from the DOM entirely if the user doesn't have the required role: - -```html - - -``` - -The `HasRoleDirective` reads the user's roles directly from the OIDC token via `OidcAuthService`. If the user is not HRAdmin, `ViewContainerRef` never creates the element — it doesn't exist in the DOM at all: - -```typescript -// src/app/shared/directives/has-role.directive.ts -private updateView(roles: string | string[]): void { - this.viewContainer.clear(); - - const hasRole = this.checkRole(roles); - - if (hasRole) { - this.viewContainer.createEmbeddedView(this.templateRef); - } - // else: element is simply not rendered -} -``` - -### Layer 2 — A Confirmation Dialog Requires Explicit Intent - -Even for HRAdmin users, clicking Delete does not immediately call the API. A Material confirmation dialog opens first: - -```typescript -// src/app/routes/employees/employee-list.component.ts -deleteEmployee(employee: Employee): void { - const dialogRef = this.dialog.open(ConfirmDialogComponent, { - width: '400px', - data: { - title: 'Delete Employee', - message: `Are you sure you want to delete ${name}? This action cannot be undone.`, - confirmText: 'Delete', - cancelText: 'Cancel', - }, - }); - - dialogRef.afterClosed().subscribe(confirmed => { - if (!confirmed) return; // User clicked Cancel — nothing happens - - this.employeeService.delete(employee.id).subscribe({ ... }); - }); -} -``` - -The user must explicitly click the red **Delete** button in the dialog. Closing the dialog or clicking Cancel aborts the operation. - -### Layer 3 — The API Enforces the HRAdmin Role - -Even if someone bypassed the UI entirely and sent a raw HTTP DELETE request, the .NET Web API has its own authorization. Every DELETE endpoint is decorated with `[Authorize(Policy = "AdminPolicy")]`: - -```csharp -// TalentManagementAPI.WebApi/Controllers/EmployeesController.cs -[HttpDelete("{id}")] -[Authorize(Policy = AuthorizationConsts.AdminPolicy)] -public async Task Delete(Guid id) -{ - return Ok(await Mediator.Send(new DeleteEmployeeByIdCommand { Id = id })); -} -``` - -`AdminPolicy` is configured in `Program.cs` to require the `HRAdmin` role from the JWT token claims: - -```csharp -// Program.cs -options.AddPolicy(AuthorizationConsts.AdminPolicy, policy => - policy.RequireRole(adminRole)); // adminRole = "HRAdmin" from appsettings.json -``` - -The role value comes from the identity token claims issued by IdentityServer — the same `role` claim that Angular reads to show/hide buttons. The API and Angular share the same source of truth. - -**What the API returns if authorization fails:** - -* No token at all → `401 Unauthorized` -* Valid token but wrong role (Employee or Manager) → `403 Forbidden` - -**The complete picture for Delete:** - -``` -Employee user Manager user HRAdmin user - │ │ │ - [views list] [views list] [views list] - │ │ │ - *appHasRole fails *appHasRole fails *appHasRole passes - button not rendered button not rendered button rendered - │ │ │ - [cannot click] [cannot click] [clicks Delete] - │ - Confirmation dialog - │ - [confirms Delete] - │ - DELETE /api/v1/Employees/{id} - Authorization: Bearer - │ - API validates JWT - checks HRAdmin role claim - │ - 200 OK - - [bypasses UI, sends raw DELETE] [bypasses UI, sends raw DELETE] - with Employee token with Manager token - │ │ - API: 403 Forbidden API: 403 Forbidden - (authenticated, wrong role) (authenticated, wrong role) -``` - -**The design principle:** Route guards protect page navigation. Structural directives protect in-page actions. The API protects the data itself. All three are needed — none alone is sufficient. - -This directive-based role protection is covered in depth in the next article: [Show the Right Buttons to the Right People: Role-Based UI in Angular](#). - ---- - -## 💻 Try It Yourself - -**Start all three services** — See [Quick Start Setup](../QUICKSTART.md) for startup commands, application URLs, and test credentials. - -**What to observe:** - -1. Open a private/incognito window and navigate to `http://localhost:4200/employees` — you are immediately redirected to IdentityServer -2. Log in as `antoinette16` (Employee) — you see the employee list but no Create or Edit buttons -3. Try typing `http://localhost:4200/employees/create` in the address bar while logged in as Employee — you land on the 403 page -4. Log in as `rosamond33` (Manager) — Create and Edit buttons appear; but `/positions/create` still shows 403 -5. Log in as `ashtyn1` (HRAdmin) — full access to everything including Positions and Salary Ranges - ---- - -## 📊 Real-World Impact - -**Without route guards:** - -* ❌ Unauthenticated users see protected pages (or broken API errors) -* ❌ Role enforcement exists only in the UI — bypassed by typing a URL -* ❌ Every component must check auth state and redirect manually -* ❌ Security depends on the UI hiding buttons — not on the routing layer - -**With route guards:** - -* ✅ Unauthenticated users are sent to IdentityServer automatically -* ✅ Role enforcement happens at the router — URL navigation is blocked -* ✅ Components are clean — no auth checks, no redirects, no boilerplate -* ✅ One change in `app.routes.ts` instantly secures a new route -* ✅ Return URL means users land exactly where they intended after login - ---- - -## 🌟 Why This Matters - -Route guards are the Angular idiom for **separating authentication concerns from business logic**. Your components don't need to know about tokens, roles, or redirects — they just render. The router handles who is allowed to reach them. - -The pattern here — a broad `canActivateChild` on the layout parent, plus targeted role guards on sensitive actions — scales cleanly. Adding a new feature route requires a single line in `app.routes.ts`. If the route needs role protection, add `canActivate: [managerGuard]`. That's it. - -**Transferable skills:** - -* **`canActivate` and `canActivateChild`** — Applicable to any Angular application, not just OIDC-secured ones -* **Functional guards** — The modern Angular approach (since v14.2); simpler than class-based guards -* **Role-based routing** — The same pattern works with any identity provider: Auth0, Azure AD, Keycloak, or your own backend -* **Return URL pattern** — Standard practice for any app that needs post-login redirection - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #angular #oauth2 #routeguards #openidconnect #identityserver #webdevelopment #authentication #security #typescript #angularmaterial #spa #rbac #fullstack #dotnet #playwright diff --git a/blogs/series-1-authentication/1.3-angular-http-interceptor.md b/blogs/series-1-authentication/1.3-angular-http-interceptor.md deleted file mode 100644 index 474624e..0000000 --- a/blogs/series-1-authentication/1.3-angular-http-interceptor.md +++ /dev/null @@ -1,442 +0,0 @@ -# Never Forget a Bearer Token Again: Angular's HTTP Interceptor Explained - -## How One File Automatically Secures Every API Request in Your Angular App - -You've implemented OIDC login. Your access token is sitting safely in memory. Now every component that calls the API needs to attach it. So you write `this.authService.getAccessToken()` in your employee service. Then again in your department service. Then again in every other service. Then a new developer joins and forgets to add it to the new positions service. - -There is a better way. One file. Zero repetition. Every HTTP request secured automatically. - -![Swagger UI showing the employee API endpoints](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-employees-resource-expanded.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article covers the HTTP interceptor layer — the middleware that silently adds the Bearer token to every outgoing API request.** - ---- - -## 🎓 What You'll Learn - -* **What an HTTP interceptor is** — Angular's middleware pattern for HTTP requests -* **How `authTokenInterceptor` works** — The exact code that adds `Authorization: Bearer` to every request -* **How to register interceptors** — The modern functional approach with `provideHttpClient` -* **The full interceptor chain** — All six interceptors in this app and what each one does -* **How `BaseApiService` stays clean** — API calls with zero auth code in services or components -* **How to verify it with Playwright** — Tests that confirm the Bearer token is attached - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Read the PKCE article** — [Why Your Angular App Needs PKCE](#) — understand how the access token is obtained -* **Read the Route Guards article** — [Lock Down Your Angular Routes](#) — understand how route protection works -* **Basic Angular knowledge** — Services, `HttpClient`, and dependency injection - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem: Token Injection Scattered Everywhere - -Without an interceptor, every service that calls the API needs to handle the token manually: - -```typescript -// ❌ Without interceptor — token code in every service -@Injectable({ providedIn: 'root' }) -export class EmployeeService { - getEmployees(): Observable { - const token = this.authService.getAccessToken(); // ← repeated - const headers = new HttpHeaders({ Authorization: `Bearer ${token}` }); - return this.http.get('/api/v1/employees', { headers }); - } - - deleteEmployee(id: string): Observable { - const token = this.authService.getAccessToken(); // ← repeated again - const headers = new HttpHeaders({ Authorization: `Bearer ${token}` }); - return this.http.delete(`/api/v1/employees/${id}`, { headers }); - } -} -``` - -**Problems with this approach:** - -* **Error-prone** — Every developer must remember to add the token to every method -* **Duplication** — The same three lines appear in every service, every method -* **Silent failures** — A forgotten token causes a `401 Unauthorized` with no clear cause -* **Hard to change** — Updating how you get the token requires changes in dozens of places - ---- - -## 💡 The Solution: HTTP Interceptors - -Angular's HTTP client supports **interceptors** — functions that sit between your code and the network. Every `HttpClient` request passes through the interceptor chain before being sent. Every response passes back through on the way in. - -**Think of interceptors as middleware for HTTP:** - -``` -Your Service Interceptor Chain Network - │ │ │ - │── http.get() ────────►│ │ - │ authToken adds Bearer │ - │ logging records start time │ - │ settings adds language header │ - │ │── GET /api/employees ───►│ - │ │◄── 200 OK ───────────────│ - │ error checks status code │ - │ logging records duration │ - │◄── Observable ─────│ │ -``` - -You write the auth logic once, in one place. Every request gets it automatically. - ---- - -## 🚀 How It Works: The Code - -### The Auth Token Interceptor - -This is the entire interceptor that secures every API call in the application: - -```typescript -// src/app/core/interceptors/auth-token-interceptor.ts -import { HttpInterceptorFn } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { OidcAuthService } from '../authentication/oidc-auth.service'; - -export const authTokenInterceptor: HttpInterceptorFn = (req, next) => { - const authService = inject(OidcAuthService); - - // If not authenticated, pass the request through unchanged - if (!authService.isAuthenticated()) { - return next(req); - } - - // Get the access token from the OIDC library - const token = authService.getAccessToken(); - - if (!token) { - return next(req); - } - - // Clone the request — HTTP requests are immutable, so we must clone to modify - const authReq = req.clone({ - setHeaders: { - Authorization: `Bearer ${token}`, - }, - }); - - // Pass the cloned request with the header to the next interceptor - return next(authReq); -}; -``` - -**Four things to understand:** - -* **`HttpInterceptorFn`** — The modern functional type for interceptors (introduced in Angular 15). No class, no `implements`, no boilerplate -* **`inject(OidcAuthService)`** — Dependency injection works inside functional interceptors. You can inject any service -* **`req.clone()`** — HTTP requests are immutable. You cannot modify them directly. `clone()` creates a copy with your changes applied -* **`return next(req)`** for unauthenticated requests — Anonymous requests (like loading public assets) are passed through unchanged. The app supports anonymous access mode, so not every request needs a token - -### The `getAccessToken()` Method - -The interceptor calls `authService.getAccessToken()`, which delegates to the `angular-oauth2-oidc` library: - -```typescript -// src/app/core/authentication/oidc-auth.service.ts -getAccessToken(): string { - return this.oauthService.getAccessToken(); -} - -isAuthenticated(): boolean { - return this.oauthService.hasValidAccessToken(); -} -``` - -`hasValidAccessToken()` checks both that the token exists **and** that it has not expired. If the token has expired, `isAuthenticated()` returns `false` and the request is sent without a token — which will trigger `401 Unauthorized` from the API. The `errorInterceptor` handles that response. - -### Registering the Interceptor - -The interceptor is registered once in `app.config.ts`: - -```typescript -// src/app/app.config.ts -import { provideHttpClient, withInterceptors } from '@angular/common/http'; -import { authTokenInterceptor } from './core/interceptors/auth-token-interceptor'; -import { interceptors } from '@core'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient( - withInterceptors([authTokenInterceptor, ...interceptors]) - ), - // ... - ], -}; -``` - -`authTokenInterceptor` is listed **first** — it runs before all other interceptors. Every subsequent interceptor and every HTTP call made by `HttpClient` anywhere in the app automatically gets the Bearer token attached. - ---- - -## 🔗 The Full Interceptor Chain - -This app has six interceptors running on every request: - -``` -Request flow (top to bottom): -────────────────────────────────────────────────────────────────── -1. authTokenInterceptor → adds Authorization: Bearer -2. baseUrlInterceptor → prepends base URL to relative paths -3. apiInterceptor → checks API response error codes -4. errorInterceptor → handles 401, 403, 404, 500 responses -5. loggingInterceptor → logs method, URL, status, duration -6. settingsInterceptor → adds Accept-Language header -────────────────────────────────────────────────────────────────── -Request exits to network → API at https://localhost:44378 -Response travels back through the chain in reverse -────────────────────────────────────────────────────────────────── -``` - -**What each one does:** - -* **`authTokenInterceptor`** — Adds `Authorization: Bearer ` to authenticated requests -* **`baseUrlInterceptor`** — Converts relative paths (`/api/v1/employees`) to absolute URLs when `baseUrl` is configured -* **`apiInterceptor`** — Checks if the API response wraps an error code (`code !== 0`) and shows a toast notification -* **`errorInterceptor`** — Handles HTTP error status codes: shows a toast for most errors, navigates to `/403`, `/404`, or `/500` pages for those status codes -* **`loggingInterceptor`** — Logs every request's method, URL, response status, and duration in milliseconds -* **`settingsInterceptor`** — Adds `Accept-Language` header from the user's language preference - -The error interceptor in detail — it handles the `401 Unauthorized` case that occurs when a token expires: - -```typescript -// src/app/core/interceptors/error-interceptor.ts -export function errorInterceptor(req: HttpRequest, next: HttpHandlerFn) { - const router = inject(Router); - const toast = inject(HotToastService); - const errorPages = [STATUS.FORBIDDEN, STATUS.NOT_FOUND, STATUS.INTERNAL_SERVER_ERROR]; - - return next(req).pipe( - catchError((error: HttpErrorResponse) => { - if (errorPages.includes(error.status)) { - // Navigate to Angular error page (403, 404, 500) - router.navigateByUrl(`/${error.status}`, { skipLocationChange: true }); - } else { - // Show toast notification for other errors - toast.error(getMessage(error)); - - if (error.status === STATUS.UNAUTHORIZED) { - // 401 — token missing or expired. Log but don't auto-redirect - // (authGuard handles redirect to IdentityServer for protected routes) - console.warn('Unauthorized access - authentication required'); - } - } - return throwError(() => error); - }) - ); -} -``` - ---- - -## 🗺️ Request Flow: The Full Picture - -Here is what happens from a component calling `getAll()` to the API returning data: - -``` -EmployeeListComponent - │ - │ this.employeeService.getAll() - ▼ -BaseApiService.getAll() - │ - │ this.http.get('https://localhost:44378/api/v1/employees') - │ ← no auth code here — HttpClient handles it - ▼ -── Interceptor Chain ────────────────────────────────────────── -1. authTokenInterceptor - isAuthenticated()? YES → clone req, add Authorization: Bearer eyJ... - NO → pass req unchanged (anonymous) - -2. baseUrlInterceptor - URL already absolute → pass through unchanged - -3. apiInterceptor - (runs on response) → check response.code === 0 ✅ - -4. errorInterceptor - (runs on response) → no error → pass through - -5. loggingInterceptor - logs: "GET https://localhost:44378/api/v1/employees 200 in 42ms" - -6. settingsInterceptor - adds: Accept-Language: en -── End Interceptor Chain ────────────────────────────────────── - │ - │ GET https://localhost:44378/api/v1/employees - │ Authorization: Bearer eyJhbGciOi... - │ Accept-Language: en - ▼ -.NET Web API validates JWT → checks Employee/Manager/HRAdmin role - │ - │ 200 OK + JSON response - ▼ -── Response travels back through chain ──────────────────────── - │ - ▼ -EmployeeListComponent receives Employee[] -``` - ---- - -## 🧹 Clean Services — Zero Auth Code - -Because the interceptor handles token injection, every API service is completely clean: - -```typescript -// src/app/services/api/base-api.service.ts -export abstract class BaseApiService { - protected http = inject(HttpClient); - protected apiUrl = environment.apiUrl; - protected abstract readonly endpoint: string; - - getAll(params?: QueryParams): Observable { - return this.http.get>(`${this.apiUrl}/${this.endpoint}`, { params: ... }) - .pipe(map(response => response.value)); - // ← no token code. The interceptor handles it. - } - - getById(id: string): Observable { - return this.http.get>(`${this.apiUrl}/${this.endpoint}/${id}`) - .pipe(map(response => response.value as T)); - // ← no token code. - } - - delete(id: string): Observable { - return this.http.delete(`${this.apiUrl}/${this.endpoint}/${id}`); - // ← no token code. DELETE still gets the Bearer header automatically. - } -} -``` - -Every entity service — `EmployeeService`, `DepartmentService`, `PositionService` — extends `BaseApiService` and inherits these clean methods. Not a single one contains authentication code. - ---- - -## 🧪 Verify It Works: Playwright Test - -The following Playwright test intercepts the outgoing HTTP request and verifies the Authorization header: - -```typescript -// Tests/AngularNetTutorial-Playwright/tests/auth/login-flow.spec.ts -import { test, expect } from '@playwright/test'; -import { loginAsRole } from '../../fixtures/auth.fixtures'; - -test('should attach Bearer token to API requests after login', async ({ page }) => { - await loginAsRole(page, 'manager'); - - // Capture the next request to the employees API - let capturedAuthHeader = ''; - await page.route('**/api/v1/employees**', route => { - const headers = route.request().headers(); - capturedAuthHeader = headers['authorization'] ?? ''; - route.continue(); - }); - - // Navigate to trigger the API call - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - // Verify the Authorization header was sent with Bearer token format - expect(capturedAuthHeader).toMatch(/^Bearer .+/); -}); - -test('should NOT attach token to requests when not authenticated', async ({ page }) => { - // Don't log in — access as anonymous - await page.goto('/'); - - let capturedAuthHeader = ''; - await page.route('**/api/**', route => { - const headers = route.request().headers(); - capturedAuthHeader = headers['authorization'] ?? ''; - route.continue(); - }); - - await page.waitForLoadState('networkidle'); - - // Anonymous requests should have no Authorization header - expect(capturedAuthHeader).toBe(''); -}); -``` - -**Run the tests:** - -```bash -cd Tests/AngularNetTutorial-Playwright -npx playwright test tests/auth/login-flow.spec.ts --ui -``` - -You can also verify manually in **Chrome DevTools → Network tab:** - -1. Log in as `rosamond33` (Manager) -2. Navigate to `/employees` -3. Find the `GET employees` request in the Network tab -4. Click **Headers** → look for `Authorization: Bearer eyJhbGci...` - ---- - -## 💻 Try It Yourself - -**Start all three services** — See [Quick Start Setup](../QUICKSTART.md) for startup commands, application URLs, and test credentials. - ---- - -## 📊 Real-World Impact - -**Before the interceptor:** - -* ❌ `getAccessToken()` called in every service method -* ❌ New developer forgets token → silent 401 with no obvious cause -* ❌ Token retrieval logic spread across 20+ methods -* ❌ Changing the auth library means updating every service - -**After the interceptor:** - -* ✅ Token logic lives in exactly one place — `auth-token-interceptor.ts` -* ✅ Every new service and every new method gets the token automatically -* ✅ Services are clean — only business logic, no auth concern -* ✅ Changing the auth library means updating one file - ---- - -## 🌟 Why This Matters - -The HTTP interceptor pattern is the **single most impactful architectural decision** you can make for authentication in Angular. It enforces a clean separation: services handle data, interceptors handle cross-cutting concerns. - -The interceptor chain in this app also demonstrates a broader principle — **composition over configuration**. Instead of one monolithic interceptor that does everything, six small interceptors each do one thing well. They are independently testable, independently replaceable, and easy to reason about. - -**Transferable skills:** - -* **Functional interceptors** — The modern Angular approach. Works the same way for any cross-cutting concern: logging, caching, retry logic, compression -* **`req.clone()` pattern** — HTTP immutability is a pattern you'll encounter in other frameworks too (Express middleware, Fetch API wrappers) -* **Interceptor ordering** — Understanding that interceptors run in registration order is critical for debugging unexpected behaviour - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #angular #oauth2 #httpinterceptor #openidconnect #identityserver #webdevelopment #authentication #security #typescript #angularmaterial #spa #jwt #fullstack #dotnet #playwright diff --git a/blogs/series-1-authentication/1.4-angular-role-based-ui.md b/blogs/series-1-authentication/1.4-angular-role-based-ui.md deleted file mode 100644 index 9227b3c..0000000 --- a/blogs/series-1-authentication/1.4-angular-role-based-ui.md +++ /dev/null @@ -1,548 +0,0 @@ -# Show the Right Buttons to the Right People: Role-Based UI in Angular - -## How *appHasRole and ngx-permissions Control What Each User Sees — Without Cluttering Your Components - -Your app has three types of users: Employee, Manager, and HRAdmin. An Employee should see data but not be able to change it. A Manager can add and edit. An HRAdmin can do everything — including delete. How do you make the UI reflect this without writing `if (isManager || isHRAdmin)` scattered through every component? - -This article walks through two complementary techniques used in the AngularNetTutorial app: a custom structural directive that hides individual buttons, and ngx-permissions that controls the navigation menu — both driven by the same OIDC token claims. - -![Employee list page showing role-appropriate action buttons](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-list-page.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article covers role-based UI rendering — controlling which buttons, actions, and navigation items each user role can see.** - ---- - -## 🎓 What You'll Learn - -* **Two approaches to role-based UI** — `*appHasRole` for buttons, ngx-permissions for navigation -* **How `*appHasRole` works** — A structural directive that removes elements from the DOM entirely -* **How ngx-permissions works** — A permission layer that maps OIDC roles to named capabilities -* **How `StartupService` connects them** — Translating OIDC token roles to permissions on login and logout -* **The permission hierarchy** — What HRAdmin, Manager, Employee, and Guest can each do -* **How to verify it with Playwright** — Tests that check button visibility per role - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Read the PKCE article** — [Why Your Angular App Needs PKCE](#) — understand how roles arrive in the OIDC token -* **Read the Route Guards article** — [Lock Down Your Angular Routes](#) — understand how navigation is protected at the router level -* **Basic Angular knowledge** — Directives, structural directives (`*ngIf`), and Angular Material - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem: Role Logic Creeping Into Every Component - -Without a clean abstraction, role checks end up written inline everywhere: - -```html - - - - -``` - -```typescript -// ❌ And in the component too -get canEdit(): boolean { - return this.authService.isManager() || this.authService.isHRAdmin(); -} -``` - -**Problems:** - -* **Duplication** — The same role check appears in every list component, every detail component -* **Coupling** — Components directly depend on `OidcAuthService` for UI decisions -* **Brittleness** — Adding a new role or changing role logic means updating every component -* **Testing overhead** — Every component test needs to mock auth state - ---- - -## 💡 Two Complementary Approaches - -This app uses two techniques, each for a different context: - -``` -Role-Based UI in AngularNetTutorial -├── *appHasRole directive → buttons and sections in components -│ ├── Add Employee button -│ ├── Edit Employee button -│ ├── Delete Employee button -│ └── Dashboard Quick Actions -│ -└── ngx-permissions → sidebar navigation menu items - ├── "Add Employee" menu item (canAdd) - ├── "Add Department" menu item (canAdd) - └── Other menu items with permissions -``` - -Both read from the same source of truth — the `role` claim in the OIDC identity token. - ---- - -## 🚀 Approach 1: The `*appHasRole` Directive - -### What It Does - -`*appHasRole` is a custom structural directive that completely removes an element from the DOM if the user doesn't have the required role. The element is not hidden with CSS — it is never created at all. - -```html - - - - - - - - - - -``` - -The same pattern repeats consistently across all entity pages: - -* **View/detail button** — no `*appHasRole` — all authenticated users can view -* **Edit button** — `*appHasRole="['HRAdmin', 'Manager']"` -* **Delete button** — `*appHasRole="['HRAdmin']"` -* **Add button** — `*appHasRole="['HRAdmin', 'Manager']"` - -### How the Directive Works - -```typescript -// src/app/shared/directives/has-role.directive.ts -@Directive({ - selector: '[appHasRole]', - standalone: true, -}) -export class HasRoleDirective implements OnInit, OnDestroy { - private authService = inject(OidcAuthService); - private templateRef = inject(TemplateRef); - private viewContainer = inject(ViewContainerRef); - private subscription?: Subscription; - - private roles!: string | string[]; - - @Input() set appHasRole(roles: string | string[]) { - this.updateView(roles); - } - - ngOnInit(): void { - // Re-check roles when authentication state changes (login / logout) - this.subscription = this.authService.isAuthenticated$.subscribe(() => { - if (this.roles) { - this.updateView(this.roles); - } - }); - } - - private updateView(roles: string | string[]): void { - this.roles = roles; - this.viewContainer.clear(); // Always clear first - - const hasRole = this.checkRole(roles); - - if (hasRole) { - this.viewContainer.createEmbeddedView(this.templateRef); // Render - } - // else: nothing — the element is simply not in the DOM - } - - private checkRole(roles: string | string[]): boolean { - if (!this.authService.isAuthenticated()) { - return false; - } - if (typeof roles === 'string') { - return this.authService.hasRole(roles); - } - if (Array.isArray(roles)) { - return this.authService.hasAnyRole(roles); // true if user has ANY of the listed roles - } - return false; - } - - ngOnDestroy(): void { - this.subscription?.unsubscribe(); - } -} -``` - -**Three things to notice:** - -* **`ViewContainerRef`** — This is what makes it structural. Instead of hiding with `display:none`, `viewContainer.createEmbeddedView()` and `viewContainer.clear()` actually add and remove the DOM node. An Employee inspecting page source will not find the Delete button at all -* **`isAuthenticated$` subscription** — The directive reacts to login/logout. If a user logs in mid-session, buttons appear immediately without a page refresh -* **`hasAnyRole()`** — When an array is passed (`['HRAdmin', 'Manager']`), the directive shows the element if the user has **at least one** of the listed roles - -### Where Roles Come From - -The directive reads roles directly from the OIDC token via `OidcAuthService`: - -```typescript -// src/app/core/authentication/oidc-auth.service.ts -getUserRoles(): string[] { - const claims = this.oauthService.getIdentityClaims() as any; - if (!claims) return []; - - const role = claims['role']; // 'role' claim from IdentityServer token - - if (Array.isArray(role)) { - return role; // User can have multiple roles - } else if (typeof role === 'string') { - return [role]; // Single role returned as string - } - return []; -} - -hasRole(role: string): boolean { - return this.getUserRoles().includes(role); -} - -hasAnyRole(roles: string[]): boolean { - const userRoles = this.getUserRoles(); - return roles.some(role => userRoles.includes(role)); -} -``` - -The `role` claim is set in IdentityServer when a user logs in — it is part of the identity token issued by Duende IdentityServer. - ---- - -## 🚀 Approach 2: ngx-permissions for the Navigation Menu - -### Why a Different Approach for Navigation? - -Navigation menus are configured in a data file (`menu.json`), not in Angular templates. This makes the `*appHasRole` directive less practical — you'd need a separate directive instance for each menu item. Instead, the app uses **ngx-permissions**, which lets menu configuration declare required permissions as data. - -### The Permission Mapping - -`StartupService` runs at app startup and after every login/logout. It translates OIDC roles into ngx-permissions roles and named permissions: - -```typescript -// src/app/core/bootstrap/startup.service.ts -setPermissions() { - const roles = this.oidcAuth.getUserRoles(); - - const allPermissions = ['canAdd', 'canDelete', 'canEdit', 'canRead']; - - this.rolesService.flushRoles(); // Clear all previous roles - - if (roles.length > 0) { - // Authenticated user — load permissions based on role - this.permissonsService.loadPermissions(allPermissions); - - if (roles.includes('HRAdmin')) { - this.rolesService.addRoles({ HRAdmin: allPermissions }); - // HRAdmin can: canAdd, canDelete, canEdit, canRead - } - if (roles.includes('Manager')) { - this.rolesService.addRoles({ Manager: allPermissions }); - // Manager can: canAdd, canDelete, canEdit, canRead - } - if (roles.includes('Employee')) { - this.rolesService.addRoles({ Employee: ['canRead'] }); - // Employee can only: canRead - } - } else { - // Anonymous user - this.permissonsService.loadPermissions(['canRead']); - this.rolesService.addRoles({ Guest: ['canRead'] }); - // Guest can only: canRead - } -} -``` - -This runs automatically whenever `OidcAuthService` emits a `permissionsChange$` event — which fires after login and after logout: - -```typescript -constructor() { - this.oidcAuth.permissionsChange$.subscribe(() => { - this.setPermissions(); // Re-load permissions on every auth state change - }); -} -``` - -### The Permission Hierarchy - -``` -Role canRead canEdit canAdd canDelete -────────── ──────── ──────── ─────── ───────── -HRAdmin ✅ ✅ ✅ ✅ -Manager ✅ ✅ ✅ ✅ -Employee ✅ ❌ ❌ ❌ -Guest ✅ ❌ ❌ ❌ -``` - -**Note:** Both Manager and HRAdmin get all four permissions. The distinction between Manager and HRAdmin is enforced by **route guards** (for page access) and **`*appHasRole`** (for Delete buttons) — not by the permission names alone. - -### Menu Configuration with Permissions - -The sidebar navigation menu is defined in `menu.json`. Menu items declare their required permissions: - -```json -{ - "menu": [ - { - "route": "dashboard", - "name": "dashboard", - "type": "link", - "icon": "dashboard" - }, - { - "route": "employees", - "name": "employees", - "type": "sub", - "icon": "people", - "children": [ - { - "route": "", - "name": "employeeList", - "type": "link" - }, - { - "route": "create", - "name": "addEmployee", - "type": "link", - "permissions": { - "only": ["canAdd"] - } - } - ] - } - ] -} -``` - -The sidebar template reads `permissions.only` from each menu item and passes it to `*ngxPermissionsOnly`: - -```html - - - - -``` - -An Employee or Guest never sees "Add Employee" in the sidebar because they don't have `canAdd`. A Manager or HRAdmin sees it because `setPermissions()` loaded `canAdd` for their role. - ---- - -## 🗺️ What Each Role Sees - -``` - Employee Manager HRAdmin - ───────────── ───────────── ───────────── -Sidebar nav: - Dashboard ✅ visible ✅ visible ✅ visible - Employee List ✅ visible ✅ visible ✅ visible - Add Employee ❌ hidden ✅ visible ✅ visible - (canAdd required) (no canAdd) (has canAdd) (has canAdd) - -Employee List page: - View button ✅ visible ✅ visible ✅ visible - Edit button ❌ not in DOM ✅ visible ✅ visible - (*appHasRole= (no match) (Manager match) (HRAdmin match) - ['HRAdmin','Mgr']) - - Delete button ❌ not in DOM ❌ not in DOM ✅ visible - (*appHasRole= (no match) (no match) (HRAdmin match) - ['HRAdmin']) - - Add button ❌ not in DOM ✅ visible ✅ visible - (*appHasRole= (no match) (Manager match) (HRAdmin match) - ['HRAdmin','Mgr']) - -Dashboard: - Quick Actions ❌ not in DOM ✅ visible ✅ visible - section (no match) (Manager match) (HRAdmin match) -``` - ---- - -## 🧪 Verify It Works: Playwright Tests - -```typescript -// Tests/AngularNetTutorial-Playwright/tests/auth/role-based-access.spec.ts -import { test, expect } from '@playwright/test'; -import { loginAsRole } from '../../fixtures/auth.fixtures'; - -test.describe('Role-Based UI — Employee (read-only)', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'employee'); - }); - - test('should NOT show Add button to Employee', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const addButton = page.locator('button').filter({ hasText: /create|add.*employee|new/i }); - const isVisible = await addButton.isVisible({ timeout: 2000 }).catch(() => false); - expect(isVisible).toBe(false); - }); - - test('should NOT show Edit buttons to Employee', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const editButtons = page.locator('button').filter({ hasText: /edit/i }); - const isVisible = await editButtons.first().isVisible({ timeout: 2000 }).catch(() => false); - expect(isVisible).toBe(false); - }); - - test('should NOT show Delete buttons to Employee', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const deleteButtons = page.locator('button').filter({ hasText: /delete/i }); - const isVisible = await deleteButtons.first().isVisible({ timeout: 2000 }).catch(() => false); - expect(isVisible).toBe(false); - }); -}); - -test.describe('Role-Based UI — Manager (add and edit)', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'manager'); - }); - - test('should show Add button to Manager', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const addButton = page.locator('button').filter({ hasText: /create|add.*employee|new/i }); - await expect(addButton.first()).toBeVisible({ timeout: 3000 }); - }); - - test('should show Edit buttons to Manager', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const editButtons = page.locator('button, a').filter({ hasText: /edit/i }); - await expect(editButtons.first()).toBeVisible({ timeout: 3000 }); - }); - - test('should NOT show Delete buttons to Manager', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const deleteButtons = page.locator('button').filter({ hasText: /delete/i }); - const isVisible = await deleteButtons.first().isVisible({ timeout: 2000 }).catch(() => false); - expect(isVisible).toBe(false); - }); -}); - -test.describe('Role-Based UI — HRAdmin (full access)', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'hrAdmin'); - }); - - test('should show all action buttons to HRAdmin', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const editButtons = page.locator('button, a').filter({ hasText: /edit/i }); - const deleteButtons = page.locator('button').filter({ hasText: /delete/i }); - - await expect(editButtons.first()).toBeVisible({ timeout: 3000 }); - await expect(deleteButtons.first()).toBeVisible({ timeout: 3000 }); - }); -}); -``` - -**Run the tests:** - -```bash -cd Tests/AngularNetTutorial-Playwright -npx playwright test tests/auth/role-based-access.spec.ts --ui -``` - ---- - -## 💻 Try It Yourself - -**Start all three services** — See [Quick Start Setup](../QUICKSTART.md) for startup commands, application URLs, and test credentials. - -**What to observe:** - -1. Log in as `antoinette16` (Employee) — navigate to `/employees`. No Add, Edit, or Delete buttons. The "Add Employee" menu item in the sidebar is also missing -2. Log in as `rosamond33` (Manager) — Add and Edit buttons appear. Delete is still hidden. "Add Employee" appears in the sidebar -3. Log in as `ashtyn1` (HRAdmin) — All buttons visible including Delete -4. Open browser DevTools → Elements tab while logged in as Employee. Search for the Delete button's HTML — it doesn't exist in the DOM at all - ---- - -## 📊 Real-World Impact - -**Without role-based UI directives:** - -* ❌ Role checks duplicated in every component template and TypeScript -* ❌ Adding a new role requires updating every component -* ❌ Developers forget to add checks on new features -* ❌ Components are tightly coupled to the auth service - -**With `*appHasRole` and ngx-permissions:** - -* ✅ Role checks declared once, at the element level, in HTML -* ✅ New UI elements only need `*appHasRole="['Manager']"` — no TypeScript changes -* ✅ Directive subscribes to auth changes — login/logout updates UI instantly -* ✅ ngx-permissions in menu.json means nav changes need zero Angular code changes - ---- - -## 🌟 Why This Matters - -Role-based UI is about **trust boundaries in your user interface**. Route guards stop unauthorised users from reaching a page. The API stops unauthorised users from modifying data. Role-based UI ensures that authorised users only see the actions they're supposed to take — reducing confusion and accidental operations. - -The combination of `*appHasRole` (reactive, OIDC-driven, DOM-level) and ngx-permissions (config-driven, permission-named) is a pattern that scales cleanly. As the app grows — more entity types, more roles, more nuanced permissions — each approach handles its layer without bleeding into the other. - -**Transferable skills:** - -* **Structural directives** — The `ViewContainerRef` pattern for conditional rendering is more powerful than `*ngIf` when you need reactive re-evaluation or the element must truly not exist in the DOM -* **ngx-permissions** — Works with any authentication system, not just OIDC; the key insight is mapping business permissions (`canAdd`) to auth roles rather than using auth roles directly in templates -* **Separation of concerns** — UI rendering, routing, and API protection are three separate layers. Each must be implemented independently - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #angular #oauth2 #rbac #rolebased #angularmaterial #ngxpermissions #webdevelopment #authentication #security #typescript #spa #fullstack #dotnet #playwright #ux diff --git a/blogs/series-2-dotnet-api/2.1-dotnet-clean-architecture.md b/blogs/series-2-dotnet-api/2.1-dotnet-clean-architecture.md deleted file mode 100644 index 9687b1a..0000000 --- a/blogs/series-2-dotnet-api/2.1-dotnet-clean-architecture.md +++ /dev/null @@ -1,902 +0,0 @@ -# How to Structure a .NET 10 API So It Doesn't Become a Mess - -## A Walking Tour of Clean Architecture: Domain, Application, Infrastructure, and WebApi Layers - -Every .NET API project starts the same way — clean, organized, full of good intentions. Six months later, controllers are calling `DbContext` directly, validation logic is scattered everywhere, and nobody wants to touch the codebase for fear of breaking something invisible. - -Clean Architecture solves this by enforcing strict rules about which layer can talk to which. This article walks through the exact structure used in the **TalentManagement API** — a real, production-style .NET 10 Web API from the AngularNetTutorial series. You'll see every layer, every pattern, and exactly why each decision was made. - -![Swagger API Endpoints](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-api-endpoints.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how the .NET API is structured using Clean Architecture.** - ---- - -## 📚 What You'll Learn - -* Why Clean Architecture prevents the "big ball of mud" problem -* The four-layer structure: Domain, Application, Infrastructure, and WebApi -* How CQRS and MediatR keep command logic separate from query logic -* How the Repository and Specification patterns eliminate raw LINQ in handlers -* How FluentValidation, Value Objects, and Domain Events fit into the picture -* The complete dependency flow — and why it only points one direction - ---- - -## 🎯 What Is Clean Architecture? - -Clean Architecture (introduced by Robert C. Martin) organizes code into concentric circles. The key rule: **dependencies only point inward**. Outer layers depend on inner layers — never the reverse. - -``` -┌─────────────────────────────────────────────────────────┐ -│ WebApi (Presentation) │ -│ Controllers, Middleware, Swagger, Program.cs │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ Infrastructure │ │ -│ │ EF Core DbContext, Repositories, Email, Cache │ │ -│ │ ┌─────────────────────────────────────────────┐ │ │ -│ │ │ Application │ │ │ -│ │ │ Commands, Queries, Validators, Mappings │ │ │ -│ │ │ ┌───────────────────────────────────────┐ │ │ │ -│ │ │ │ Domain │ │ │ │ -│ │ │ │ Entities, Value Objects, Interfaces │ │ │ │ -│ │ │ └───────────────────────────────────────┘ │ │ │ -│ │ └─────────────────────────────────────────────┘ │ │ -│ └───────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ - -Dependency rule: arrows point INWARD only -Domain knows nothing about Application, Infrastructure, or WebApi -``` - -**What this buys you:** - -* Swap SQL Server for PostgreSQL without touching a single handler -* Add new features without fear of breaking unrelated code -* Test business logic in isolation without a running database - -The TalentManagement API has six projects mapping cleanly to this structure: - -``` -TalentManagementAPI/ -├── src/ -│ ├── Core/ -│ │ ├── TalentManagementAPI.Domain -│ │ └── TalentManagementAPI.Application -│ ├── Infrastructure/ -│ │ ├── TalentManagementAPI.Infrastructure.Persistence -│ │ └── TalentManagementAPI.Infrastructure.Shared -│ └── Presentation/ -│ └── TalentManagementAPI.WebApi -└── tests/ - ├── TalentManagementAPI.Application.Tests - ├── TalentManagementAPI.Infrastructure.Tests - └── TalentManagementAPI.WebApi.Tests -``` - -This project structure was scaffolded from the [VSIXTemplateOnionAPI](https://marketplace.visualstudio.com/items?itemName=workcontrol.VSIXTemplateOnionAPI) Visual Studio extension — a Clean Architecture project template that generates the full layer structure with MediatR, FluentValidation, and EF Core already wired up. The template eliminates the boilerplate so you can focus on domain logic from day one. - ---- - -## 🧩 Layer 1: Domain — The Heart of the System - -The Domain layer is the innermost ring. It has **zero dependencies on any NuGet package** (except the .NET base class library). No EF Core. No MediatR. No HTTP. - -This layer answers one question: **what is the business?** - -### Base Entities - -Every entity in the system inherits from `BaseEntity`, which provides a `Guid` primary key: - -```csharp -public abstract class BaseEntity -{ - public virtual Guid Id { get; set; } -} -``` - -Entities that need audit tracking inherit from `AuditableBaseEntity`: - -```csharp -public abstract class AuditableBaseEntity : BaseEntity -{ - public string CreatedBy { get; set; } - public DateTime Created { get; set; } - public string LastModifiedBy { get; set; } - public DateTime? LastModified { get; set; } -} -``` - -The timestamps are populated automatically by `ApplicationDbContext.SaveChanges()` — more on that in the Infrastructure section. - -### The Employee Entity - -`Employee` is the main aggregate. Notice it doesn't hold `FirstName` and `LastName` as plain strings — it uses a **Value Object**: - -```csharp -public class Employee : AuditableBaseEntity -{ - public PersonName Name { get; set; } // Value Object - - [NotMapped] - public string FirstName => Name?.FirstName; - - [NotMapped] - public string FullName => Name?.FullName; - - public Guid PositionId { get; set; } - public virtual Position Position { get; set; } - - public Guid DepartmentId { get; set; } - public virtual Department Department { get; set; } - - public decimal Salary { get; set; } - public DateTime Birthday { get; set; } - public string Email { get; set; } - public Gender Gender { get; set; } - public string EmployeeNumber { get; set; } - public string Prefix { get; set; } - public string Phone { get; set; } -} -``` - -### Value Objects: Domain Integrity Built In - -A Value Object enforces business rules at construction time. You **cannot create an invalid `PersonName`** — the constructor throws if the rules are violated: - -```csharp -public sealed class PersonName -{ - public string FirstName { get; private set; } - public string MiddleName { get; private set; } - public string LastName { get; private set; } - - public PersonName(string firstName, string middleName, string lastName) - { - FirstName = Normalize(firstName); - LastName = Normalize(lastName); - MiddleName = string.IsNullOrWhiteSpace(middleName) - ? null - : Normalize(middleName); - - if (FirstName.Length == 0 || LastName.Length == 0) - throw new ArgumentException("First and last name are required."); - } - - public string FullName => - string.IsNullOrWhiteSpace(MiddleName) - ? $"{FirstName} {LastName}" - : $"{FirstName} {MiddleName} {LastName}"; - - private static string Normalize(string value) => - string.Join(' ', - (value ?? "").Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries)); -} -``` - -**Why this matters:** validation lives in the domain, not scattered across controllers or services. Any code that creates an `Employee` must produce a valid `PersonName` — the compiler and runtime enforce it. - -The Domain layer also defines **interfaces** that the Application layer uses as abstractions over the database: - -``` -Domain/ -├── Common/ -│ ├── BaseEntity.cs -│ └── AuditableBaseEntity.cs -├── Entities/ -│ ├── Employee.cs -│ ├── Position.cs -│ ├── Department.cs -│ └── SalaryRange.cs -├── ValueObjects/ -│ ├── PersonName.cs -│ ├── PositionTitle.cs -│ └── DepartmentName.cs -└── Enums/ - └── Gender.cs -``` - ---- - -## ⚙️ Layer 2: Application — Orchestrating Business Logic - -The Application layer is where **use cases live**. It knows about the Domain (inner layer) but nothing about EF Core, SQL Server, or HTTP. - -This layer uses **CQRS** (Command Query Responsibility Segregation) with **MediatR**. Every operation is either: - -* A **Command** — changes state (Create, Update, Delete) -* A **Query** — reads state (Get, GetById, GetAll) - -``` -Application/ -├── Features/ -│ ├── Employees/ -│ │ ├── Commands/ -│ │ │ ├── CreateEmployee/ -│ │ │ │ ├── CreateEmployeeCommand.cs -│ │ │ │ └── CreateEmployeeCommandValidator.cs -│ │ │ ├── UpdateEmployee/ -│ │ │ └── DeleteEmployeeById/ -│ │ └── Queries/ -│ │ ├── GetEmployees/ -│ │ └── GetEmployeeById/ -│ ├── Departments/ -│ ├── Positions/ -│ └── SalaryRanges/ -├── Interfaces/ -│ └── Repositories/ -│ └── IEmployeeRepositoryAsync.cs -├── Specifications/ -│ └── Employees/ -│ └── EmployeesByFiltersSpecification.cs -├── Behaviours/ -│ └── ValidationBehavior.cs -└── Mappings/ - └── GeneralProfile.cs -``` - -### Commands: Create, Update, Delete - -Each command is a self-contained unit: the **request**, the **handler**, and the **validator** live in the same folder. - -Here's `CreateEmployeeCommand`: - -```csharp -public class CreateEmployeeCommand : IRequest> -{ - public string FirstName { get; set; } - public string MiddleName { get; set; } - public string LastName { get; set; } - public Guid PositionId { get; set; } - public Guid DepartmentId { get; set; } - public decimal Salary { get; set; } - public DateTime Birthday { get; set; } - public string Email { get; set; } - public Gender Gender { get; set; } - public string EmployeeNumber { get; set; } - public string Prefix { get; set; } - public string Phone { get; set; } - - public class CreateEmployeeCommandHandler - : IRequestHandler> - { - private readonly IEmployeeRepositoryAsync _repository; - private readonly IMapper _mapper; - private readonly IEventDispatcher _eventDispatcher; - - public CreateEmployeeCommandHandler( - IEmployeeRepositoryAsync repository, - IMapper mapper, - IEventDispatcher eventDispatcher) - { - _repository = repository; - _mapper = mapper; - _eventDispatcher = eventDispatcher; - } - - public async Task> Handle( - CreateEmployeeCommand request, - CancellationToken cancellationToken) - { - var employee = _mapper.Map(request); - await _repository.AddAsync(employee); - await _eventDispatcher.PublishAsync( - new EmployeeChangedEvent(employee.Id), cancellationToken); - return Result.Success(employee.Id); - } - } -} -``` - -Notice what the handler does **not** do: - -* No `new SqlConnection(...)` — it uses `IEmployeeRepositoryAsync` (an interface) -* No `if (firstName == null)` — that's the validator's job -* No cache manipulation — a domain event handles that - -### FluentValidation: Rules Without Noise - -Every command has a paired validator that MediatR runs automatically via a pipeline behavior: - -```csharp -public class CreateEmployeeCommandValidator - : AbstractValidator -{ - public CreateEmployeeCommandValidator() - { - RuleFor(e => e.FirstName) - .NotEmpty().WithMessage("{PropertyName} is required.") - .MaximumLength(100); - - RuleFor(e => e.LastName) - .NotEmpty().WithMessage("{PropertyName} is required.") - .MaximumLength(100); - - RuleFor(e => e.Email) - .NotEmpty() - .EmailAddress().WithMessage("{PropertyName} must be a valid email."); - - RuleFor(e => e.EmployeeNumber) - .NotEmpty() - .MaximumLength(20); - - RuleFor(e => e.PositionId) - .NotEmpty(); - - RuleFor(e => e.DepartmentId) - .NotEmpty(); - - RuleFor(e => e.Salary) - .GreaterThanOrEqualTo(0); - - RuleFor(e => e.Birthday) - .LessThan(DateTime.UtcNow); - } -} -``` - -The `ValidationBehavior` pipeline behavior intercepts every `Mediator.Send()` call, runs all registered validators for that request type, and throws a `ValidationException` if any rule fails — **before the handler ever runs**. - -### Specification Pattern: No Raw LINQ in Handlers - -Instead of writing LINQ queries inside handlers (which mixes query logic with business logic), all query logic lives in **Specification** classes using the Ardalis.Specification library: - -```csharp -public class EmployeesByFiltersSpecification : Specification -{ - public EmployeesByFiltersSpecification( - GetEmployeesQuery request, bool applyPaging = true) - { - // Multi-field filter (OR logic) - var hasLastName = !string.IsNullOrWhiteSpace(request.LastName); - var hasFirstName = !string.IsNullOrWhiteSpace(request.FirstName); - var hasEmail = !string.IsNullOrWhiteSpace(request.Email); - - if (hasLastName || hasFirstName || hasEmail) - { - var lastName = request.LastName?.ToLower().Trim() ?? ""; - var firstName = request.FirstName?.ToLower().Trim() ?? ""; - var email = request.Email?.ToLower().Trim() ?? ""; - - Query.Where(e => - (hasLastName && e.Name.LastName.ToLower().Contains(lastName)) || - (hasFirstName && e.Name.FirstName.ToLower().Contains(firstName)) || - (hasEmail && e.Email.ToLower().Contains(email)) - ); - } - - // Eager loading - Query.Include(e => e.Position); - - // Sorting - Query.OrderBy(e => e.Name.LastName); - - // Pagination - if (applyPaging && request.PageSize > 0) - { - Query.Skip((request.PageNumber - 1) * request.PageSize) - .Take(request.PageSize); - } - - Query.AsNoTracking().TagWith("GetEmployeesByFilters"); - } -} -``` - -The handler just passes the specification to the repository — no SQL, no LINQ, no DbContext reference: - -```csharp -var pagedSpec = new EmployeesByFiltersSpecification(request); -var employees = await _repository.ListAsync(pagedSpec); -``` - -### Repository Interfaces - -The Application layer defines the contracts. Infrastructure provides the implementations: - -```csharp -public interface IGenericRepositoryAsync where T : class -{ - Task GetByIdAsync(Guid id); - Task AddAsync(T entity); - Task UpdateAsync(T entity); - Task DeleteAsync(T entity); - Task> ListAsync(ISpecification specification); - Task CountAsync(ISpecification specification); -} - -public interface IEmployeeRepositoryAsync - : IGenericRepositoryAsync -{ - Task<(IEnumerable data, RecordsCount recordsCount)> - GetEmployeeResponseAsync(GetEmployeesQuery requestParameters); -} -``` - -### Domain Events: Decoupled Side Effects - -After an employee is created or updated, a **domain event** notifies other parts of the system (such as the cache layer) without the handler needing to know who's listening: - -```csharp -// Sealed record — immutable by design -public sealed record EmployeeChangedEvent(Guid EmployeeId) : IDomainEvent; -``` - -The `EventDispatcher` resolves all registered handlers for the event type and invokes them: - -```csharp -public async Task PublishAsync(TEvent domainEvent, CancellationToken ct = default) - where TEvent : IDomainEvent -{ - var handlers = _serviceProvider.GetServices>(); - foreach (var handler in handlers) - { - await handler.HandleAsync(domainEvent, ct); - } -} -``` - -The `CacheInvalidationEventHandler` listens for `EmployeeChangedEvent` and clears the employee list cache — no direct dependency between the command handler and the cache layer. - -**The caching layer itself is powered by EasyCaching** — registered via `AddEasyCachingInfrastructure()` in `Program.cs`. EasyCaching supports both **in-memory** (single-server, zero dependencies) and **distributed** (Redis, for multi-instance deployments) providers. Switching between them is a one-line change in `appsettings.json` with no code changes required. Article 2.5 covers the full EasyCaching setup, TTL configuration, and diagnostic headers. - -### Object Mapping with Mapster - -Commands arrive as flat DTOs. The domain uses value objects. **Mapster** bridges the gap: - -```csharp -config.NewConfig() - .MapWith(src => new Employee - { - Name = new PersonName(src.FirstName, src.MiddleName, src.LastName), - PositionId = src.PositionId, - DepartmentId = src.DepartmentId, - Salary = src.Salary, - // ... - }); - -config.NewConfig() - .Map(dest => dest.FirstName, src => src.Name.FirstName) - .Map(dest => dest.LastName, src => src.Name.LastName); -``` - ---- - -## 🗄️ Layer 3: Infrastructure — Where Data Lives - -The Infrastructure layer implements the Application layer's interfaces. It knows about EF Core, SQL Server, and caching — but nothing in the Application or Domain layers references Infrastructure directly. - -``` -Infrastructure.Persistence/ -├── Contexts/ -│ └── ApplicationDbContext.cs -├── Repositories/ -│ ├── GenericRepositoryAsync.cs -│ └── EmployeeRepositoryAsync.cs -├── SeedData/ -│ └── DbInitializer.cs -└── ServiceRegistration.cs -``` - -### ApplicationDbContext: Auto Audit Timestamps - -The `DbContext` intercepts every `SaveChanges()` call to stamp audit fields automatically — no handler ever has to set `Created` or `LastModified` manually: - -```csharp -public class ApplicationDbContext : DbContext -{ - private readonly IDateTimeService _dateTime; - - public DbSet Employees { get; set; } - public DbSet Departments { get; set; } - public DbSet Positions { get; set; } - public DbSet SalaryRanges { get; set; } - - public override Task SaveChangesAsync( - CancellationToken cancellationToken = default) - { - AssignIds(); - UpdateAuditFields(); - return base.SaveChangesAsync(cancellationToken); - } - - private void AssignIds() - { - foreach (var entry in ChangeTracker.Entries() - .Where(e => e.State == EntityState.Added)) - { - if (entry.Entity.Id == Guid.Empty) - entry.Entity.Id = Guid.NewGuid(); - } - } - - private void UpdateAuditFields() - { - foreach (var entry in ChangeTracker.Entries()) - { - switch (entry.State) - { - case EntityState.Added: - entry.Entity.Created = _dateTime.NowUtc; - break; - case EntityState.Modified: - entry.Entity.LastModified = _dateTime.NowUtc; - break; - } - } - } -} -``` - -**Two small decisions with big impact:** - -* `IDateTimeService` instead of `DateTime.UtcNow` — makes unit tests deterministic -* No-tracking by default (`QueryTrackingBehavior.NoTracking`) — improves read performance - -### GenericRepositoryAsync: One Implementation for All Entities - -The generic repository handles CRUD for any entity. The Ardalis.Specification `SpecificationEvaluator` translates Specification objects into EF Core LINQ queries: - -```csharp -public class GenericRepositoryAsync : IGenericRepositoryAsync - where T : class -{ - private readonly ApplicationDbContext _dbContext; - private readonly DbSet _dbSet; - - public GenericRepositoryAsync(ApplicationDbContext dbContext) - { - _dbContext = dbContext; - _dbSet = dbContext.Set(); - } - - public async Task> ListAsync(ISpecification specification) - { - return await SpecificationEvaluator.Default - .GetQuery(_dbSet.AsQueryable(), specification) - .ToListAsync(); - } - - public async Task AddAsync(T entity) - { - await _dbSet.AddAsync(entity); - await _dbContext.SaveChangesAsync(); - return entity; - } - - public async Task UpdateAsync(T entity) - { - _dbContext.Entry(entity).State = EntityState.Modified; - await _dbContext.SaveChangesAsync(); - } - - public async Task DeleteAsync(T entity) - { - _dbSet.Remove(entity); - await _dbContext.SaveChangesAsync(); - } -} -``` - -### EmployeeRepositoryAsync: Specialized Queries - -`EmployeeRepositoryAsync` extends the generic repository to add employee-specific behavior — data shaping (dynamic field projection): - -```csharp -public class EmployeeRepositoryAsync - : GenericRepositoryAsync, IEmployeeRepositoryAsync -{ - private readonly IDataShapeHelper _dataShaper; - - public async Task<(IEnumerable data, RecordsCount recordsCount)> - GetEmployeeResponseAsync(GetEmployeesQuery request) - { - var recordsTotal = await _repository.CountAsync(); - - // Specification without paging (for filtered count) - var filteredSpec = new EmployeesByFiltersSpecification( - request, applyPaging: false); - - // Specification with paging (for page data) - var pagedSpec = new EmployeesByFiltersSpecification(request); - - var recordsFiltered = await CountAsync(filteredSpec); - var resultData = await ListAsync(pagedSpec); - - // Dynamic field projection: only return requested fields - var shapedData = _dataShaper.ShapeData( - resultData, request.Fields); - - return (shapedData, new RecordsCount - { - RecordsTotal = recordsTotal, - RecordsFiltered = recordsFiltered - }); - } -} -``` - -**Data shaping** lets the client request only the fields it needs: `GET /api/v1/employees?fields=firstName,lastName,email`. The `IDataShapeHelper` validates the requested field names against the view model before applying projection — preventing field name fishing attacks. - -### ServiceRegistration: Feature-Flagged Database - -A standout feature: the database provider is controlled by a **feature flag** in `appsettings.json`. This makes local development and CI testing trivially easy — no SQL Server required: - -```csharp -public static void AddPersistenceInfrastructure( - this IServiceCollection services, - IConfiguration configuration) -{ - var useInMemory = configuration - .GetSection("FeatureManagement") - .GetValue("UseInMemoryDatabase") ?? false; - - if (useInMemory) - { - services.AddDbContext(options => - options.UseInMemoryDatabase("ApplicationDb")); - } - else - { - services.AddDbContextPool((provider, options) => - options.UseSqlServer( - configuration.GetConnectionString("DefaultConnection"), - sql => sql.EnableRetryOnFailure( - maxRetryCount: 5, - maxRetryDelay: TimeSpan.FromSeconds(15), - errorNumbersToAdd: null))); - } - - // Auto-register all repositories via Scrutor - services.Scan(selector => selector - .FromAssemblies(Assembly.GetExecutingAssembly()) - .AddClasses(c => c.AssignableTo(typeof(IGenericRepositoryAsync<>))) - .AsImplementedInterfaces() - .WithTransientLifetime()); -} -``` - -**Scrutor** auto-registers every class that implements `IGenericRepositoryAsync` — adding a new entity requires zero changes to the DI wiring. - ---- - -## 🌐 Layer 4: WebApi — The API Endpoints - -The WebApi (Presentation) layer is the entry point for HTTP requests. Controllers are deliberately thin — they delegate immediately to MediatR: - -``` -WebApi/ -├── Controllers/ -│ ├── BaseApiController.cs -│ └── v1/ -│ ├── EmployeesController.cs -│ ├── DepartmentsController.cs -│ ├── PositionsController.cs -│ └── SalaryRangesController.cs -├── Authorization/ -│ ├── AuthorizationConsts.cs -│ └── AuthEnabledRequirement.cs -├── Middlewares/ -│ ├── ErrorHandlingMiddleware.cs -│ └── RequestTimingMiddleware.cs -└── Program.cs -``` - -### BaseApiController: MediatR Wired In - -Every controller inherits from `BaseApiController`, which gives them access to MediatR without constructor injection boilerplate: - -```csharp -[ApiController] -[Route("api/v{version:apiVersion}/[controller]")] -public abstract class BaseApiController : ControllerBase -{ - private IMediator _mediator; - - protected IMediator Mediator => - _mediator ??= HttpContext.RequestServices.GetService(); -} -``` - -### EmployeesController: One Line Per Endpoint - -The controller's only job is mapping HTTP verbs to MediatR messages. Notice the entire `Get` action is a single line: - -```csharp -[ApiVersion("1.0")] -public class EmployeesController : BaseApiController -{ - [HttpGet] - [AllowAnonymous] - public async Task Get([FromQuery] GetEmployeesQuery filter) - => Ok(await Mediator.Send(filter)); - - [HttpGet("{id}")] - [AllowAnonymous] - public async Task Get(Guid id) - => Ok(await Mediator.Send(new GetEmployeeByIdQuery { Id = id })); - - [HttpPost] - [Authorize] - [ProducesResponseType(StatusCodes.Status201Created)] - public async Task Post(CreateEmployeeCommand command) - { - var result = await Mediator.Send(command); - return CreatedAtAction(nameof(Get), new { id = result.Value }, result); - } - - [HttpPut("{id}")] - [Authorize] - public async Task Put(Guid id, UpdateEmployeeCommand command) - { - if (id != command.Id) return BadRequest(); - return Ok(await Mediator.Send(command)); - } - - [HttpDelete("{id}")] - [Authorize(Policy = AuthorizationConsts.AdminPolicy)] - public async Task Delete(Guid id) - => Ok(await Mediator.Send(new DeleteEmployeeByIdCommand { Id = id })); -} -``` - -**Authorization at a glance:** - -* `[AllowAnonymous]` — GET endpoints are public (read-only data) -* `[Authorize]` — POST and PUT require a valid Bearer token (any authenticated user) -* `[Authorize(Policy = "AdminPolicy")]` — DELETE requires the `HRAdmin` role - -### Role-Based Authorization: Feature-Flagged - -Authorization policies are defined in `Program.cs` and are feature-flagged — set `AuthEnabled: false` in `appsettings.Development.json` to bypass auth entirely during development: - -```csharp -var authEnabled = configuration - .GetSection("FeatureManagement") - .GetValue("AuthEnabled"); - -var adminRole = configuration["ApiRoles:AdminRole"]; // "HRAdmin" -var managerRole = configuration["ApiRoles:ManagerRole"]; // "Manager" - -builder.Services.AddAuthorization(options => -{ - if (authEnabled) - { - options.AddPolicy("AdminPolicy", - policy => policy.RequireRole(adminRole)); - - options.AddPolicy("ManagerPolicy", - policy => policy.RequireRole(managerRole, adminRole)); - } - else - { - // Dev mode: all policies pass - options.AddPolicy("AdminPolicy", - policy => policy.RequireAssertion(_ => true)); - } -}); -``` - -The role names come from `appsettings.json` — changing from `"HRAdmin"` to `"Administrator"` requires a config change, not a code change. - -### Program.cs: Wiring the Layers - -The entry point calls one extension method per layer: - -```csharp -builder.Services.AddApplicationLayer(); // Commands, queries, validators -builder.Services.AddPersistenceInfrastructure(cfg); // EF Core, repositories -builder.Services.AddSharedInfrastructure(cfg); // DateTime, email services -builder.Services.AddEasyCachingInfrastructure(cfg); // Caching layer -``` - -Each layer registers its own dependencies. `Program.cs` doesn't need to know which repositories or handlers exist — the layers self-register via Scrutor scanning. - ---- - -## 🔗 How a Request Flows Through All Four Layers - -Following a `POST /api/v1/employees` request end-to-end: - -``` -HTTP POST /api/v1/employees - │ - ▼ -[WebApi] EmployeesController.Post() - └─► Mediator.Send(CreateEmployeeCommand) - │ - ▼ -[Application] ValidationBehavior (MediatR pipeline) - └─► CreateEmployeeCommandValidator — validates fields - │ - ▼ -[Application] CreateEmployeeCommandHandler - └─► IMapper.Map(command) - │ (Mapster creates new Employee with PersonName value object) - ▼ -[Application] IEmployeeRepositoryAsync.AddAsync(employee) - │ - ▼ -[Infrastructure] EmployeeRepositoryAsync.AddAsync() - └─► DbContext.SaveChangesAsync() - │ (auto-assigns Guid, stamps Created timestamp) - ▼ -[Infrastructure] SQL INSERT INTO Employees - │ - ▼ -[Application] IEventDispatcher.PublishAsync(EmployeeChangedEvent) - └─► CacheInvalidationEventHandler — clears employee cache - │ - ▼ -[WebApi] Returns 201 Created with new employee ID -``` - -No layer skips a step. No controller accesses the database directly. No handler knows about HTTP. - ---- - -## ✅ Why This Architecture Pays Off - -**Testability** — Application layer handlers can be tested with mock repositories. No test database required for business logic tests. - -**Replaceability** — The SQL Server implementation can be swapped for PostgreSQL or Cosmos DB by replacing `Infrastructure.Persistence`. The Application layer is unchanged. - -**Feature flags** — `UseInMemoryDatabase` and `AuthEnabled` flags in `appsettings.json` mean developers can run the full API locally with no SQL Server and no IdentityServer. - -**Zero-boilerplate DI** — Scrutor auto-registers all repositories. Adding a new entity (`SalaryRange`) requires no changes to `ServiceRegistration.cs`. - -**Self-documenting structure** — Finding the "create employee" logic means navigating to `Features/Employees/Commands/CreateEmployee/`. No guessing. - ---- - -## 🔑 Patterns at a Glance - -* **Clean Architecture** — Dependency rule: Domain ← Application ← Infrastructure ← WebApi -* **CQRS** — Commands (write) and Queries (read) in separate classes, separate handlers -* **MediatR** — Single dispatcher decouples controllers from handlers; pipeline behaviors add cross-cutting concerns -* **Repository Pattern** — `IGenericRepositoryAsync` abstracts EF Core; Infrastructure layer provides implementations -* **Specification Pattern** — Ardalis.Specification encapsulates query logic; handlers pass specs, not LINQ -* **Value Objects** — `PersonName`, `PositionTitle`, `DepartmentName` enforce domain rules at construction -* **Domain Events** — `EmployeeChangedEvent` decouples cache invalidation from write handlers -* **FluentValidation** — Command validators run automatically via MediatR pipeline behavior -* **Mapster** — Lightweight object mapping with explicit profiles for Value Object conversion -* **Feature Management** — `AuthEnabled` and `UseInMemoryDatabase` flags for environment-specific behavior - ---- - -## 🌟 Why This Matters - -Clean Architecture is one of those patterns that feels like extra work until the first time you need to swap a database, add a new feature without touching ten files, or write a unit test for a business rule in isolation. In a tutorial project, these benefits aren't immediately obvious — but in a real codebase with multiple developers and evolving requirements, the strict layer rules pay for themselves within weeks. - -The structure here — Domain, Application, Infrastructure, WebApi — isn't specific to .NET. It applies to any backend technology. The names may differ (Hexagonal Architecture, Ports and Adapters, Onion Architecture), but the core rule is identical: business logic must not depend on infrastructure details. - -**Transferable skills:** - -* **Clean Architecture layers** — Applicable to any .NET project, Java Spring, or Python FastAPI backend -* **CQRS with MediatR** — Useful for any system with distinct read/write workloads or complex business rules -* **Repository + Specification Pattern** — Foundation for any data-access layer that needs to stay testable and swappable - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #dotnet #cleanarchitecture #cqrs #mediatr #aspnetcore #webapi #repository #specification #fluentvalidation #entityframeworkcore #ddd #designpatterns #backend #fullstack #angular diff --git a/blogs/series-2-dotnet-api/2.2-dotnet-jwt-validation.md b/blogs/series-2-dotnet-api/2.2-dotnet-jwt-validation.md deleted file mode 100644 index cf3564e..0000000 --- a/blogs/series-2-dotnet-api/2.2-dotnet-jwt-validation.md +++ /dev/null @@ -1,533 +0,0 @@ -# How Your .NET API Knows to Trust Angular — JWT Validation Explained - -## Connecting IdentityServer, Access Tokens, and API Authorization in One Clear Flow - -Your Angular app logs in. IdentityServer issues a token. Angular attaches it to every API request. But what actually happens on the .NET side when that `Authorization: Bearer ...` header arrives? How does the API know the token is genuine, unexpired, and issued for the right audience — without ever calling IdentityServer at runtime? - -This article walks through the exact JWT validation setup in the **TalentManagement API** — from the `appsettings.json` configuration to `TokenValidationParameters`, authorization policies, JwtBearerEvents for debugging, and the feature flag that lets you disable auth entirely during development. - -![Swagger Employee Endpoint](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-employee-resource-expanded.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how the .NET API validates JWT tokens issued by IdentityServer.** - ---- - -## 📚 What You'll Learn - -* How JWT validation works without a round-trip to IdentityServer -* The three things every `.AddJwtBearer()` call validates: issuer, audience, and signature -* How `appsettings.json` wires the API to Duende IdentityServer -* How role-based policies (`AdminPolicy`, `ManagerPolicy`) map to OIDC token claims -* The `AuthEnabled` feature flag for auth-free development -* `JwtBearerEvents` for diagnosing the most common auth failures - ---- - -## 🔐 The Three-Party Trust Model - -Before diving into code, the picture to keep in mind: - -``` -┌─────────────┐ 1. Login ┌──────────────────┐ -│ Angular │ ───────────────► │ IdentityServer │ -│ (Browser) │ ◄─────────────── │ (Token Issuer) │ -└─────────────┘ 2. JWT Token └──────────────────┘ - │ │ - │ 3. GET /api/v1/employees │ (publishes discovery doc) - │ Authorization: Bearer │ /.well-known/openid-configuration - ▼ ▼ -┌─────────────────────────────────────────────────────┐ -│ .NET 10 Web API │ -│ │ -│ On startup: fetch signing keys from IdentityServer │ -│ On each request: validate JWT locally (no HTTP) │ -└─────────────────────────────────────────────────────┘ -``` - -**Key insight:** The API never calls IdentityServer during a request. On startup, it fetches IdentityServer's public signing keys once via the **OIDC discovery document** (`/.well-known/openid-configuration`). Every subsequent JWT validation is done **locally** — fast, no network round-trip, no IdentityServer dependency at runtime. - ---- - -## 📦 What's Inside a JWT Token - -A JWT has three base64-encoded sections separated by dots: `header.payload.signature`. - -The **payload** is where the useful claims live. Decode a token from this app and you'll find: - -```json -{ - "iss": "https://localhost:44310", - "sub": "abc123-user-guid", - "aud": "app.api.talentmanagement", - "exp": 1712345678, - "iat": 1712342078, - "scope": [ - "openid", - "profile", - "email", - "roles", - "app.api.talentmanagement.read", - "app.api.talentmanagement.write" - ], - "role": "HRAdmin", - "email": "ashtyn1@example.com", - "name": "Ashtyn User" -} -``` - -**Claims the API cares about:** - -* **`iss`** (Issuer) — must match `Sts:ValidIssuer` in `appsettings.json` -* **`aud`** (Audience) — must match `Sts:Audience` in `appsettings.json` -* **`exp`** (Expiration) — token must not be expired -* **`role`** — used by authorization policies (`AdminPolicy` checks for `"HRAdmin"`) -* **Signature** — verified against IdentityServer's public key fetched at startup - -The **`scope`** claims (`app.api.talentmanagement.read`, `.write`) tell IdentityServer which resources the client is allowed to access. The API's `Audience` value (`app.api.talentmanagement`) must match one of the scopes the Angular client requested — this is how Angular's `environment.ts` scope string connects to the API. - ---- - -## ⚙️ API Configuration: appsettings.json - -The API points to IdentityServer via three settings under the `Sts` key: - -```json -{ - "Sts": { - "ServerUrl": "https://localhost:44310", - "ValidIssuer": "https://localhost:44310", - "Audience": "app.api.talentmanagement" - }, - "FeatureManagement": { - "AuthEnabled": true - }, - "ApiRoles": { - "EmployeeRole": "Employee", - "ManagerRole": "Manager", - "AdminRole": "HRAdmin" - } -} -``` - -* **`ServerUrl`** — the IdentityServer base URL; used as the `Authority` for the JWT bearer handler. The middleware fetches `https://localhost:44310/.well-known/openid-configuration` at startup to discover signing keys. -* **`ValidIssuer`** — the expected `iss` claim in every JWT. Usually the same as `ServerUrl`, but can differ if IdentityServer is behind a reverse proxy. -* **`Audience`** — the expected `aud` claim. Tokens issued for a different API will be rejected. - -And in `appsettings.Development.json`, one additional safety valve: - -```json -{ - "Sts": { - "IgnoreCertificateErrors": true - } -} -``` - -In development, IdentityServer runs on `localhost` with a self-signed certificate. `IgnoreCertificateErrors: true` prevents the API from rejecting the TLS handshake when fetching discovery keys. This flag is only in the Development config — never production. - ---- - -## 🔧 AddJWTAuthentication: The Full Validation Setup - -The authentication is configured in `ServiceExtensions.cs`. Here's the complete method: - -```csharp -public static void AddJWTAuthentication( - this IServiceCollection services, - IConfiguration configuration) -{ - var authority = configuration["Sts:ServerUrl"]; - var audience = configuration["Sts:Audience"]; - - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.RequireHttpsMetadata = true; - options.IncludeErrorDetails = true; - options.Authority = authority; // fetches discovery doc on startup - options.Audience = audience; - options.SaveToken = true; - - // Dev-only: bypass self-signed certificate errors - var ignoreCertErrors = - configuration.GetValue("Sts:IgnoreCertificateErrors"); - if (ignoreCertErrors) - { - options.BackchannelHttpHandler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }; - } - - // Explicit validation rules - var explicitIssuer = configuration["Sts:ValidIssuer"]; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidIssuer = string.IsNullOrWhiteSpace(explicitIssuer) - ? authority - : explicitIssuer, - ValidateAudience = true, - ValidAudience = audience, - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - ClockSkew = TimeSpan.FromMinutes(2) - }; - - // Diagnostic logging - options.Events = new JwtBearerEvents { /* see below */ }; - }); -} -``` - -### What Each TokenValidationParameter Does - -**`ValidateIssuer = true`** -Rejects any token whose `iss` claim doesn't match `ValidIssuer`. A token from a different IdentityServer instance — even a valid one — will be rejected. - -**`ValidateAudience = true`** -Rejects tokens not intended for this API. If Angular accidentally requests a token for a different resource, the API returns `401` immediately. - -**`ValidateIssuerSigningKey = true`** -Verifies the token's cryptographic signature using the public key downloaded from IdentityServer's JWKS endpoint (`/.well-known/openid-configuration` → `jwks_uri`). A tampered token — even one with a valid issuer and audience — fails this check. - -**`ValidateLifetime = true`** -Rejects expired tokens. The `exp` claim is a Unix timestamp; the middleware compares it to the current UTC time. - -**`ClockSkew = TimeSpan.FromMinutes(2)`** -Allows a 2-minute tolerance for clock differences between the API server and IdentityServer. The default is 5 minutes — this tightens it. Without this, a token could appear expired on the API server even though IdentityServer issued it seconds ago. - -**`SaveToken = true`** -Stores the raw JWT string in the authentication ticket, making it accessible via `HttpContext.GetTokenAsync("access_token")` if needed later (e.g., for downstream API calls). - ---- - -## 🛡️ Authorization Policies: What Roles Can Do - -Authentication answers "who is this user?" Authorization answers "what are they allowed to do?" - -Role-based policies are configured in `Program.cs`: - -```csharp -var authEnabled = builder.Configuration - .GetSection("FeatureManagement") - .GetValue("AuthEnabled"); - -var adminRole = builder.Configuration["ApiRoles:AdminRole"]; // "HRAdmin" -var managerRole = builder.Configuration["ApiRoles:ManagerRole"]; // "Manager" -var employeeRole = builder.Configuration["ApiRoles:EmployeeRole"]; // "Employee" - -builder.Services.AddAuthorization(options => -{ - if (authEnabled) - { - options.AddPolicy("AdminPolicy", - policy => policy.RequireRole(adminRole)); - - options.AddPolicy("ManagerPolicy", - policy => policy.RequireRole(managerRole, adminRole)); - - options.AddPolicy("EmployeePolicy", - policy => policy.RequireRole(employeeRole, managerRole, adminRole)); - } - else - { - // Dev/test mode: all policies auto-pass - options.AddPolicy("AdminPolicy", - policy => policy.RequireAssertion(_ => true)); - // ... - } -}); -``` - -**Policy hierarchy:** - -``` -AdminPolicy → HRAdmin only -ManagerPolicy → Manager or HRAdmin -EmployeePolicy → Employee, Manager, or HRAdmin -``` - -**How these map to the controller:** - -```csharp -[HttpGet] -[AllowAnonymous] // ← no token required -public async Task Get(...) => ... - -[HttpPost] -[Authorize] // ← any authenticated user passes -public async Task Post(...) => ... - -[HttpDelete("{id}")] -[Authorize(Policy = "AdminPolicy")] // ← HRAdmin role required -public async Task Delete(Guid id) => ... -``` - -**What happens when validation fails:** - -``` -Request arrives without a token - → [Authorize] → 401 Unauthorized - → (WWW-Authenticate: Bearer header included) - -Request arrives with valid token, but user has "Employee" role - → [Authorize(Policy = "AdminPolicy")] → 403 Forbidden - → (token is valid; the role claim just doesn't satisfy the policy) -``` - -401 and 403 are different errors. **401** means "prove who you are." **403** means "I know who you are — you just can't do this." - ---- - -## 🔌 How Angular's Scopes Connect to the API Audience - -The Angular `environment.ts` requests these scopes on login: - -```typescript -scope: 'openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write' -``` - -IdentityServer maps `app.api.talentmanagement.read` and `app.api.talentmanagement.write` to the `app.api.talentmanagement` **API resource**. The `aud` claim in the resulting JWT is set to `app.api.talentmanagement` — exactly what the API's `Sts:Audience` expects. - -``` -Angular scope request: "app.api.talentmanagement.read" - │ - ▼ - IdentityServer API Resource: "app.api.talentmanagement" - │ - ▼ - JWT aud claim: "app.api.talentmanagement" - │ - ▼ - API Sts:Audience: "app.api.talentmanagement" ✅ -``` - -If the Angular scope string doesn't include `app.api.talentmanagement.read` (or `.write`), IdentityServer will reject the authorization request with `invalid_scope` before any token is issued. - ---- - -## 🚩 AuthEnabled: Skip Auth During Development - -The `AuthEnabled` feature flag in `appsettings.json` controls whether authentication is required: - -```json -"FeatureManagement": { - "AuthEnabled": true -} -``` - -When `AuthEnabled: false`, `Program.cs` replaces the JWT middleware with a custom `AuthEnabledRequirement` that auto-succeeds: - -```csharp -builder.Services.AddAuthorization(options => -{ - // Default policy: passes if AuthEnabled=false, or if authenticated when true - options.DefaultPolicy = authEnabled - ? new AuthorizationPolicyBuilder() - .AddRequirements(new AuthEnabledRequirement()) - .Build() - : new AuthorizationPolicyBuilder() - .RequireAssertion(_ => true) - .Build(); -}); - -if (authEnabled) -{ - builder.Services.AddJWTAuthentication(builder.Configuration); -} -``` - -The `AuthEnabledRequirementHandler` checks the feature flag at runtime via `IFeatureManagerSnapshot`: - -```csharp -protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - AuthEnabledRequirement requirement) -{ - var enabled = await _featureManager - .IsEnabledAsync("AuthEnabled"); - - if (!enabled) - { - context.Succeed(requirement); // auth disabled → always pass - return; - } - - if (context.User?.Identity?.IsAuthenticated == true) - { - context.Succeed(requirement); - } -} -``` - -**Practical use:** Set `AuthEnabled: false` to run the full API against the Angular app without needing IdentityServer running. Every `[Authorize]` endpoint passes. Useful for UI development when you want to focus on components rather than the auth flow. - ---- - -## 🔍 JwtBearerEvents: Diagnosing Auth Failures - -The JWT middleware fires three events that make auth failures visible in logs: - -```csharp -options.Events = new JwtBearerEvents -{ - OnAuthenticationFailed = context => - { - Log.Error( - context.Exception, - "JWT authentication failed. Authority={Authority}, Audience={Audience}", - options.Authority, - options.Audience); - return Task.CompletedTask; - }, - - OnTokenValidated = context => - { - var issuer = context.SecurityToken?.Issuer; - var subject = context.Principal?.FindFirst("sub")?.Value; - Log.Information( - "JWT token validated. Issuer={Issuer}, Subject={Subject}", - issuer, - subject); - return Task.CompletedTask; - }, - - OnChallenge = context => - { - Log.Warning( - "JWT challenge triggered. Error={Error}, ErrorDescription={ErrorDescription}", - context.Error, - context.ErrorDescription); - return Task.CompletedTask; - } -}; -``` - -**What each event tells you:** - -* **`OnAuthenticationFailed`** fires when the token is structurally invalid, expired, has a bad signature, or has the wrong issuer/audience. The `context.Exception` contains the specific reason. -* **`OnTokenValidated`** fires after a successful validation. Useful for confirming which user the API sees. -* **`OnChallenge`** fires when a request hits a protected endpoint without a valid token — the event that triggers the `401` response. - -**Most common failures and what the log will say:** - -``` -"JWT authentication failed" with SecurityTokenExpiredException - → Token is expired. Angular should have refreshed it automatically. - -"JWT authentication failed" with SecurityTokenInvalidAudienceException - → Audience mismatch. Check Sts:Audience matches the Angular scope's resource name. - -"JWT authentication failed" with SecurityTokenInvalidIssuerException - → Issuer mismatch. IdentityServer URL changed? Check Sts:ValidIssuer. - -"JWT challenge triggered" with no exception (Error=invalid_token) - → Angular isn't sending the token. Check the HTTP interceptor. -``` - ---- - -## 🔄 The Complete Request Lifecycle - -Putting it all together — a `DELETE /api/v1/employees/{id}` from an HRAdmin user: - -``` -Angular: "DELETE /api/v1/employees/abc123" - Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... - │ - ▼ -[API Middleware] UseAuthentication() - 1. Extract Bearer token from Authorization header - 2. Decode JWT header → find signing algorithm (RS256) - 3. Fetch IdentityServer public key (cached from startup) - 4. Verify signature ✅ - 5. Check iss = "https://localhost:44310" ✅ - 6. Check aud = "app.api.talentmanagement" ✅ - 7. Check exp > now ✅ - 8. Populate HttpContext.User with claims (role=HRAdmin, sub=..., email=...) - │ - ▼ -[API Middleware] UseAuthorization() - 9. Controller action has [Authorize(Policy = "AdminPolicy")] - 10. "AdminPolicy" requires role "HRAdmin" - 11. HttpContext.User.IsInRole("HRAdmin") = true ✅ - │ - ▼ -[Controller] EmployeesController.Delete(id) - 12. Mediator.Send(new DeleteEmployeeByIdCommand { Id = id }) - │ - ▼ - 13. 200 OK (or 204 No Content) -``` - -If step 4 fails → `OnAuthenticationFailed` event → `401 Unauthorized` -If step 11 fails → `403 Forbidden` - ---- - -## 🛠️ Common Errors and Fixes - -**API returns 401 on every request** -* Check IdentityServer is running at `https://localhost:44310` -* Check `Sts:ServerUrl` in `appsettings.json` matches the running IdentityServer URL -* Check Angular's `environment.ts` `identityServerUrl` matches -* Run the API and look for the `JWT authentication failed` log line — the exception message will name the exact problem - -**"SecurityTokenInvalidAudienceException" in logs** -* The token's `aud` claim doesn't match `Sts:Audience` -* Check Angular scope includes `app.api.talentmanagement.read` (not some other scope name) -* Check IdentityServer's API resource is named `app.api.talentmanagement` - -**API returns 401 with self-signed cert error** -* Set `Sts:IgnoreCertificateErrors: true` in `appsettings.Development.json` -* This only affects the startup key fetch — not the token validation itself - -**API returns 403 instead of 401** -* The token is valid — the user just lacks the required role -* Check the user's role claim in IdentityServer admin (`https://localhost:44303`) -* Verify `ApiRoles:AdminRole = "HRAdmin"` matches the role name IdentityServer issues - -**`AuthEnabled: false` not working — still getting 401** -* Check the right `appsettings.json` file is loaded (Development vs Production) -* Verify `FeatureManagement:AuthEnabled` is `false`, not `"false"` (string) - ---- - -## 🌟 Why This Matters - -JWT validation is invisible when it works and baffling when it breaks. The `TokenValidationParameters` configuration — issuer, audience, signing key, lifetime — maps directly to claims in the token. Understanding this mapping is what separates developers who copy auth code from those who can debug a `401` in under five minutes. - -The pattern here — configuring `AddJwtBearer`, using OIDC discovery endpoints, validating issuer and audience — is not specific to Duende IdentityServer. The same setup works with Okta, Auth0, Azure AD, or any OIDC-compliant provider. The `Authority` and `Audience` values change; the code structure doesn't. - -**Transferable skills:** - -* **JWT Bearer configuration** — Applicable to any .NET API with OAuth 2.0 / OIDC authentication -* **`TokenValidationParameters`** — Useful for any identity provider (Okta, Auth0, Azure AD, Cognito) -* **`JwtBearerEvents` debugging** — Foundation for diagnosing auth failures in any secured API - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #dotnet #jwt #aspnetcore #webapi #oauth2 #openidconnect #identityserver #authentication #authorization #jwtbearer #cleanarchitecture #fullstack #angular #security #csharp diff --git a/blogs/series-2-dotnet-api/2.3-dotnet-api-versioning.md b/blogs/series-2-dotnet-api/2.3-dotnet-api-versioning.md deleted file mode 100644 index 8b3ece5..0000000 --- a/blogs/series-2-dotnet-api/2.3-dotnet-api-versioning.md +++ /dev/null @@ -1,429 +0,0 @@ -# Future-Proof Your .NET API: Add Versioning Without Breaking Existing Clients - -## Why `/api/v1/` Matters and How to Implement Versioning the Right Way - -You ship a REST API. Angular consumes it. Six months later, a requirement changes — the response shape needs to be different, a field needs to be renamed, or a breaking change is unavoidable. Without versioning, you have two choices: break existing clients, or never evolve the API. Neither is acceptable. - -API versioning solves this by letting multiple versions coexist at the same URL prefix. Clients opt in to the new version when they're ready. Old clients keep working unchanged. - -This article walks through the exact versioning setup in the **TalentManagement API** — how the route templates, version readers, Swagger integration, and Angular client URL all fit together. - -![Swagger API Endpoints](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-departments-resource-expanded.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how the .NET API supports multiple versions without breaking existing clients.** - ---- - -## 📚 What You'll Learn - -* Why URL-based versioning (`/api/v1/`, `/api/v2/`) is the most practical approach -* How `Asp.Versioning.Mvc` wires versioning into ASP.NET Core -* The route template `api/v{version:apiVersion}/[controller]` and what each part means -* Three ways clients can specify a version: URL segment, header, media type -* How `AssumeDefaultVersionWhenUnspecified` keeps old clients working -* How `ReportApiVersions` signals available versions to clients -* How Angular's `environment.ts` locks to a specific API version -* What adding a v2 endpoint alongside v1 looks like in practice - ---- - -## 🎯 Why Version the URL? - -There are three common strategies for API versioning: - -**URL segment versioning** — `/api/v1/employees`, `/api/v2/employees` -* Explicit and visible in every request -* Easy to test in a browser or with curl -* Works with every HTTP client — no special headers required - -**Header versioning** — `x-api-version: 2` -* Keeps URLs clean -* Less discoverable — clients need documentation to know it exists - -**Media type versioning** — `Accept: application/json;version=2` -* Fully REST-compliant (the version is part of the resource representation) -* Complicated to implement and test - -The TalentManagement API supports **all three simultaneously** — but the primary and most practical method is URL segments. Angular's `environment.ts` bakes in `/api/v1/` directly: - -```typescript -export const environment = { - apiUrl: 'https://localhost:44378/api/v1', - // ... -}; -``` - -Every Angular service call goes to `/api/v1/employees`, `/api/v1/positions`, etc. When a v2 is introduced, Angular updates `apiUrl` to `/api/v2/` when it's ready — existing v1 clients are untouched. - ---- - -## 📦 The Package: Asp.Versioning.Mvc - -The API uses the official Microsoft API versioning library: - -```xml - -``` - -`Asp.Versioning.Mvc.ApiExplorer` bundles two things: - -* **`Asp.Versioning.Mvc`** — the core versioning middleware (route constraints, version readers, `[ApiVersion]` attribute) -* **`Asp.Versioning.Mvc.ApiExplorer`** — Swagger/OpenAPI integration (groups endpoints by version, substitutes `{version}` in route templates) - ---- - -## ⚙️ Registration: AddApiVersioning and AddApiExplorer - -Versioning is configured in `ServiceExtensions.cs` via two extension methods, both called from `Program.cs`: - -```csharp -// Program.cs -builder.Services.AddApiVersioningExtension(); -builder.Services.AddMvcCore().AddApiExplorer(); -builder.Services.AddVersionedApiExplorerExtension(); -``` - -### AddApiVersioningExtension — Core Setup - -```csharp -public static void AddApiVersioningExtension( - this IServiceCollection services) -{ - services.AddApiVersioning(config => - { - config.DefaultApiVersion = new ApiVersion(1, 0); - config.AssumeDefaultVersionWhenUnspecified = true; - config.ReportApiVersions = true; - }); -} -``` - -**`DefaultApiVersion = new ApiVersion(1, 0)`** -Sets the fallback version when no version is specified. A request to `/api/employees` (without a version segment) is treated as `/api/v1/employees`. - -**`AssumeDefaultVersionWhenUnspecified = true`** -The backward-compatibility switch. Any client that doesn't send a version — old Angular code, curl scripts, health checks — gets routed to v1 automatically instead of receiving a `400 Bad Request`. - -**`ReportApiVersions = true`** -Adds a response header to every API response listing the available versions: - -``` -api-supported-versions: 1.0 -``` - -Clients can inspect this header to discover what versions exist without reading documentation. - -### AddVersionedApiExplorerExtension — Swagger Integration - -```csharp -public static void AddVersionedApiExplorerExtension( - this IServiceCollection services) -{ - var apiVersioningBuilder = services.AddApiVersioning(options => - { - options.ReportApiVersions = true; - options.DefaultApiVersion = new ApiVersion(1, 0); - options.AssumeDefaultVersionWhenUnspecified = true; - options.ApiVersionReader = ApiVersionReader.Combine( - new UrlSegmentApiVersionReader(), - new HeaderApiVersionReader("x-api-version"), - new MediaTypeApiVersionReader("x-api-version")); - }); - - apiVersioningBuilder.AddApiExplorer(options => - { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - }); -} -``` - -**`ApiVersionReader.Combine(...)`** — Three ways to pass the version: - -``` -# 1. URL segment (primary — most common) -GET /api/v1/employees - -# 2. Custom header -GET /api/employees -x-api-version: 1 - -# 3. Media type parameter -GET /api/employees -Accept: application/json;x-api-version=1 -``` - -The first reader that finds a version wins. If no reader finds one, `AssumeDefaultVersionWhenUnspecified` kicks in. - -**`GroupNameFormat = "'v'VVV"`** — How Swagger groups endpoints. `VVV` is a format specifier that produces `v1`, `v2`, etc. This creates a separate Swagger document per API version. - -**`SubstituteApiVersionInUrl = true`** — Replaces `{version}` in route templates with the actual version number in Swagger UI. Without this, the Swagger UI shows literal `{version}` in every URL instead of `1`. - ---- - -## 🏗️ The Route Template: api/v{version:apiVersion}/[controller] - -The version is embedded directly in every URL via the route template defined on `BaseApiController`: - -```csharp -[ApiController] -[Route("api/v{version:apiVersion}/[controller]")] -public abstract class BaseApiController : ControllerBase -{ - private IMediator _mediator; - protected IMediator Mediator => - _mediator ??= HttpContext.RequestServices.GetService(); -} -``` - -Breaking down `api/v{version:apiVersion}/[controller]`: - -``` -api/ — literal prefix -v — literal "v" character -{version:apiVersion} — route parameter constrained to a valid API version -/ — separator -[controller] — replaced at runtime with the controller name (minus "Controller") -``` - -`EmployeesController` → `/api/v1/employees` -`DepartmentsController` → `/api/v1/departments` -`PositionsController` → `/api/v1/positions` - -The `:apiVersion` constraint is registered by `AddApiVersioning()`. It validates that the value in the `{version}` segment is a known version — requests to `/api/v99/employees` return `400 Bad Request` if v99 isn't registered. - ---- - -## 🏷️ The [ApiVersion] Attribute - -Every controller declares which version(s) it handles with `[ApiVersion]`: - -```csharp -[ApiVersion("1.0")] -public class EmployeesController : BaseApiController -{ - [HttpGet] - [AllowAnonymous] - public async Task Get([FromQuery] GetEmployeesQuery filter) - => Ok(await Mediator.Send(filter)); - - [HttpPost] - [Authorize] - public async Task Post(CreateEmployeeCommand command) { ... } - - [HttpDelete("{id}")] - [Authorize(Policy = AuthorizationConsts.AdminPolicy)] - public async Task Delete(Guid id) { ... } -} -``` - -All controllers in the `/Controllers/v1/` folder declare `[ApiVersion("1.0")]`: - -``` -Controllers/ -├── BaseApiController.cs — [Route("api/v{version:apiVersion}/[controller]")] -└── v1/ - ├── EmployeesController.cs — [ApiVersion("1.0")] - ├── DepartmentsController.cs — [ApiVersion("1.0")] - ├── PositionsController.cs — [ApiVersion("1.0")] - ├── SalaryRangesController.cs — [ApiVersion("1.0")] - ├── DashboardController.cs — [ApiVersion("1.0")] - └── CacheController.cs — [ApiVersion("1.0")] -``` - -Some controllers override the base route when the controller name doesn't match the desired URL segment: - -```csharp -[ApiVersion("1.0")] -[Route("api/v{version:apiVersion}/dashboard")] // ← explicit route -public sealed class DashboardController : BaseApiController -{ - [HttpGet("metrics")] - public async Task GetMetrics() - => Ok(await Mediator.Send(new GetDashboardMetricsQuery())); -} -``` - -Without the explicit `[Route]`, the base class template would generate `/api/v1/dashboard` anyway (since the controller name is `Dashboard`), but the explicit declaration makes the intent unmistakable. - ---- - -## 🔄 How Angular Stays in Sync - -Angular's `BaseApiService` never constructs API URLs manually — it reads `apiUrl` from the environment: - -```typescript -export abstract class BaseApiService { - protected apiUrl = environment.apiUrl; // "https://localhost:44378/api/v1" - - getAll(params?: QueryParams): Observable { - return this.http.get>( - `${this.apiUrl}/${this.endpoint}` - ); - } -} -``` - -`EmployeeService` only specifies the resource name: - -```typescript -@Injectable({ providedIn: 'root' }) -export class EmployeeService extends BaseApiService { - protected readonly endpoint = 'employees'; -} -``` - -This produces: `https://localhost:44378/api/v1/employees` - -**The version is isolated to one place** — `environment.ts`. When v2 is ready, a single line changes: - -```typescript -// Before -apiUrl: 'https://localhost:44378/api/v1', - -// After (once v2 is ready) -apiUrl: 'https://localhost:44378/api/v2', -``` - -No individual service files change. No HTTP calls need to be hunted down. - ---- - -## 🚀 What Adding a v2 Endpoint Looks Like - -When a breaking change is unavoidable, v2 is added **alongside** v1 — existing clients are untouched. - -### Step 1: Create a v2 controller - -```csharp -// Controllers/v2/EmployeesController.cs -namespace TalentManagementAPI.WebApi.Controllers.v2 -{ - [ApiVersion("2.0")] - public class EmployeesController : BaseApiController - { - [HttpGet] - [AllowAnonymous] - public async Task Get([FromQuery] GetEmployeesV2Query filter) - => Ok(await Mediator.Send(filter)); - - // POST, PUT, DELETE remain same as v1 — not every endpoint needs a v2 - } -} -``` - -### Step 2: Register the version - -The new controller just needs `[ApiVersion("2.0")]`. The route template `api/v{version:apiVersion}/[controller]` already handles `/api/v2/employees` — no route changes needed. - -### Step 3: Deprecate v1 when ready - -```csharp -[ApiVersion("1.0", Deprecated = true)] // still works, but signals clients to migrate -[ApiVersion("2.0")] -public class EmployeesController : BaseApiController { ... } -``` - -The `api-supported-versions` header now shows both: - -``` -api-supported-versions: 1.0, 2.0 -api-deprecated-versions: 1.0 -``` - -Angular updates its `environment.ts` to `/api/v2` when ready. Old client deployments still on v1 continue working until the deprecation window closes. - ---- - -## 🔍 Verifying Versioning Works - -**With curl — URL segment:** - -```bash -# v1 explicitly -curl https://localhost:44378/api/v1/employees - -# Default version (no segment — gets v1 due to AssumeDefaultVersionWhenUnspecified) -curl https://localhost:44378/api/employees - -# Unknown version — returns 400 -curl https://localhost:44378/api/v99/employees -``` - -**With curl — header:** - -```bash -curl https://localhost:44378/api/employees \ - -H "x-api-version: 1" -``` - -**Inspect the response headers:** - -```bash -curl -I https://localhost:44378/api/v1/employees - -# Response includes: -# api-supported-versions: 1.0 -``` - -**In Swagger UI:** - -The `GroupNameFormat = "'v'VVV"` setting creates a version dropdown in the Swagger UI. Each version gets its own Swagger document with fully substituted URLs (no `{version}` placeholder). - ---- - -## 🎯 Key Design Decisions - -**Version in the URL, not just headers** -URL-based versioning is visible in browser history, server logs, and analytics. Debugging a 401 error is easier when the URL shows `/api/v1/employees` rather than `/api/employees` with a hidden version header. - -**`AssumeDefaultVersionWhenUnspecified = true`** -Existing Angular code that predates versioning doesn't break. Health checks, monitoring tools, and integration tests that don't pass a version number all land on v1. - -**`ReportApiVersions = true`** -Every response advertises available versions. Angular code could theoretically inspect this header to detect when a new version is available — useful for proactive migration notifications. - -**Version isolated in `environment.ts`** -The Angular `BaseApiService` bakes the version into the base URL. Updating to v2 is a one-line config change, not a codebase-wide refactor. - -**Folder convention mirrors the version** -All v1 controllers live in `/Controllers/v1/`. When v2 is added, a `/Controllers/v2/` folder appears. The folder structure tells you which version a controller belongs to at a glance — before reading a single attribute. - ---- - -## 🌟 Why This Matters - -The moment a public-facing API ships, it becomes a contract. Changing that contract without versioning means breaking every client that depends on it — Angular apps, mobile apps, third-party integrations, monitoring tools. Versioning is how you maintain that contract while still evolving the API. - -The patterns here — URL-based versions, `AssumeDefaultVersionWhenUnspecified`, isolated controller folders, version baked into Angular's `environment.ts` — are not opinions. They're practices that teams discover after breaking production clients at least once. Starting with versioning is significantly cheaper than retrofitting it later. - -**Transferable skills:** - -* **URL-based API versioning** — Applicable to any ASP.NET Core REST API, regardless of authentication or architecture -* **`Asp.Versioning.Http` package** — Useful for any .NET API that needs to support multiple client versions simultaneously -* **Angular environment URL isolation** — Foundation for managing environment-specific API versions without code changes - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #dotnet #aspnetcore #webapi #apiversion #versioning #restapi #cleanarchitecture #csharp #fullstack #angular #swagger #openapi #backwardscompatibility #microservices diff --git a/blogs/series-2-dotnet-api/2.4-dotnet-swagger-jwt.md b/blogs/series-2-dotnet-api/2.4-dotnet-swagger-jwt.md deleted file mode 100644 index 8691700..0000000 --- a/blogs/series-2-dotnet-api/2.4-dotnet-swagger-jwt.md +++ /dev/null @@ -1,389 +0,0 @@ -# Test Your Secured .NET API Without Writing a Single Line of Frontend Code - -## Configuring Swagger to Accept Bearer Tokens for Interactive API Exploration - -You've built a .NET API with JWT authentication, role-based policies, and clean architecture. Now you want to test it. The old approach: spin up the Angular app, log in, open browser DevTools, copy the access token, paste it into a `curl` command, and run your test. That's four manual steps before you've tested anything. - -The better approach: open Swagger UI, click **Authorize**, paste the token once, and call any endpoint directly in the browser — with full request/response details, no terminal required. - -This article walks through the complete Swagger setup in the **TalentManagement API** — how NSwag is configured, how the Bearer security scheme works, how `[Authorize]` attributes appear as lock icons in the UI, and a step-by-step walkthrough for testing a secured `DELETE` endpoint. - -![Swagger Positions Endpoint](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-positions-resource-expanded.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Swagger is configured to test secured .NET API endpoints with Bearer tokens.** - ---- - -## 📚 What You'll Learn - -* Why NSwag instead of Swashbuckle — and what changes -* The `AddOpenApiDocument()` configuration that wires up Bearer authentication in Swagger -* How `AspNetCoreOperationSecurityScopeProcessor` automatically adds lock icons to protected endpoints -* How `UseOpenApi()` and `UseSwaggerUi()` serve the JSON spec and interactive UI -* Step-by-step: get a JWT token and test a secured endpoint in Swagger UI -* How `[ProducesResponseType]` attributes improve the generated documentation - ---- - -## 🔧 NSwag vs Swashbuckle - -ASP.NET Core ships with built-in support for OpenAPI documents, and most projects choose between two libraries to power the Swagger UI: - -**Swashbuckle** — the classic choice, deeply integrated with ASP.NET Core's `AddSwaggerGen()`. Most tutorials use it. - -**NSwag** — generates both the OpenAPI spec and strongly-typed C# / TypeScript client code from it. The TalentManagement API uses NSwag because the same spec that powers the Swagger UI can generate Angular HTTP clients automatically. - -The key difference in code: - -``` -Swashbuckle: services.AddSwaggerGen() → app.UseSwagger() → app.UseSwaggerUI() -NSwag: services.AddOpenApiDocument() → app.UseOpenApi() → app.UseSwaggerUi() -``` - -Everything else — Bearer token setup, lock icons, `[Authorize]` integration — works the same way conceptually. - ---- - -## ⚙️ AddSwaggerExtension: Registering the Document - -Swagger is registered in `ServiceExtensions.cs`: - -```csharp -public static void AddSwaggerExtension(this IServiceCollection services) -{ - services.AddOpenApiDocument(config => - { - config.DocumentName = "v1"; - config.Version = "v1"; - config.Title = "Clean Architecture - TalentManagementAPI.WebApi"; - config.Description = - "This Api will be responsible for overall data distribution and authorization."; - - config.PostProcess = document => - { - document.Info.Contact = new OpenApiContact - { - Name = "Jane Doe", - Email = "jdoe@janedoe.com", - Url = "https://janedoe.com/contact", - }; - }; - - // Register the Bearer security scheme - config.AddSecurity("Bearer", new OpenApiSecurityScheme - { - Type = OpenApiSecuritySchemeType.Http, - Scheme = "Bearer", - BearerFormat = "JWT", - In = OpenApiSecurityApiKeyLocation.Header, - Name = "Authorization", - Description = - "Input your Bearer token in this format - " + - "Bearer {your token here} to access this API", - }); - - // Auto-apply the Bearer requirement to endpoints that need it - config.OperationProcessors.Add( - new AspNetCoreOperationSecurityScopeProcessor("Bearer")); - }); -} -``` - -### What Each Part Does - -**`DocumentName = "v1"`** — the identifier for this OpenAPI document. The `{documentName}` placeholder in the route templates below resolves to `v1`: - -``` -/swagger/v1/swagger.json ← the raw JSON spec -/swagger ← the interactive UI -``` - -**`PostProcess`** — a callback that runs after the document is generated. Used here to add contact information to the `info` block of the OpenAPI spec. Useful for adding a license, terms-of-service URL, or any metadata that doesn't fit the main configuration. - -**`AddSecurity("Bearer", ...)`** — registers the JWT Bearer security scheme with the OpenAPI spec. This is what makes the **Authorize** button appear in the Swagger UI. Breaking down each property: - -``` -Type = OpenApiSecuritySchemeType.Http - → The scheme is HTTP authentication (as opposed to apiKey or oauth2) - -Scheme = "Bearer" - → The HTTP authentication scheme name (used in the Authorization header) - -BearerFormat = "JWT" - → Informational only — tells the UI to display "JWT" as the token format hint - -In = OpenApiSecurityApiKeyLocation.Header - → The token is sent in the request header (not a query param or cookie) - -Name = "Authorization" - → The header name where the token is placed -``` - -**`AspNetCoreOperationSecurityScopeProcessor("Bearer")`** — this is the key piece that connects `[Authorize]` attributes on controllers to the Bearer scheme in Swagger. It inspects each endpoint at document-generation time: - -* Endpoints with `[Authorize]` → get a lock icon 🔒 and include Bearer in their security requirements -* Endpoints with `[AllowAnonymous]` → get no lock icon, no Bearer requirement - -Without this processor, you'd have the Authorize button but no endpoints would actually require auth in the UI. - ---- - -## 🚀 UseSwaggerExtension: Serving the UI - -The middleware pipeline in `AppExtensions.cs` serves two things: - -```csharp -public static void UseSwaggerExtension(this IApplicationBuilder app) -{ - // Serve the raw OpenAPI JSON spec - app.UseOpenApi(settings => - { - settings.Path = "/swagger/{documentName}/swagger.json"; - }); - - // Serve the interactive Swagger UI - app.UseSwaggerUi(settings => - { - settings.Path = "/swagger"; - settings.DocumentPath = "/swagger/{documentName}/swagger.json"; - }); -} -``` - -**`UseOpenApi`** serves the machine-readable JSON document at: - -``` -https://localhost:44378/swagger/v1/swagger.json -``` - -This is the raw OpenAPI 3.0 spec — useful for generating client code with NSwag CLI or importing into Postman. - -**`UseSwaggerUi`** serves the interactive HTML/JS UI at: - -``` -https://localhost:44378/swagger -``` - -The UI loads the JSON spec from `DocumentPath` and renders it as the familiar expandable endpoint explorer. - -### Middleware Order Matters - -In `Program.cs`, the middleware is registered in this specific order: - -```csharp -app.UseRouting(); -app.UseCors("AllowAll"); -app.UseAuthentication(); // ← must come before UseAuthorization -app.UseAuthorization(); // ← must come before UseSwaggerExtension -app.UseSwaggerExtension(); // ← Swagger after auth middleware -app.MapControllers(); -``` - -**Why this order?** When Swagger UI sends a request to a protected endpoint, `UseAuthentication()` reads the `Authorization` header and populates `HttpContext.User`. Then `UseAuthorization()` evaluates the role policies. If Swagger came before auth middleware, the protected endpoints would return `401` even with a valid token pasted into the UI. - ---- - -## 🔒 Lock Icons: What You See in the UI - -The `AspNetCoreOperationSecurityScopeProcessor` maps `[Authorize]` attributes to lock icons: - -``` -[AllowAnonymous] → 🔓 No lock — open padlock icon, no auth required -[Authorize] → 🔒 Lock — any authenticated user -[Authorize(Policy = "AdminPolicy")] → 🔒 Lock — HRAdmin role required -``` - -The Employees resource in the UI looks like this: - -``` -GET /api/v1/employees 🔓 Get all employees (public) -GET /api/v1/employees/{id} 🔓 Get employee by ID (public) -POST /api/v1/employees 🔒 Create employee (authenticated) -PUT /api/v1/employees/{id} 🔒 Update employee (authenticated) -DELETE /api/v1/employees/{id} 🔒 Delete employee (AdminPolicy) -``` - -The lock is visual only — the actual enforcement happens in the authorization middleware. But it immediately tells any developer which endpoints require authentication before they try to call them. - ---- - -## 🔑 Step-by-Step: Testing a Secured Endpoint - -### Step 1: Get an Access Token - -The easiest way is to let Angular do the OIDC flow and then copy the token from the browser: - -1. Start all three services (IdentityServer, API, Angular) -2. Open the Angular app at `http://localhost:4200` -3. Log in with an HRAdmin user (`ashtyn1` / `Pa$$word123`) -4. Open browser DevTools → **Application** → **Session Storage** → `http://localhost:4200` -5. Find the key containing `access_token` — copy the value - -The token is a long base64 string starting with `eyJ...`. - -Alternatively, get a token directly from IdentityServer's token endpoint without any Angular involvement: - -```bash -curl -X POST https://localhost:44310/connect/token \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=password" \ - -d "client_id=TalentManagement" \ - -d "client_secret=your_secret" \ - -d "username=ashtyn1" \ - -d "password=Pa$$word123" \ - -d "scope=openid profile roles app.api.talentmanagement.read app.api.talentmanagement.write" \ - -k -``` - -> **⚠️ Password grant not enabled by default.** The seed data file (`identityserverdata.json`) does not include `password` in the `TalentManagement` client's allowed grant types, so this curl command will return `unauthorized_client` out of the box. To enable it, open the IdentityServer Admin UI (`https://localhost:44303`), navigate to **Clients → TalentManagement → Basics → Allowed Grant Types**, and add `password`. This change is not persisted to the seed file — it must be re-added if the database is recreated. -> -> **Before enabling password grant, consider:** -> * **Add `offline_access` to AllowedScopes** if you also need refresh tokens — the password flow requires it explicitly. -> * **Password grant (Resource Owner Password Credentials) is generally discouraged for new applications.** It requires the client to handle user credentials directly, which eliminates the security isolation that OAuth 2.0 is designed to provide. -> * **Avoid mixing `authorization_code` and `password` on the same client.** The `TalentManagement` client is a browser-based SPA using `authorization_code` + PKCE. Adding `password` to the same client mixes trust levels. The better design is a separate machine/trusted client dedicated to programmatic token acquisition. - -The response contains `"access_token": "eyJ..."`. - -### Step 2: Open Swagger UI - -Navigate to `https://localhost:44378/swagger`. - -You'll see all endpoints grouped by resource (Employees, Departments, Positions, etc.), each showing the HTTP method, route, and summary from the XML doc comments. - -### Step 3: Authorize - -Click the **Authorize** button (top right, next to the lock icon). - -A dialog appears with a single input field labelled **Bearer (http, Bearer)**: - -``` -Value: _________________________ - -Important: Paste ONLY the token value. -NSwag adds "Bearer " automatically. -``` - -**Critical detail:** Because `Type = OpenApiSecuritySchemeType.Http` and `Scheme = "Bearer"` are set, NSwag knows this is an HTTP Bearer scheme. It prepends `Bearer ` to your token automatically when sending requests. If you paste `Bearer eyJ...` instead of just `eyJ...`, the API receives `Bearer Bearer eyJ...` and returns `401`. - -Paste the raw token, click **Authorize**, click **Close**. - -### Step 4: Test a Locked Endpoint - -Expand **DELETE /api/v1/employees/{id}**. The lock icon is now closed (gold), indicating you're authorized. - -1. Click **Try it out** -2. Enter an employee ID (copy one from a `GET /api/v1/employees` response) -3. Click **Execute** - -The UI sends: - -``` -DELETE https://localhost:44378/api/v1/employees/abc123-guid -Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... -``` - -If the token has the `HRAdmin` role claim, you get `200 OK`. If you're logged in as an Employee or Manager, you get `403 Forbidden` — the Swagger UI shows the response body and headers so you can see exactly what the API returned. - -### Step 5: Test a Public Endpoint Without Auth - -Expand **GET /api/v1/employees**. The lock is open. Click **Try it out** → **Execute**. - -This works immediately — no token required. The Swagger UI doesn't send the `Authorization` header for `[AllowAnonymous]` endpoints even when you're authorized in the UI. - ---- - -## 📝 ProducesResponseType: Better API Docs - -Controllers use `[ProducesResponseType]` to declare exactly which HTTP status codes each endpoint can return. These appear as response examples in the Swagger UI: - -```csharp -[HttpPost] -[Authorize] -[ProducesResponseType(StatusCodes.Status201Created)] -[ProducesResponseType(StatusCodes.Status400BadRequest)] -public async Task Post(CreateEmployeeCommand command) -{ - var result = await Mediator.Send(command); - return CreatedAtAction(nameof(Get), new { id = result.Value }, result); -} -``` - -In the Swagger UI, the POST endpoint shows documented responses for both `201 Created` and `400 Bad Request` — including the response schema for each. This tells API consumers what to expect before they've written a single line of client code. - -Without `[ProducesResponseType]`, the UI only shows a generic `200 OK` response, which doesn't reflect what the endpoint actually returns. - ---- - -## 🌐 The OpenAPI JSON Spec - -The raw spec at `https://localhost:44378/swagger/v1/swagger.json` is a machine-readable description of every endpoint. You can use it to: - -**Generate Angular HTTP clients** with the NSwag CLI: - -```bash -npx nswag openapi2tsclient \ - /input:https://localhost:44378/swagger/v1/swagger.json \ - /output:src/app/api/api-client.ts \ - /template:Angular -``` - -This generates a typed Angular service with strongly-typed request/response models — matching every endpoint, query parameter, and request body exactly. - -**Import into Postman:** - -1. Open Postman → Import -2. Paste `https://localhost:44378/swagger/v1/swagger.json` -3. Postman creates a collection with every endpoint pre-configured - -**Import into Insomnia, HTTPie, or any OpenAPI-compatible tool** — the spec is vendor-neutral. - ---- - -## 🎯 Key Design Decisions - -**NSwag over Swashbuckle** — The OpenAPI JSON spec can generate typed TypeScript clients for Angular, eliminating hand-written HTTP service code. If client generation isn't needed, Swashbuckle is simpler. - -**`AspNetCoreOperationSecurityScopeProcessor`** — This processor reads `[Authorize]` attributes at document-generation time, not at runtime. The lock icons in Swagger UI are purely visual documentation; the actual security is enforced by the auth middleware regardless of what Swagger shows. - -**`Type = OpenApiSecuritySchemeType.Http`** — Using the `Http` type means the UI only asks for the raw token value (not `Bearer `). This is less error-prone than `ApiKey` type, where developers must remember to include the `Bearer ` prefix themselves. - -**`DocumentName = "v1"`** — The document name matches the API version. If a v2 is added, a second `AddOpenApiDocument()` call with `DocumentName = "v2"` creates a separate Swagger document, separate Authorize button, and separate UI dropdown — without touching the v1 configuration. - ---- - -## 🌟 Why This Matters - -Swagger UI with Bearer authentication turns a secured API from a black box into an interactive development tool. Instead of writing test scripts or spinning up a frontend just to verify an endpoint, any developer on the team can open the UI, paste a token, and explore the API immediately. This dramatically shortens the feedback loop during API development. - -The NSwag setup here — `AddOpenApiDocument`, `AspNetCoreOperationSecurityScopeProcessor`, `UseSwaggerUi` — applies to any ASP.NET Core API. And the OpenAPI JSON spec it generates is vendor-neutral: Postman, Insomnia, HTTPie, and code generators all consume it. The spec outlives any single tool choice. - -**Transferable skills:** - -* **NSwag Bearer configuration** — Applicable to any ASP.NET Core API with JWT authentication -* **`AspNetCoreOperationSecurityScopeProcessor`** — Useful for any project where Swagger lock icons need to reflect `[Authorize]` attributes automatically -* **OpenAPI spec as a source of truth** — Foundation for generating typed client code, Postman collections, and API contracts for any REST API - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #dotnet #swagger #openapi #nswag #aspnetcore #webapi #jwt #authentication #apidocumentation #restapi #cleanarchitecture #csharp #fullstack #angular #devtools diff --git a/blogs/series-2-dotnet-api/2.5-dotnet-easycaching.md b/blogs/series-2-dotnet-api/2.5-dotnet-easycaching.md deleted file mode 100644 index da326a7..0000000 --- a/blogs/series-2-dotnet-api/2.5-dotnet-easycaching.md +++ /dev/null @@ -1,284 +0,0 @@ -# Speed Up Your Dashboard: Easy Response Caching in .NET 10 With EasyCaching - -## How In-Memory Caching, Domain Event Invalidation, and Diagnostic Headers Transform API Performance - -Your dashboard aggregates data from multiple tables on every load. Your employee list re-queries the database for every page view, even when nothing changed in the last five minutes. These patterns work fine with ten users — and collapse under real load. **EasyCaching** solves this with a clean, configuration-driven caching layer that plugs directly into your .NET Clean Architecture. - -![Application Dashboard](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/application-dashboard-anonymous.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article shows how the API layer uses EasyCaching to cache dashboard metrics and list queries — with automatic invalidation via domain events.** - ---- - -## 🎓 What You'll Learn - -* **EasyCaching setup** — Register Memory and Redis providers through `appsettings.json` with zero code changes to switch between them -* **MediatR pipeline caching** — Add caching to any query handler without modifying business logic -* **Domain event invalidation** — How `CacheInvalidationEventHandler` clears stale cache automatically when data changes -* **Diagnostic headers** — The `X-Cache-Status: HIT/MISS` pattern for verifying cache behavior in development -* **Admin cache endpoints** — Manual invalidation and hit/miss statistics via `CacheController` - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 2.1 read** — Clean Architecture layers (Domain, Application, Infrastructure, WebApi) -* **.NET SDK 10.0+** — [Download](https://dotnet.microsoft.com/download) -* **Basic MediatR knowledge** — Queries, handlers, and pipeline behaviors - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem - -Every time a user loads the dashboard, the API runs an aggregation query across employees, positions, departments, and salary ranges. Every time the employee list page loads, the API runs a paginated query with filters. These queries run identically for every user, on every request, even when the underlying data hasn't changed in hours. - -**Common pain points without caching:** - -* **Dashboard response time degrades** as the database grows — aggregations that take 10ms with 100 rows take 500ms with 100,000 -* **Repeated identical queries** — 50 concurrent users viewing the dashboard = 50 identical aggregation queries hitting the database simultaneously -* **No visibility** — developers can't tell whether a slow response is a cache miss or a slow query without adding logging -* **All-or-nothing invalidation** — without event-driven invalidation, you either cache too aggressively (stale data) or not at all (no benefit) - ---- - -## 💡 The Solution - -**EasyCaching** is a .NET caching library that abstracts over `IMemoryCache` and distributed providers (Redis, Memcached). The tutorial API uses it with three design decisions: - -* ✅ **Configuration-driven** — all cache keys, TTLs, and provider settings live in `appsettings.json`; no magic strings in code -* ✅ **MediatR pipeline integration** — caching wraps the query handler transparently; the handler itself has no cache knowledge -* ✅ **Event-driven invalidation** — domain events trigger `CacheInvalidationEventHandler` automatically on any write -* ✅ **Diagnostic headers** — `X-Cache-Status: HIT` or `MISS` on every cached response, visible in browser DevTools -* ✅ **Admin endpoints** — `POST /api/v1/cache/invalidate` and `GET /api/v1/cache/stats` for operational visibility - ---- - -## 🚀 How It Works - -### Step 1: Configuration — Everything in appsettings.json - -All cache behavior is controlled from `appsettings.json`, with no cache provider code needed at the feature layer: - -```json -"Caching": { - "Enabled": true, - "Provider": "Memory", - "KeyPrefix": "MyOnion", - "ProviderSettings": { - "Memory": { "SizeLimitMB": 256 }, - "Distributed": { - "ConnectionString": "redis:6379", - "IndexKeyTtlSeconds": 600 - } - }, - "PerEndpoint": { - "Dashboard:Metrics": { "AbsoluteTtlSeconds": 300 }, - "Employees:GetAll": { "AbsoluteTtlSeconds": 300, "SlidingTtlSeconds": 120 }, - "Positions:GetAll": { "AbsoluteTtlSeconds": 180 } - }, - "Diagnostics": { - "EmitCacheStatusHeader": true, - "HeaderName": "X-Cache-Status", - "KeyHeaderName": "X-Cache-Key", - "DurationHeaderName": "X-Cache-Duration-Ms", - "KeyDisplayMode": "Hash" - } -} -``` - -**What this does:** Switching from in-memory to Redis is one config line change (`"Provider": "Distributed"`). Per-endpoint TTLs let you tune aggressiveness per use case — dashboard metrics (5 minutes) vs. positions list (3 minutes). - -Cache key constants live in `TalentManagementAPI.Application/Common/Caching/CacheKeyPrefixes.cs`: - -```csharp -public static class CacheKeyPrefixes -{ - public const string DashboardMetrics = "Dashboard:Metrics"; - public const string EmployeesAll = "Employees:GetAll"; - public const string PositionsAll = "Positions:GetAll"; -} -``` - -**Key point:** No string literals scattered across the codebase. All cache key references point to these constants. - -### Step 2: MediatR Pipeline Caching - -The cleanest place to add caching in a CQRS architecture is the **MediatR pipeline** — a behavior that wraps every query handler transparently. - -`GetEmployeesCachingDecorator.cs` intercepts `GetEmployeesQuery` before it reaches the database handler: - -```csharp -public class GetEmployeesCachingDecorator - : IPipelineBehavior - where TRequest : GetEmployeesQuery -{ - private readonly ICacheProvider _cache; - - public async Task Handle( - TRequest request, RequestHandlerDelegate next, CancellationToken ct) - { - // Build cache key from all query parameters - var cacheKey = BuildCacheKey(request); - - // Check cache first - var cached = await _cache.GetAsync(cacheKey); - if (cached != null) - return cached; - - // Cache miss — call the real handler - var result = await next(); - - // Store result with configured TTL - await _cache.SetAsync(cacheKey, result, CacheKeyPrefixes.EmployeesAll); - - return result; - } -} -``` - -**Key point:** The `GetEmployeesQueryHandler` has zero knowledge of caching. Pagination parameters, filters, sort order, and page size are all encoded into the cache key — so `page=1&size=10&search=Smith` and `page=2&size=10&search=Smith` are separate cache entries. - -For the dashboard, caching is applied directly in the handler since it's a single fixed key: - -```csharp -// GetDashboardMetricsQueryHandler -var cached = await _cache.GetAsync(CacheKeyPrefixes.DashboardMetrics); -if (cached != null) - return cached; - -var metrics = await _db.BuildDashboardMetrics(); -await _cache.SetAsync(CacheKeyPrefixes.DashboardMetrics, metrics, CacheKeyPrefixes.DashboardMetrics); -return metrics; -``` - -### Step 3: Event-Driven Invalidation - -Caching is only useful if stale data gets cleared. The API uses **domain events** — the same mechanism described in Article 2.1 — to trigger cache invalidation on any write. - -`CacheInvalidationEventHandler.cs` listens for all four entity change events: - -```csharp -public class CacheInvalidationEventHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler -{ - private readonly ICacheProvider _cache; - - public async Task Handle(EmployeeChangedEvent notification, CancellationToken ct) - { - // Clear employee list (all filter combinations) - await _cache.RemoveByPrefixAsync(CacheKeyPrefixes.EmployeesAll); - - // Clear dashboard (employee count/stats changed) - await _cache.RemoveAsync(CacheKeyPrefixes.DashboardMetrics); - } - - public async Task Handle(DepartmentChangedEvent notification, CancellationToken ct) - { - // Only dashboard is affected by department changes - await _cache.RemoveAsync(CacheKeyPrefixes.DashboardMetrics); - } - // ...same pattern for Position and SalaryRange events -} -``` - -**Key point:** `RemoveByPrefixAsync` clears ALL `Employees:GetAll:*` entries at once — regardless of pagination parameters or filter combinations. No need to enumerate which cache keys exist. - -**The full write flow:** - -* User creates an employee via Angular UI → `POST /api/v1/employees` -* `CreateEmployeeCommandHandler` saves to database -* `EmployeeChangedEvent` fires via MediatR -* `CacheInvalidationEventHandler` clears `Employees:GetAll:*` and `Dashboard:Metrics` -* Next dashboard load or employee list load returns fresh data - ---- - -## 💻 Try It Yourself - -**Start all three services** — See [Quick Start Setup](../QUICKSTART.md) for startup commands, application URLs, and test credentials. - -**Verify caching in browser DevTools:** - -![Swagger Cache Resource Expanded](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-employee-resource-expanded.png) - -1. Open `https://localhost:44378/swagger` and authenticate with your Bearer token -2. Call `GET /api/v1/employees` — inspect response headers in the Network tab -3. Look for `X-Cache-Status: MISS` on the first call -4. Call `GET /api/v1/employees` again — `X-Cache-Status: HIT` and `X-Cache-Duration-Ms` shows remaining TTL -5. Create or update an employee via `POST /api/v1/employees` -6. Call `GET /api/v1/employees` again — `X-Cache-Status: MISS` (cache was invalidated by the write) - -**Admin endpoints (HRAdmin role required):** - -```bash -# Manual cache clear -POST https://localhost:44378/api/v1/cache/invalidate -Authorization: Bearer {hrAdminToken} - -# View hit/miss statistics -GET https://localhost:44378/api/v1/cache/stats -Authorization: Bearer {hrAdminToken} -``` - ---- - -## 📊 Real-World Impact - -**Before caching:** - -* ❌ Every dashboard load runs a full aggregation query across all tables -* ❌ 50 concurrent users = 50 simultaneous identical database queries -* ❌ No visibility — can't tell if slowness is query performance or load -* ❌ Response time degrades linearly as data grows - -**After EasyCaching:** - -* ✅ Dashboard metrics served from memory in < 1ms after first load (5-minute cache window) -* ✅ 50 concurrent users = 1 database query + 49 cache hits -* ✅ `X-Cache-Status: HIT/MISS` in every response header — instant feedback during development -* ✅ Response time stays constant regardless of database size during the cache window - ---- - -## 🌟 Why This Matters - -The EasyCaching integration demonstrates a principle that scales beyond this tutorial: **caching should be infrastructure, not business logic.** The `GetEmployeesQueryHandler` doesn't know or care that its results are cached. The `CreateEmployeeCommandHandler` doesn't know or care that it triggers cache invalidation. These concerns are handled at the pipeline and event layers — exactly where they belong in Clean Architecture. - -**Transferable skills:** - -* **MediatR pipeline behaviors** — The same pattern works for logging, validation, and authorization — not just caching -* **Event-driven cache invalidation** — Applicable to any CQRS architecture where write and read paths are separate -* **Configuration-driven providers** — The Memory ↔ Redis swap pattern works for any EasyCaching-backed service -* **Diagnostic headers** — The `X-Cache-Status` pattern is a lightweight alternative to distributed tracing for cache debugging - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #dotnet #csharp #caching #easycaching #cleanarchitecture #mediator #mediatR #webapi #performance #redis #inmemorycache #domaineventss #cqrs #aspnetcore #fullstack #tutorial diff --git a/blogs/series-3-angular-material/3.1-angular-material-datatable.md b/blogs/series-3-angular-material/3.1-angular-material-datatable.md deleted file mode 100644 index 9c33f81..0000000 --- a/blogs/series-3-angular-material/3.1-angular-material-datatable.md +++ /dev/null @@ -1,608 +0,0 @@ -# Build a Production-Ready Data Table in Angular Material: Sort, Filter, Page - -## From Zero to a Fully Functional Employee List with MatTable, MatPaginator, and Server-Side Filtering - -Angular Material's `mat-table` is one of those components that looks simple in a tutorial — and then turns complicated fast when real requirements arrive: server-side pagination, multi-field filtering, autocomplete suggestions, loading spinners, empty states, and role-based action buttons all in one component. - -This article builds the complete Employee List from the **TalentManagement** app — a production-style data table that handles 1,000+ employees without loading them all at once. Every line of code comes from the real application. - -![Employee List Page](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-list-page.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Angular Material's data table is built for production use with server-side pagination and filtering.** - ---- - -## 📚 What You'll Learn - -* How `mat-table` binds to a data array with `[dataSource]` -* Defining columns with `matColumnDef`, `mat-header-cell`, and `mat-cell` -* Server-side pagination with `MatPaginator` and the `PagedResponse` API contract -* Converting `MatPaginator`'s 0-based page index to the API's 1-based `pageNumber` -* Auto-submitting filters with `debounceTime` on `FormGroup.valueChanges` -* Autocomplete suggestions powered by the same search API — with a minimum character threshold -* `MatProgressSpinner` for loading state and a "no results" empty row -* Role-based action buttons using `*appHasRole` (View/Edit/Delete per role) - ---- - -## 🏗️ The Component at a Glance - -The `EmployeeListComponent` is a standalone Angular component with these responsibilities: - -``` -EmployeeListComponent -│ -├── Search Form (ReactiveFormsModule) -│ ├── 5 filter fields: EmployeeNumber, FirstName, LastName, Email, PositionTitle -│ ├── Autocomplete dropdown per field (live API query, min 2 chars) -│ └── Auto-submit on value change (500ms debounce) -│ -├── Data Table (mat-table) -│ ├── 6 columns: Employee #, Name, Email, Phone, Position, Actions -│ ├── Loading spinner (mat-spinner) -│ └── Empty state row -│ -├── Paginator (mat-paginator) -│ ├── Page sizes: [5, 10, 25, 50, 100] -│ └── Total count from API (server-side) -│ -└── Role-Based Actions - ├── View: all users - ├── Edit: HRAdmin, Manager - └── Delete: HRAdmin only -``` - -The component uses server-side filtering and pagination — every filter change and page turn fires a new API request. No data is held in memory beyond the current page. - ---- - -## 📋 Building the Table: mat-table Basics - -### The Minimal Setup - -The `mat-table` directive turns a plain HTML `` into a Material data table. It binds to a data array via `[dataSource]`: - -```html -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Employee #{{ employee.employeeNumber }}Name{{ getFullName(employee) }}Actions -
- - - -
-
-
- info -

No employees found

-
-
-``` - -### How Column Definitions Work - -Each column follows the same three-part pattern: - -``` - ← 1. Name (must match displayedColumns entry) - ← 2. Header cell template - ← 3. Data cell template with row context - -``` - -The `displayedColumns` array in the component controls which columns appear and in what order: - -```typescript -displayedColumns: string[] = [ - 'employeeNumber', - 'name', - 'email', - 'phone', - 'positionTitle', - 'actions', -]; -``` - -Reordering columns means reordering this array — no template changes required. - -### Computed Columns - -The `name` column doesn't map to a single property. It calls a component method that assembles the display value: - -```typescript -getFullName(employee: Employee): string { - const parts = [ - employee.prefix, // "Mr.", "Dr.", etc. - employee.firstName, - employee.middleName, - employee.lastName, - ].filter(Boolean); // removes null/undefined/empty strings - return parts.join(' '); -} -``` - -This pattern — a column whose content is computed rather than a direct property — works identically. The `*matCellDef="let employee"` context variable is just the row object; you can call any method or pipe on it. - -### Column Width with CSS - -Material table columns get CSS classes automatically based on their `matColumnDef` value: `.mat-column-employeeNumber`, `.mat-column-name`, etc. Use these to set fixed widths: - -```scss -.mat-column-employeeNumber { width: 120px; } -.mat-column-name { min-width: 200px; } -.mat-column-email { min-width: 200px; } -.mat-column-phone { width: 150px; } -.mat-column-positionTitle { min-width: 200px; } -.mat-column-actions { width: 120px; } -``` - ---- - -## 📄 Server-Side Pagination with MatPaginator - -### The API Contract - -The API returns a `PagedResponse` on every list request: - -```typescript -export interface PagedResponse { - value: T[]; // the current page of data - pageNumber: number; // 1-based current page - pageSize: number; - recordsFiltered: number; // filtered count (with active search) - recordsTotal: number; // total count in database - isSuccess: boolean; - errors: string[]; - executionTimeMs: number; -} -``` - -The `totalCount` from `recordsTotal` feeds `mat-paginator`'s `[length]` input — this is what the paginator uses to calculate how many page buttons to show. - -### Wiring Up the Paginator - -In the template, `mat-paginator` is wired to the component state and fires a `(page)` event on every navigation: - -```html - - -``` - -**`[length]="totalCount"`** — the total number of records (not the current page count). Without this, the paginator doesn't know how many pages exist. - -**`[pageIndex]="pageNumber - 1"`** — `MatPaginator` is 0-based (`0 = first page`), but the API expects 1-based page numbers. The subtraction converts between them. - -**`showFirstLastButtons`** — adds skip-to-first and skip-to-last navigation buttons. - -### Handling Page Changes - -```typescript -pageSize = 10; -pageNumber = 1; // 1-based for the API - -onPageChange(event: PageEvent): void { - this.pageSize = event.pageSize; - this.pageNumber = event.pageIndex + 1; // 0-based → 1-based - this.loadEmployees(); -} -``` - -`PageEvent` carries three properties: `pageIndex` (0-based), `pageSize`, and `length`. The `+ 1` is the only translation needed between Angular Material's 0-based convention and the API's 1-based convention. - -### Loading Data - -```typescript -loadEmployees(): void { - this.loading = true; - - const params = { - PageNumber: this.pageNumber, - PageSize: this.pageSize, - ...this.searchForm.value, // spread active filter values - }; - - // Remove empty filter values so the API doesn't filter on blank strings - Object.keys(params).forEach(key => { - if (params[key] === '' || params[key] === null || params[key] === undefined) { - delete params[key]; - } - }); - - this.employeeService.getAllPaged(params).subscribe({ - next: response => { - this.employees = response.value; - this.totalCount = response.recordsTotal; - this.loading = false; - }, - error: error => { - console.error('Error loading employees:', error); - this.loading = false; - }, - }); -} -``` - -The `...this.searchForm.value` spread includes all active filter values in the same `params` object as pagination. `BaseApiService.buildHttpParams()` converts this flat object to `HttpParams`: - -```typescript -protected buildHttpParams(params?: QueryParams): HttpParams { - let httpParams = new HttpParams(); - if (params) { - Object.keys(params).forEach(key => { - const value = params[key]; - if (value !== null && value !== undefined) { - httpParams = httpParams.set(key, value.toString()); - } - }); - } - return httpParams; -} -``` - -The resulting HTTP request looks like: - -``` -GET /api/v1/employees?PageNumber=1&PageSize=10&LastName=smith -``` - ---- - -## 🔍 Server-Side Filtering with Reactive Forms - -### The Form Structure - -Five filter fields share a single `FormGroup`: - -```typescript -initSearchForm(): void { - this.searchForm = this.fb.group({ - FirstName: [''], - LastName: [''], - Email: [''], - EmployeeNumber: [''], - PositionTitle: [''], - }); -} -``` - -### Auto-Submit on Value Change - -Instead of a "Search" button, the form submits automatically whenever any field changes — with a 500ms debounce to avoid firing on every keystroke: - -```typescript -setupAutoSubmit(): void { - this.searchForm.valueChanges - .pipe( - debounceTime(500), - distinctUntilChanged( - (prev, curr) => JSON.stringify(prev) === JSON.stringify(curr) - ), - takeUntil(this.destroy$) - ) - .subscribe(() => { - this.pageNumber = 1; // reset to page 1 when filter changes - this.loadEmployees(); - }); -} -``` - -**`debounceTime(500)`** — waits 500ms after the last keystroke before firing. Prevents an API call for every character typed. - -**`distinctUntilChanged`** — skips the emission if the form value hasn't actually changed (e.g., clicking into a field and clicking out without typing). The JSON comparison handles the nested object comparison. - -**`takeUntil(this.destroy$)`** — unsubscribes when the component is destroyed, preventing memory leaks. The `destroy$` subject is completed in `ngOnDestroy()`. - -**`this.pageNumber = 1`** — always resets to the first page when filters change. Without this, a user filtering to find "Smith" on page 3 would see page 3 of the Smith results (which might be empty) instead of page 1. - -### The Clear Button - -```typescript -onClearSearch(): void { - this.searchForm.reset(); // sets all fields to null - this.pageNumber = 1; - if (this.paginator) { - this.paginator.pageIndex = 0; // sync the paginator UI - } - this.loadEmployees(); -} -``` - -`searchForm.reset()` triggers `valueChanges`, which triggers `setupAutoSubmit()`, which calls `loadEmployees()`. But `onClearSearch()` also calls `loadEmployees()` directly — so the data reloads twice. The `distinctUntilChanged` guard minimizes this, and the double-load is harmless since both requests return the same result. - ---- - -## ✨ Autocomplete: Live Search Suggestions - -![Employee Search Filtering UI](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-search-filtering-ui.png) - -Each filter field has an autocomplete dropdown populated by a live API query. The same `getAllPaged()` endpoint used for the table also powers the suggestions: - -### Setting Up Autocomplete Observables - -```typescript -setupAutocomplete(): void { - this.filteredLastNames$ = this.searchForm.get('LastName')!.valueChanges.pipe( - startWith(''), - debounceTime(300), // shorter debounce than auto-submit - distinctUntilChanged(), - switchMap(value => this.getAutocompleteOptions('LastName', value)) - ); - - // Same pattern for FirstName, Email, EmployeeNumber, PositionTitle... -} -``` - -**`debounceTime(300)`** — 300ms for autocomplete (faster feedback) vs 500ms for auto-submit (avoid excessive table reloads). - -**`switchMap`** — cancels the previous API request if the user keeps typing. Without `switchMap`, fast typists would see stale suggestions from earlier requests arriving after newer ones. - -### Fetching Suggestions - -```typescript -getAutocompleteOptions( - field: string, - value: string -): Observable { - if (!value || value.length < 2) { - return of([]); // minimum 2 characters before querying - } - - const params: any = { - PageNumber: 1, - PageSize: 10, - [field]: value, // dynamic field name as computed property key - }; - - return this.employeeService.getAllPaged(params).pipe( - map(response => { - const fieldMap: { [key: string]: (emp: Employee) => string } = { - 'EmployeeNumber': emp => emp.employeeNumber, - 'FirstName': emp => emp.firstName, - 'LastName': emp => emp.lastName, - 'Email': emp => emp.email, - 'PositionTitle': emp => emp.positionTitle || '', - }; - - return response.value - .map(emp => fieldMap[field](emp)) - .filter((v, i, arr) => v && arr.indexOf(v) === i); // unique, non-empty - }), - catchError(() => of([])) - ); -} -``` - -**`value.length < 2`** — requires at least 2 characters. Autocomplete on 0 or 1 characters returns too many results to be useful and puts unnecessary load on the API. - -**`[field]: value`** — computed property key syntax sends `{ LastName: "sm" }` to the API. The same endpoint that filters the table filters the autocomplete suggestions. - -**Unique values** — `.filter((v, i, arr) => v && arr.indexOf(v) === i)` removes duplicates. Multiple employees can share the same last name; autocomplete should show each unique value once. - -### The Autocomplete Template - -```html - - Last Name - - - - {{ option }} - - - -``` - -The `| async` pipe subscribes to `filteredLastNames$` and unsubscribes automatically when the template is destroyed — no manual subscription management needed. - ---- - -## ⏳ Loading State and Empty State - -### Loading Spinner - -A `mat-spinner` covers the table area while data is fetching: - -```html -
-
- -
- - - -
-
-``` - -The `*ngIf="!loading"` on the `` ensures the table only renders once data is available, avoiding a flash of an empty table. - -### Empty State Row - -When the API returns zero results, the table shows a helpful message: - -```html - - - -``` - -`[attr.colspan]="displayedColumns.length"` spans all columns dynamically — if you add or remove a column, the empty row spans correctly without touching the template. - ---- - -## 🔒 Role-Based Action Buttons - -Each action button uses `*appHasRole` to conditionally render based on the current user's roles: - -```html - - - - - - - - -``` - -`*appHasRole` is a structural directive — it removes the element from the DOM entirely if the role check fails. An Employee user sees only the View button. A Manager sees View and Edit. An HRAdmin sees all three. - -The same check applies to the **Add Employee** button in the card header: - -```html - -``` - ---- - -## 🧩 Putting It All Together: Component Initialization - -```typescript -ngOnInit(): void { - this.initSearchForm(); // 1. Build the FormGroup - this.setupAutocomplete(); // 2. Wire autocomplete observables to form fields - this.setupAutoSubmit(); // 3. Subscribe to form changes → reload table - this.loadEmployees(); // 4. Initial load on page open -} -``` - -The order matters: autocomplete setup must come after `initSearchForm()` (observables subscribe to the form controls). Auto-submit setup after that (subscribes to `valueChanges`). The initial load happens last. - -### Cleanup with OnDestroy - -```typescript -private destroy$ = new Subject(); - -ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); -} -``` - -The `takeUntil(this.destroy$)` in `setupAutoSubmit()` uses this subject to unsubscribe cleanly when Angular destroys the component. Without this, the subscription would fire after navigation away from the employee list — causing API calls from a dead component. - ---- - -## 🎯 Key Design Decisions - -**Server-side pagination over client-side** — loading all 1,000+ employees to paginate in the browser defeats the purpose of pagination. Every page turn is a new API request returning only 10 (or 25, or 50) records. - -**Auto-submit over a Search button** — users expect filtering to be instant. A debounced `valueChanges` subscription gives the "search as you type" experience without flooding the API. - -**Reset to page 1 on filter change** — if you're on page 3 filtering by "Smith" and you add a second filter, there may not be a page 3 in the new results. Always resetting to page 1 prevents the paginator from showing a page that doesn't exist. - -**`switchMap` for autocomplete** — cancels in-flight requests when the user keeps typing. Prevents stale suggestions from earlier (slower) requests appearing after newer (faster) ones. - -**`*appHasRole` removes from DOM** — unlike `[hidden]` or `[style.display]`, the structural directive removes the element entirely. An Employee user cannot inspect the DOM and find a hidden Delete button. - -**Modular imports** — each Material module is explicitly imported in the `imports` array of the standalone component: `MatTableModule`, `MatPaginatorModule`, `MatFormFieldModule`, `MatAutocompleteModule`, etc. Tree-shaking ensures only the modules actually used are included in the bundle. - -## 🌟 Why This Matters - -Production data tables are rarely simple static lists. Server-side pagination, reactive filtering, and debounced auto-submit are the patterns that appear in every real-world Angular Material project with more than a few hundred rows. Building them correctly the first time — with `MatPaginator`, `MatSort`, and `switchMap`-based observables — is significantly easier than refactoring a client-side table after it starts timing out in production. - -The column visibility pattern tied to role claims (`*appHasRole`) transfers directly to any table where different users need different views of the same data. The `switchMap` cancellation behavior that prevents stale filter responses is a general RxJS pattern for any user-initiated search. - -**Transferable skills:** - -* **Server-side pagination with `MatPaginator`** — Applicable to any Angular Material data table with large datasets -* **Reactive filtering with `switchMap`** — Foundation for debounced search in any Angular feature -* **Role-based column visibility** — Pattern for showing/hiding table columns based on user permissions - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #angular #angularmaterial #mattable #datatable #pagination #rxjs #reactiveforms #autocomplete #typescript #fullstack #dotnet #materialdesign #frontend #spa #webdevelopment diff --git a/blogs/series-3-angular-material/3.2-angular-reactive-forms.md b/blogs/series-3-angular-material/3.2-angular-reactive-forms.md deleted file mode 100644 index d91ebe3..0000000 --- a/blogs/series-3-angular-material/3.2-angular-reactive-forms.md +++ /dev/null @@ -1,535 +0,0 @@ -# Reactive Forms Done Right: Validation Patterns Every Angular Developer Should Know - -## Required Fields, Email Validation, and Min Value Rules with Angular Material — One Form for Create and Edit - -Template-driven forms work fine for login boxes. Reactive forms are what you use when the stakes are higher: a form that creates records in a production database, needs per-field validation messages, must pre-populate for editing, and has to submit different API payloads depending on whether you're creating or updating. - -This article builds the **Employee Form** from the TalentManagement app — a single component that handles both "Create Employee" and "Edit Employee" modes, validates 12 fields with real business rules, loads dropdown options from the API, and gives users clear error messages at the right moment. - -![Employee Form](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-form.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Angular reactive forms handle validation, create mode, and edit mode in a single reusable component.** - ---- - -## 📚 What You'll Learn - -* How `FormBuilder` and `FormGroup` replace template-driven form boilerplate -* The four built-in validators used in this form: `required`, `email`, `maxLength`, `min` -* How `mat-error` with `hasError()` shows the right message for the right error -* Why `markAllAsTouched()` is the correct way to trigger validation on submit -* One component, two modes: detecting create vs edit via route params -* `patchValue()` for populating a form from an API response without resetting untouched fields -* Loading dropdown options (Departments, Positions) from the API into `mat-select` -* `mat-datepicker` for the Date of Birth field -* Disabling the submit button during API calls to prevent double-submits -* `MatSnackBar` for success and error feedback - ---- - -## 🏗️ Single Form, Two Modes - -The `EmployeeFormComponent` handles both creation and editing with one `FormGroup`. The mode is determined by whether the route contains an `id` parameter: - -``` -/employees/create → isEditMode = false → "Create Employee" -/employees/edit/:id → isEditMode = true → "Edit Employee" -``` - -```typescript -checkEditMode(): void { - this.employeeId = this.route.snapshot.paramMap.get('id') || undefined; - this.isEditMode = !!this.employeeId; - - if (this.isEditMode && this.employeeId) { - this.loadEmployee(this.employeeId); - } -} -``` - -`!!this.employeeId` converts the string-or-undefined to a boolean. If `id` is present, `loadEmployee()` fetches the employee from the API and populates the form. If not, the form stays empty and ready for input. - -The form title, submit button label, and API call all adapt to the mode: - -```typescript -getFormTitle(): string { - return this.isEditMode ? 'Edit Employee' : 'Create Employee'; -} -``` - -```html -{{ getFormTitle() }} - - -``` - ---- - -## 📋 Building the FormGroup - -`FormBuilder.group()` creates the `FormGroup` with initial values and validators in one declarative block: - -```typescript -initForm(): void { - this.employeeForm = this.fb.group({ - employeeNumber: ['', [Validators.required, Validators.maxLength(50)]], - prefix: ['', Validators.maxLength(10)], - firstName: ['', [Validators.required, Validators.maxLength(100)]], - middleName: ['', Validators.maxLength(100)], - lastName: ['', [Validators.required, Validators.maxLength(100)]], - birthday: [null, Validators.required], - gender: [Gender.Male, Validators.required], - email: ['', [Validators.required, Validators.email, Validators.maxLength(255)]], - phone: ['', [Validators.required, Validators.maxLength(20)]], - salary: [0, [Validators.required, Validators.min(0)]], - positionId: ['', Validators.required], - departmentId: ['', Validators.required], - }); -} -``` - -Each field follows the tuple format: `[initialValue, validators]`. When a field has multiple validators, pass them as an array. - -**A few notable defaults:** - -* `birthday: [null, ...]` — `null` instead of `''` because the date picker expects a null starting value, not an empty string -* `gender: [Gender.Male, ...]` — pre-selected to avoid a blank select on first load -* `salary: [0, ...]` — starts at zero to give the number input a meaningful default - -**Optional vs required:** - -Fields without `Validators.required` are optional by design: - -```typescript -prefix: ['', Validators.maxLength(10)], // optional -middleName: ['', Validators.maxLength(100)], // optional -``` - -The `maxLength` validator still runs on optional fields — an empty string passes `maxLength`, so no error is shown when the field is blank. But if a user types more than the limit, the error appears immediately. - ---- - -## ✅ Validators: Four Patterns - -### 1. Required - -```typescript -firstName: ['', [Validators.required, Validators.maxLength(100)]], -``` - -`Validators.required` fails on empty strings, null, and undefined. It sets the `required` error key on the control when invalid. - -### 2. Email Format - -```typescript -email: ['', [Validators.required, Validators.email, Validators.maxLength(255)]], -``` - -`Validators.email` uses a regex to check for a valid email format. It sets the `email` error key — separate from `required`. This matters for showing the right error message: "Email is required" vs "Please enter a valid email." - -### 3. Max Length - -```typescript -employeeNumber: ['', [Validators.required, Validators.maxLength(50)]], -``` - -`Validators.maxLength(n)` fails when `value.length > n`. It sets the `maxlength` error key with details about the actual and allowed length. - -### 4. Min Value - -```typescript -salary: [0, [Validators.required, Validators.min(0)]], -``` - -`Validators.min(0)` fails when the numeric value is less than 0. It sets the `min` error key. Useful for numeric inputs where negative values don't make business sense. - ---- - -## 🔴 Displaying Errors: mat-error and hasError() - -`mat-error` inside a `mat-form-field` only displays when the field is invalid **and touched** (the user has interacted with it). Angular Material handles this timing automatically — errors don't flash on every field the moment the form loads. - -### Single Error Per Field - -```html - - First Name - - - First name is required - - -``` - -`employeeForm.get('firstName')` returns the `AbstractControl` for that field. `?.hasError('required')` uses optional chaining to avoid null errors during form initialization, and checks for the specific error key. - -### Multiple Errors Per Field - -When a field has multiple validators, each possible error gets its own `mat-error` element: - -```html - - Email - - - - Email is required - - - Please enter a valid email - - -``` - -Angular Material shows all `mat-error` elements whose `*ngIf` is true simultaneously — but in practice, only one error is active at a time. An empty field fails `required` but not `email`. A field with `"notanemail"` fails `email` but not `required`. The `hasError()` check makes each message mutually exclusive. - -The salary field handles two numeric validators: - -```html - - Salary - - - - - Salary is required - - - Salary must be greater than 0 - - -``` - -`matTextPrefix` adds the `$` symbol inside the form field to the left of the input — a built-in Material feature requiring no custom CSS. - ---- - -## 📤 Submitting: markAllAsTouched() - -The form binds to `ngSubmit`: - -```html -
-``` - -The submit handler checks validity before calling the API: - -```typescript -onSubmit(): void { - if (this.employeeForm.invalid) { - this.employeeForm.markAllAsTouched(); - return; - } - - this.loading = true; - // ... API call -} -``` - -**Why `markAllAsTouched()`?** - -`mat-error` only displays for touched fields. A user who clicks Submit immediately without touching any field would see a valid-looking form even though required fields are empty — the errors are hidden because nothing has been "touched." - -`markAllAsTouched()` marks every control as touched simultaneously, forcing Angular Material to display all pending validation errors at once. The user sees every problem on the first submit attempt, not one field at a time. - ---- - -## 🔄 Edit Mode: patchValue() - -When editing an existing employee, `loadEmployee()` fetches the record and populates the form: - -```typescript -loadEmployee(id: string): void { - this.loading = true; - this.employeeService.getById(id).subscribe({ - next: (employee: Employee) => { - this.employeeForm.patchValue({ - employeeNumber: employee.employeeNumber, - prefix: employee.prefix, - firstName: employee.firstName, - middleName: employee.middleName, - lastName: employee.lastName, - birthday: employee.birthday || employee.dateOfBirth, - gender: employee.gender, - email: employee.email, - phone: employee.phone || employee.phoneNumber, - salary: employee.salary, - positionId: employee.positionId, - departmentId: employee.departmentId, - }); - this.loading = false; - }, - error: error => { - console.error('Error loading employee:', error); - this.showMessage('Error loading employee'); - this.loading = false; - }, - }); -} -``` - -**`patchValue()` vs `setValue()`:** - -* `setValue()` requires every field in the `FormGroup` to be provided — it throws if any key is missing -* `patchValue()` only updates the fields you provide — missing keys are ignored - -`patchValue()` is the right choice here because it's safe even if the API response is missing optional fields. - -**Field name fallbacks:** - -```typescript -birthday: employee.birthday || employee.dateOfBirth, -phone: employee.phone || employee.phoneNumber, -``` - -The API response may use slightly different field names depending on how data shaping is applied. The `||` fallback handles both naming conventions without breaking the form. - ---- - -## 🔽 API-Loaded Dropdowns with mat-select - -Department and Position dropdowns are loaded from the API on `ngOnInit`: - -```typescript -loadDependencies(): void { - this.departmentService.getAll().subscribe({ - next: departments => { this.departments = departments; }, - error: error => { - console.error('Error loading departments:', error); - this.showMessage('Error loading departments'); - }, - }); - - this.positionService.getAll().subscribe({ - next: positions => { this.positions = positions; }, - error: error => { - console.error('Error loading positions:', error); - this.showMessage('Error loading positions'); - }, - }); -} -``` - -In the template, `mat-select` binds the control and `mat-option` iterates over the loaded arrays: - -```html - - Department - - - {{ dept.name }} - - - - Department is required - - - - - Position - - - {{ position.positionTitle }} - - - - Position is required - - -``` - -`[value]="dept.id"` stores the department's GUID in the form control — not the display name. The form value sent to the API contains IDs, not text labels. The `mat-option` text (`{{ dept.name }}`) is only for display. - -The Gender dropdown uses a component property instead of an API call, since it's a fixed enum: - -```typescript -genderOptions = [ - { value: Gender.Male, label: 'Male' }, - { value: Gender.Female, label: 'Female' }, -]; -``` - -```html - - - {{ option.label }} - - -``` - ---- - -## 📅 Date Picker with mat-datepicker - -The birthday field uses Angular Material's date picker: - -```html - - Date of Birth - - - - - - Date of birth is required - - -``` - -**Three elements working together:** - -* `` — the text input, linked to the picker via template reference -* `` — the calendar icon button that opens the picker, placed as `matIconSuffix` inside the form field -* `` — the popup calendar panel - -`formControlName="birthday"` binds the selected date to the reactive form control. The date adapter configured in `app.config.ts` controls the format: - -```typescript -provideDateFnsAdapter({ - parse: { dateInput: 'yyyy-MM-dd' }, - display: { dateInput: 'yyyy-MM-dd' }, -}) -``` - -The API receives ISO date strings (`"1990-05-15"`), not JavaScript `Date` objects. - ---- - -## ⏳ Loading State and Overlay - -The template uses Angular 17's `@if` control flow syntax for the loading overlay: - -```html -@if (loading) { -
- -
-} -``` - -The submit button is disabled while any API call is in progress: - -```html - -``` - -`[disabled]="loading"` prevents double-submits — clicking Submit a second time while the API request is in flight does nothing. Without this guard, a slow connection could create the same employee twice. - ---- - -## 📤 Building the API Payload - -On submit, the form value is spread into the command interface: - -```typescript -// Create mode -const command: CreateEmployeeCommand = this.employeeForm.value; - -// Edit mode -const command: UpdateEmployeeCommand = { - id: this.employeeId, - ...this.employeeForm.value, -}; -``` - -In create mode, `this.employeeForm.value` directly matches the `CreateEmployeeCommand` interface — all form control names were chosen to align with the API command property names. In edit mode, `id` is added from `this.employeeId` since the ID isn't a form field. - ---- - -## 🔔 User Feedback with MatSnackBar - -Success and error messages use `MatSnackBar` with consistent positioning: - -```typescript -showMessage(message: string): void { - this.snackBar.open(message, 'Close', { - duration: 3000, - horizontalPosition: 'end', - verticalPosition: 'top', - }); -} -``` - -After a successful create: - -```typescript -this.showMessage('Employee created successfully'); -this.router.navigate(['/employees', employee.id]); -``` - -After a successful update: - -```typescript -this.showMessage('Employee updated successfully'); -this.router.navigate(['/employees', this.employeeId]); -``` - -Both cases navigate away after success — the snackbar appears briefly at the destination page. For errors, the component stays on the form so the user can correct the problem: - -```typescript -error: error => { - this.showMessage('Error creating employee'); - this.loading = false; // re-enable the submit button -}, -``` - ---- - -## 🎯 Key Design Decisions - -**`FormBuilder.group()` over manual `FormGroup` construction** — `FormBuilder` reduces boilerplate and keeps field names, defaults, and validators co-located. The alternative (`new FormGroup({ firstName: new FormControl('', Validators.required) })`) spreads the same information across more lines. - -**`markAllAsTouched()` on invalid submit** — the most common reactive forms mistake is not handling the "user submits without touching any field" case. `markAllAsTouched()` covers it in one line. - -**`patchValue()` for edit mode** — `setValue()` would require listing every field in the form, even if some fields aren't in the API response. `patchValue()` only updates what you provide. - -**Separate error messages per validator** — `` and `` give users precise feedback. A single generic "Invalid email" message for both cases leaves users guessing whether the field is empty or malformed. - -**`[disabled]="loading"` on submit** — prevents double-submits on slow connections without any additional logic. The form itself stays interactive so the user can review what they typed. - -**Form control names aligned with API command fields** — `this.employeeForm.value` can be spread directly into `CreateEmployeeCommand` without field name mapping. Consistent naming between form and API eliminates a class of bugs. - -## 🌟 Why This Matters - -The dual-mode create/edit form — where the same `ReactiveForm` handles both `POST` and `PUT` depending on whether an ID is present — is one of the most common patterns in CRUD applications. Building it once correctly (with `markAllAsTouched()` for submit-time validation, `patchValue()` for edit mode, and per-field error messages) means every subsequent form in the app follows the same pattern. - -The submit-guard pattern that prevents double submissions on slow networks transfers to any form in any Angular application. The validation approach using `getFieldError()` helpers eliminates the template noise that accumulates in Angular forms without a shared pattern. - -**Transferable skills:** - -* **Dual-mode `FormBuilder` initialization** — Applicable to any Angular feature with both create and edit workflows -* **`patchValue()` for edit mode** — Foundation for populating forms from existing entity data -* **Per-field error display with `markAllAsTouched()`** — Pattern for immediate validation feedback at form submit - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #angular #angularmaterial #reactiveforms #formvalidation #formbuilder #matselect #datepicker #typescript #fullstack #dotnet #materialdesign #frontend #spa #webdevelopment #ux diff --git a/blogs/series-3-angular-material/3.3-angular-material-dialogs.md b/blogs/series-3-angular-material/3.3-angular-material-dialogs.md deleted file mode 100644 index 80ad9d3..0000000 --- a/blogs/series-3-angular-material/3.3-angular-material-dialogs.md +++ /dev/null @@ -1,388 +0,0 @@ -# The Right Way to Ask "Are You Sure?" — Angular Material Dialogs for Confirm Actions - -## Building a Delete Confirmation Dialog with MatDialog, Proper Result Handling, and Reuse Across the App - -Every app that deletes data needs a confirmation step. The naive approach — a browser `window.confirm()` — is ugly, can't be styled, and doesn't integrate with Angular's change detection. Angular Material's `MatDialog` solves all of this: a fully styled, keyboard-accessible, injectable dialog that returns an Observable result you can act on. - -This article builds the `ConfirmDialogComponent` from the **TalentManagement** app — a reusable dialog used in eight places across the codebase (employee list, employee detail, department list, department detail, position list, position detail, salary range list, and salary range detail). You'll see the complete dialog component, how data flows in via `MAT_DIALOG_DATA`, how results flow out via `MatDialogRef`, and the two different patterns for handling the result depending on whether you navigate after deletion. - -![Employee CRUD Operations](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-crud-operations.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Angular Material dialogs are used for delete confirmations with proper result handling.** - ---- - -## 📚 What You'll Learn - -* The three structural elements every Material dialog needs: `mat-dialog-title`, `mat-dialog-content`, `mat-dialog-actions` -* How `MAT_DIALOG_DATA` passes typed data into the dialog component -* How `MatDialogRef.close(value)` returns a result to the caller -* `cdkFocusInitial` — why the destructive button should get focus, not the cancel button -* Opening a dialog with `MatDialog.open()` and passing config options -* `dialogRef.afterClosed().subscribe()` — the Observable that fires when the dialog closes -* The `if (!confirmed) return;` guard for handling dismiss-without-action -* Two result-handling patterns: reload-the-list vs navigate-with-snackbar - ---- - -## 🏗️ The ConfirmDialogData Interface - -The dialog needs to be reusable across many different contexts — deleting an employee, a department, a position. The content (title, message, button labels) comes from the caller, not from the dialog component itself. - -A TypeScript interface defines the contract: - -```typescript -export interface ConfirmDialogData { - title: string; - message: string; - confirmText?: string; - cancelText?: string; -} -``` - -`title` and `message` are required — every confirmation needs both. `confirmText` and `cancelText` are optional with defaults ("Delete" and "Cancel") applied in the template. The caller decides the wording: - -```typescript -// Deleting an employee -data: { - title: 'Delete Employee', - message: `Are you sure you want to delete ${name}? This action cannot be undone.`, - confirmText: 'Delete', - cancelText: 'Cancel', -} -``` - -The interface is exported alongside the component — consumers import both from the same file: - -```typescript -import { ConfirmDialogComponent, ConfirmDialogData } - from '../../shared/components/confirm-dialog/confirm-dialog'; -``` - ---- - -## 🧩 The Dialog Component - -The complete `ConfirmDialogComponent` is deliberately minimal: - -```typescript -import { Component, inject } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { - MatDialogModule, - MatDialogRef, - MAT_DIALOG_DATA, -} from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; - -@Component({ - selector: 'app-confirm-dialog', - templateUrl: './confirm-dialog.html', - imports: [MatDialogModule, MatButtonModule, MatIconModule], -}) -export class ConfirmDialogComponent { - readonly dialogRef = inject(MatDialogRef); - readonly data = inject(MAT_DIALOG_DATA); - - confirm(): void { - this.dialogRef.close(true); - } - - cancel(): void { - this.dialogRef.close(false); - } -} -``` - -### MAT_DIALOG_DATA: Receiving Data - -`MAT_DIALOG_DATA` is an injection token that carries whatever object the caller passed in the `data` config option. Using `inject(MAT_DIALOG_DATA)` gives the component typed access to the caller's data object — no `@Input()` decorators, no component bindings. - -```typescript -readonly data = inject(MAT_DIALOG_DATA); -``` - -In the template, `data.title`, `data.message`, `data.confirmText`, and `data.cancelText` are all accessible directly. - -### MatDialogRef: Returning a Result - -`MatDialogRef` is the handle for the currently open dialog. Calling `.close(value)` closes the dialog and emits `value` through the `afterClosed()` Observable on the caller's side. - -```typescript -confirm(): void { this.dialogRef.close(true); } -cancel(): void { this.dialogRef.close(false); } -``` - -The caller receives `true` if the user confirmed, `false` if they cancelled, and `undefined` if they clicked outside the dialog or pressed Escape (Material's default dismiss behavior). - ---- - -## 🎨 The Dialog Template - -```html -

- - warning - - {{ data.title }} -

- - -

{{ data.message }}

-
- - - - - -``` - -### The Three Structural Elements - -**`mat-dialog-title`** (on the `

`) — marks the element as the dialog title for accessibility. Screen readers announce this as the dialog's label. Material styles it with the correct typography. - -**`mat-dialog-content`** — the scrollable body of the dialog. If the content is taller than the dialog allows, this section scrolls while the title and actions stay fixed. - -**`mat-dialog-actions`** — the footer area containing action buttons. `align="end"` right-aligns the buttons, following Material Design conventions for dialogs. - -### Default Button Labels - -```html -{{ data.cancelText || 'Cancel' }} -{{ data.confirmText || 'Delete' }} -``` - -The `||` fallback means callers don't have to pass button labels if the defaults work. A delete confirmation that doesn't need custom labels can omit `confirmText` and `cancelText` from the `data` object entirely. - -### cdkFocusInitial: Accessibility - -```html - - -``` - -`cdkDrag` (Angular CDK) makes the button draggable — users can reposition it anywhere on the screen. The `onDragStart` guard prevents a drag event from also triggering a click. - -### Opening the Drawer - -```typescript -private readonly drawer = inject(MtxDrawer); -private drawerRef?: MtxDrawerRef; - -openPanel(templateRef: TemplateRef) { - if (this.dragging) { this.dragging = false; return; } - - this.drawerRef = this.drawer.open(templateRef, { - position: this.form.get('dir')?.value === 'rtl' ? 'left' : 'right', - width: '320px', - }); - - this.drawerRef.afterOpened().subscribe(() => { - this.formSubscription = this.form.valueChanges.subscribe(value => { - this.sendOptions(this.form.getRawValue()); - }); - }); - - this.drawerRef.afterDismissed().subscribe(() => { - this.formSubscription.unsubscribe(); - }); -} -``` - -`MtxDrawer.open()` works like Angular Material's `MatDialog.open()` — it takes a `TemplateRef` and a config object, and returns a `MtxDrawerRef`. The `position` is direction-aware: RTL apps get the drawer on the left. - -The reactive form subscription only runs while the drawer is open (`afterOpened` → `afterDismissed`). Every form value change emits immediately — no Save button. The layout updates in real time as the user changes options. - -### The Settings Form - -```html - - - Light - Dark - Auto - - - Visible - - - Above - Fixed - Static - - - - Side - Top - - - - LTR - RTL - - -``` - -Standard Angular Material form controls bound to a reactive `FormGroup` initialized from `SettingsService.options`. The form is the single source of truth for the customizer — when the drawer closes, the form subscription unsubscribes and no further updates fire. - ---- - -## 📅 The Date-fns Adapter - -Angular Material's date picker needs a date adapter to know how to parse strings into dates and format dates for display. The default `NativeDateAdapter` uses JavaScript's `Date` constructor, which has inconsistent locale behavior across browsers. - -`@ng-matero/extensions-date-fns-adapter` replaces it with [date-fns](https://date-fns.org/) — a modern, locale-aware date utility library. - -```typescript -// app.config.ts -provideDateFnsDatetimeAdapter({ - parse: { - dateInput: 'yyyy-MM-dd', - }, - display: { - dateInput: 'yyyy-MM-dd', - monthYearLabel: 'yyyy MMM', - dateA11yLabel: 'LL', - monthYearA11yLabel: 'yyyy MMM', - }, -}), -``` - -**`parse.dateInput`** — the format date-fns uses to parse a string the user types into the date input field. - -**`display.dateInput`** — the format used when displaying a selected date back in the input field. - -**Why this matters for the API:** The TalentManagement API expects ISO date strings (`"1990-05-15"`). The adapter ensures every date the user picks is formatted as `yyyy-MM-dd` before the form value is sent to the API — no `Date` object serialization quirks. - -Registering the adapter in `app.config.ts` applies it globally — every `mat-datepicker` in the application uses date-fns formatting automatically. No per-component configuration needed. - ---- - -## 🔀 What ng-matero Provides vs. What You Build - -**Provided by ng-matero (in `src/app/theme/`):** -* Responsive three-panel layout (header + sidenav + content) -* Breakpoint-aware sidebar behavior (side / collapsed / drawer) -* Live theme customizer panel with `MtxDrawer` -* Top menu alternative to sidebar -* User panel in sidebar -* Auth layout for login pages -* Navigation progress bar integration - -**Built by the app (in `src/app/routes/`):** -* Employee, department, position, salary range CRUD -* Data tables with server-side pagination and filtering -* Reactive forms with validation -* Confirm dialog for delete actions -* Role-based UI with `*appHasRole` -* OIDC authentication and HTTP interceptor - -The split is clean: ng-matero owns the shell, the app owns the domain. Neither bleeds into the other's territory. - ---- - -## 🎯 Key Design Decisions - -**Scaffold once, own forever** — ng-matero generates `src/app/theme/` and then stays out of the way. The generated files belong to the app. You can delete, rename, or rewrite any of them without worrying about package updates. ng-matero the package is just the scaffolding tool. - -**CSS class bindings over inline styles** — `[class.matero-header-fixed]="options.headerPos === 'fixed'"` is a one-line binding. The stylesheet handles what "fixed header" actually means (position, z-index, box-shadow). Separating intent (the binding) from implementation (the CSS) keeps the TypeScript readable. - -**`BreakpointObserver` over media queries in TypeScript** — responding to screen size changes via `BreakpointObserver` means the logic runs exactly when breakpoints cross, not on every resize event. Angular CDK handles the subscription lifecycle. - -**`MtxDrawer` over `MatDialog` for settings panels** — dialogs are centered, modal, and interrupt workflow. A drawer slides in from the side, stays dismissible, and lets users see the effect of their changes in the background. The right component for the right UX pattern. - -**Live form sync without a Save button** — subscribing to `form.valueChanges` inside `drawerRef.afterOpened()` means every radio button or toggle change is immediately reflected in the layout. Users see results instantly. The subscription is cleaned up in `afterDismissed()` so no stale observers remain. - -## 🌟 Why This Matters - -Starting with a professional admin shell — with a responsive sidebar, settings drawer, and theme switching already wired up — compresses weeks of UI scaffolding into hours. The ng-matero template provides Angular Material infrastructure that most teams build from scratch: `BreakpointObserver` for responsive layouts, `MtxDrawer` for settings panels, `SettingsService` for persistent preferences. - -The `AdminLayout` structure — where every feature page is a lazy-loaded child route inside the shell — is the right architecture for any Angular admin application. Learning how ng-matero implements it gives you a working mental model for structuring your own custom admin shells without the template. - -**Transferable skills:** - -* **`BreakpointObserver` for responsive layouts** — Applicable to any Angular Material app that needs to adapt to mobile/tablet/desktop -* **`MtxDrawer` settings panel** — Pattern for any settings or filter panel that overlays the main content -* **Lazy-loaded feature modules inside an admin shell** — Foundation for scalable Angular application routing structure - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #angular #angularmaterial #ngmatero #admintemplate #layout #theme #breakpointobserver #matsidednav #datefns #typescript #fullstack #dotnet #materialdesign #frontend #spa #webdevelopment #ux #responsive diff --git a/blogs/series-4-playwright-testing/4.1-playwright-first-test.md b/blogs/series-4-playwright-testing/4.1-playwright-first-test.md deleted file mode 100644 index 0b6f12e..0000000 --- a/blogs/series-4-playwright-testing/4.1-playwright-first-test.md +++ /dev/null @@ -1,626 +0,0 @@ -# Your First Playwright Test for an Angular App — From Zero to Green - -## Step-by-Step: Install Playwright, Write a Login Test, and Run It Against a Real OAuth 2.0 App - -Unit tests check functions in isolation. Integration tests check that services wire together. Neither can tell you whether a real user can actually log in to your app, navigate to the employee list, and see data — because that requires a running Angular app, a running .NET API, and a running OAuth 2.0 server all talking to each other correctly. - -That's what Playwright does. It controls a real browser, navigates through your real app with real credentials, and asserts on what the user actually sees. When the test goes green, you know the full stack works. - -This article walks through the Playwright setup in the **AngularNetTutorial** project — the config file, the centralized URLs and timeouts, the three test users, the `loginAs()` fixture, and the first real test: verifying that a user can log in through IdentityServer and land on the dashboard. - -![Employee List Page](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-list-page.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Playwright tests are structured to verify a full OAuth 2.0 login flow end to end.** - ---- - -📦 **Library credit:** Tests are powered by [Playwright](https://playwright.dev/) — Microsoft's open-source browser automation framework for Node.js. It supports Chromium, Firefox, and WebKit in a single API, runs tests in parallel by default, and produces HTML reports, traces, and video recordings on failure. - ---- - -## 📚 What You'll Learn - -* Why E2E testing is the only way to verify an OAuth 2.0 login flow -* The `playwright.config.ts` structure — projects, `baseURL`, `trace`, `video`, reporters -* How `config/test-config.ts` centralizes URLs, timeouts, and viewports -* The three test users in `config/test-users.json` — Employee, Manager, HRAdmin -* How `loginAs()` and `loginAsRole()` in `auth.fixtures.ts` encapsulate the OIDC browser flow -* Walking through `login.spec.ts` test by test -* Running tests and reading the HTML report -* The smoke test that proves create, view, and role restrictions all work - ---- - -## 🤔 Why E2E Testing for an OAuth 2.0 App? - -The full-stack login flow involves four systems: - -``` -Browser - → Angular app (http://localhost:4200) - → IdentityServer (https://localhost:44310) - → Angular callback (http://localhost:4200/callback) - → .NET API (https://localhost:44378/api/v1) -``` - -Unit tests can't touch this. Mocking IdentityServer means you're not testing IdentityServer. The only test that proves the login flow works is one that opens a real browser, clicks "Login", fills credentials on the real IdentityServer page, and verifies the dashboard appears. - -Playwright does exactly that — and it does it reliably enough to run on every commit. - ---- - -## 🗂️ Project Structure - -The Playwright tests live in `Tests/AngularNetTutorial-Playwright/`: - -``` -Tests/AngularNetTutorial-Playwright/ -├── playwright.config.ts ← entry point: projects, baseURL, reporters -├── config/ -│ ├── test-config.ts ← centralized URLs, timeouts, viewports -│ └── test-users.json ← credentials for all three roles -├── fixtures/ -│ ├── auth.fixtures.ts ← loginAs(), loginAsRole(), logout() -│ ├── data.fixtures.ts ← createEmployeeData() and other factories -│ └── api.fixtures.ts ← direct API helpers for setup/teardown -├── page-objects/ -│ ├── base-list.page.ts ← reusable list page logic -│ ├── base-form.page.ts ← reusable form logic -│ ├── employee-list.page.ts ← employee-specific list methods -│ └── employee-form.page.ts ← employee form field locators -└── tests/ - ├── auth/ - │ └── login.spec.ts ← the login tests covered in this article - ├── employee-management/ - │ ├── employee-smoke.spec.ts - │ └── ... - └── ... -``` - -This article focuses on `playwright.config.ts`, `config/`, `fixtures/auth.fixtures.ts`, and `tests/auth/login.spec.ts`. Later articles cover page objects and the full test suites. - ---- - -## ⚙️ playwright.config.ts - -The config file is the entry point for all test runs: - -```typescript -import { defineConfig, devices } from '@playwright/test'; -import { APP_URLS, TIMEOUTS, VIEWPORTS } from './config/test-config'; - -export default defineConfig({ - testDir: './tests', - timeout: TIMEOUTS.standard, // 30 seconds per test - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - - reporter: [ - ['html', { outputFolder: 'playwright-report', open: 'never' }], - ['json', { outputFile: 'test-results/results.json' }], - ['junit', { outputFile: 'test-results/junit.xml' }], - ['list'], // console output during test run - ], - - use: { - baseURL: APP_URLS.angular, // http://localhost:4200 - trace: 'on-first-retry', - video: 'retain-on-failure', - screenshot: 'only-on-failure', - viewport: VIEWPORTS.laptop, // 1366x768 - ignoreHTTPSErrors: true, // for dev self-signed certs - actionTimeout: 10000, - }, - - projects: [ - { name: 'setup', testMatch: /.*\.setup\.ts/ }, - - { - name: 'chromium', - use: { ...devices['Desktop Chrome'], viewport: VIEWPORTS.laptop }, - dependencies: ['setup'], - }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'], viewport: VIEWPORTS.laptop }, - dependencies: ['setup'], - }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'], viewport: VIEWPORTS.laptop }, - dependencies: ['setup'], - }, - - // API-only project (headless, no browser UI needed) - { - name: 'api', - testMatch: /tests\/api\/.*\.spec\.ts/, - use: { - baseURL: APP_URLS.api, - extraHTTPHeaders: { 'Accept': 'application/json' }, - }, - }, - ], -}); -``` - -### Key Decisions - -**`baseURL: APP_URLS.angular`** — every test that calls `page.goto('/')` resolves to `http://localhost:4200`. Tests never hardcode the host. - -**`ignoreHTTPSErrors: true`** — development uses self-signed certificates on IdentityServer (`https://localhost:44310`) and the API (`https://localhost:44378`). Without this, the browser would refuse the connection. - -**`trace: 'on-first-retry'`** — when a test fails and retries, Playwright records a trace file. You can open it in the Playwright Trace Viewer to see every action, screenshot, and network request — essential for debugging flaky tests. - -**`video: 'retain-on-failure'`** — a video recording is saved only for failing tests, not every test. This saves disk space while still giving you a video to watch when something goes wrong. - -**Four projects** — three browser projects (Chromium, Firefox, WebKit) for E2E tests, and one `api` project for tests that hit the API directly without a browser. All three browser projects depend on `setup`, which runs any `.setup.ts` files first (for global auth state). - ---- - -## 🔧 Centralized Config: test-config.ts - -All configurable values live in one file, imported by both `playwright.config.ts` and the tests: - -```typescript -export const APP_URLS = { - angular: 'http://localhost:4200', - api: 'https://localhost:44378/api/v1', - identityServer: 'https://localhost:44310', -} as const; - -export const TIMEOUTS = { - standard: 30000, // most operations - short: 5000, // quick checks - long: 60000, // large dataset loading - afterNavigation: 1000, - validation: 500, - dynamicContent: 2000, -} as const; - -export const VIEWPORTS = { - mobile: { width: 375, height: 667 }, - tablet: { width: 768, height: 1024 }, - laptop: { width: 1366, height: 768 }, - desktop: { width: 1920, height: 1080 }, -} as const; - -export const FEATURES = { - skipApiAuthTests: true, - runVisualTests: true, - runPerformanceTests: true, - runA11yTests: true, -} as const; -``` - -When the API moves to a different port, or timeouts need adjustment for a slower CI machine, there's one file to change. No hunting through individual spec files. - ---- - -## 👥 Three Test Users: test-users.json - -The app has three roles with different permissions. Three real test accounts exist in IdentityServer for automated testing: - -```json -{ - "employee": { - "username": "antoinette16", - "password": "Pa$$word123", - "role": "Employee", - "canCreate": false, - "canEdit": false, - "canDelete": false, - "description": "Read-only access to all modules" - }, - "manager": { - "username": "rosamond33", - "password": "Pa$$word123", - "role": "Manager", - "canCreate": true, - "canEdit": true, - "canDelete": false, - "restrictedModules": ["positions", "salaryRanges"] - }, - "hradmin": { - "username": "ashtyn1", - "password": "Pa$$word123", - "role": "HRAdmin", - "canCreate": true, - "canEdit": true, - "canDelete": true, - "description": "Full administrative access" - } -} -``` - -These are real IdentityServer accounts — not mocks. When a test logs in as `hradmin`, it authenticates against the real OAuth 2.0 server, gets a real JWT, and hits the real API. Tests that use different roles use `loginAsRole(page, 'employee')` or `loginAsRole(page, 'hradmin')` — the credentials come from this file. - ---- - -## 🔐 The loginAs() Fixture - -The full OIDC browser login flow is about 30 lines of Playwright. Wrapped in a reusable `loginAs()` function in `fixtures/auth.fixtures.ts`, every test that needs authentication calls one line: - -```typescript -export async function loginAs( - page: Page, - username: string, - password: string -): Promise { - // Start at the Angular app in Guest state - await page.goto('/'); - await page.waitForLoadState('networkidle'); - - // Clear any existing tokens for a clean state - await clearAuthTokens(page); - await page.reload(); - await page.waitForLoadState('networkidle'); - - // Open the user menu (top-right corner) - const userIcon = page.locator( - 'button[aria-label="User menu"], button mat-icon:has-text("account_circle"), header button:has(mat-icon)' - ).last(); - await userIcon.waitFor({ state: 'visible', timeout: 10000 }); - await userIcon.click(); - - // Click "Login" from the dropdown - const loginOption = page.locator( - 'button:has-text("Login"), a:has-text("Login"), [role="menuitem"]:has-text("Login")' - ).first(); - await loginOption.waitFor({ state: 'visible', timeout: 5000 }); - await loginOption.click(); - - // Wait for redirect to IdentityServer login page - await Promise.race([ - page.waitForURL(/localhost:44310.*/, { timeout: 30000 }), - page.waitForSelector('input[name="Username"]', { timeout: 30000 }) - ]); - - // Fill credentials on the IdentityServer login form - await page.fill('input[name="Username"]', username); - await page.fill('input[name="Password"]', password); - await page.click('button:has-text("Login")'); - - // Wait for OAuth callback redirect back to Angular - await page.waitForURL(/localhost:4200.*/, { timeout: 30000 }); - - // Wait for dashboard to confirm successful authentication - await page.waitForSelector( - 'h1:has-text("Dashboard"), h2:has-text("Dashboard"), .matero-page-title', - { timeout: 10000 } - ); -} -``` - -The function handles the entire PKCE flow — Angular redirects to IdentityServer, credentials are submitted on IdentityServer's page, IdentityServer redirects back with the auth code, Angular exchanges the code for tokens, and the dashboard appears. From the test's perspective: `await loginAs(page, 'ashtyn1', 'Pa$$word123')` and it's done. - -`loginAsRole()` wraps `loginAs()` using `test-users.json`: - -```typescript -export async function loginAsRole( - page: Page, - role: 'employee' | 'manager' | 'hradmin' -): Promise { - const user = testUsers[role]; - await loginAs(page, user.username, user.password); -} -``` - -Tests call `loginAsRole(page, 'manager')` without knowing credentials. If a test user's password changes, only `test-users.json` needs updating. - ---- - -## 🧪 login.spec.ts: The First Tests - -The login spec covers seven scenarios. Each one is a standalone test with its own browser state: - -```typescript -import { test, expect } from '@playwright/test'; -import { loginAs, loginAsRole, isAuthenticated, - getStoredToken, clearAuthTokens, logout } from '../../fixtures/auth.fixtures'; - -test.describe('Login Flow', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await clearAuthTokens(page); - await logout(page).catch(() => {}); - await page.goto('/'); - }); -``` - -`test.beforeEach` ensures every test starts in a clean Guest state — no leftover tokens from a previous test. - -### Test 1: Redirect to IdentityServer - -```typescript -test('should redirect to IdentityServer login page', async ({ page }) => { - await page.goto('/'); - await page.waitForLoadState('networkidle'); - - // Open user menu and click Login - const userIcon = page.locator('button[aria-label="User menu"], ...').last(); - await userIcon.click(); - const loginOption = page.locator('button:has-text("Login"), ...').first(); - await loginOption.click(); - - // Should redirect to IdentityServer - await Promise.race([ - page.waitForURL(/sts\.skoruba\.local.*/, { timeout: 15000 }), - page.waitForSelector('input[name="Username"]', { timeout: 15000 }) - ]); - - // Verify the IdentityServer login form appears - await expect(page.locator('input[name="Username"]')).toBeVisible(); - await expect(page.locator('input[name="Password"]')).toBeVisible(); - await expect(page.locator('button:has-text("Login")')).toBeVisible(); -}); -``` - -This test doesn't authenticate — it just verifies the redirect happens. `Promise.race()` handles environments where the IdentityServer may have a custom hostname. - -### Test 2: Successful Login - -```typescript -test('should successfully login with valid credentials', async ({ page }) => { - await loginAs(page, 'ashtyn1', 'Pa$$word123'); - - // Verify redirect back to Angular - await expect(page).toHaveURL(/localhost:4200/); - - // Dashboard visible = authenticated state confirmed - await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible(); - - // Verify NOT showing "Guest" - const isGuest = await page.locator('text=Guest').isVisible({ timeout: 2000 }).catch(() => false); - expect(isGuest).toBe(false); - - // Username visible in the header - const usernameDisplay = page.locator('text=/ashtyn1|antoinette16|rosamond33/i'); - await expect(usernameDisplay.first()).toBeVisible(); -}); -``` - -Three assertions: correct URL, dashboard visible, username shown. All three must pass for the login to be genuinely working. - -### Test 3: Token Stored in Browser Storage - -```typescript -test('should store access token in browser storage', async ({ page }) => { - await loginAs(page, 'ashtyn1', 'Pa$$word123'); - await page.waitForURL(/localhost:4200/); - - const token = await getStoredToken(page); - - // If token found, verify JWT format: xxx.yyy.zzz - if (token) { - expect(token).toMatch(/^[\w-]+\.[\w-]+\.[\w-]+$/); - } -}); -``` - -`getStoredToken()` checks both `sessionStorage` and `localStorage` for keys containing `access_token`. The `angular-oauth2-oidc` library stores the token in `sessionStorage` with the key `access_token`. If it's found, the regex validates it's in JWT format. - -### Test 4: Authenticated State Persists Across Navigation - -```typescript -test('should maintain authenticated state after login', async ({ page }) => { - await loginAs(page, 'ashtyn1', 'Pa$$word123'); - - const authenticated = await isAuthenticated(page); - expect(authenticated).toBe(true); - - // Navigate to a different page - await page.goto('/employees'); - - // Still authenticated — no redirect to login - await expect(page).toHaveURL(/employees/); - const stillAuthenticated = await isAuthenticated(page); - expect(stillAuthenticated).toBe(true); -}); -``` - -`isAuthenticated()` checks whether the `Guest` heading appears in the sidebar: - -```typescript -export async function isAuthenticated(page: Page): Promise { - const guestCount = await page.locator('h4:has-text("Guest")').count(); - return guestCount === 0; -} -``` - -If the user panel shows "Guest", the session is gone. If it's absent, the user is authenticated. - -### Tests 5–7: Role-Based Login - -```typescript -test('should login as Employee role', async ({ page }) => { - await loginAsRole(page, 'employee'); - await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible(); - expect(await isAuthenticated(page)).toBe(true); -}); - -test('should login as Manager role', async ({ page }) => { - await loginAsRole(page, 'manager'); - await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible(); - expect(await isAuthenticated(page)).toBe(true); -}); - -test('should login as HRAdmin role', async ({ page }) => { - await loginAsRole(page, 'hradmin'); - await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible(); - expect(await isAuthenticated(page)).toBe(true); -}); -``` - -Each role gets its own login test. All three use the same assertion: dashboard visible + authenticated. Role-specific permission tests come later — these just prove all three accounts can authenticate. - ---- - -## 🚀 Running the Tests - -**Prerequisites:** All three services must be running (IdentityServer, API, Angular). - -```bash -cd Tests/AngularNetTutorial-Playwright - -# Install dependencies (first time only) -npm install - -# Run all tests (all browsers) -npx playwright test - -# Run only the login tests -npx playwright test tests/auth/login.spec.ts - -# Run with the interactive UI (recommended for development) -npx playwright test --ui - -# Watch the browser as tests run -npx playwright test --headed - -# Run on one browser only -npx playwright test --project=chromium -``` - -**After a test run, open the HTML report:** - -```bash -npx playwright show-report -``` - -The report shows every test, its duration, pass/fail status, and — for failures — the screenshot, video, and trace file. - ---- - -## 📊 Reading the Output - -A successful run of the login spec looks like this in the console: - -``` -Running 7 tests using 3 workers - - ✓ [chromium] › tests/auth/login.spec.ts:25:3 › Login Flow › should redirect to IdentityServer (4.2s) - ✓ [chromium] › tests/auth/login.spec.ts:59:3 › Login Flow › should successfully login (6.8s) - ✓ [chromium] › tests/auth/login.spec.ts:79:3 › Login Flow › should store access token (7.1s) - ✓ [chromium] › tests/auth/login.spec.ts:101:3 › Login Flow › should maintain authenticated state (8.3s) - ✓ [chromium] › tests/auth/login.spec.ts:118:3 › Login Flow › should login as Employee role (6.5s) - ✓ [chromium] › tests/auth/login.spec.ts:127:3 › Login Flow › should login as Manager role (6.4s) - ✓ [chromium] › tests/auth/login.spec.ts:136:3 › Login Flow › should login as HRAdmin role (6.7s) - - 7 passed (28.1s) -``` - -The format is `[browser] › file:line › describe › test name (duration)`. Cross-browser runs the same seven tests three times — once per browser project. - ---- - -## 🔥 The Smoke Test: employee-smoke.spec.ts - -With login confirmed, the smoke test verifies the three most critical employee operations end to end: - -```typescript -test.describe('Employee Management - Smoke Tests', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'manager'); - await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible(); - }); - - test('should view employee list', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const employeeList = page.locator('table, mat-table, .employee-list'); - await expect(employeeList.first()).toBeVisible(); - - const rows = page.locator('tr, mat-row, .employee-row'); - expect(await rows.count()).toBeGreaterThan(0); - }); - - test('should create new employee', async ({ page }) => { - await logout(page); - await loginAsRole(page, 'hradmin'); // only HRAdmin can create - - const employeeData = createEmployeeData({ firstName: 'John', lastName: 'Doe', salary: 75000 }); - - await page.goto('/employees'); - const createButton = page.locator('button').filter({ hasText: /create|add.*employee|new/i }); - await createButton.first().click(); - - const employeeForm = new EmployeeFormPage(page); - await employeeForm.waitForForm(); - await employeeForm.fillForm({ ...employeeData, dateOfBirth: '01/01/1990' }); - await employeeForm.submit(); - - const result = await employeeForm.verifySubmissionSuccess(); - expect(result.success).toBe(true); - }); - - test('should not show Create button for Employee role', async ({ page }) => { - await logout(page); - await loginAsRole(page, 'employee'); - await page.goto('/employees'); - - const createButton = page.locator('button').filter({ hasText: /create|add.*employee|new/i }); - const hasCreate = await createButton.isVisible({ timeout: 2000 }).catch(() => false); - expect(hasCreate).toBe(false); - }); -}); -``` - -Three scenarios, three facts confirmed by the test: -* The employee list loads data from the API -* An HRAdmin can create an employee through the full form flow -* An Employee role user does NOT see the Create button (role-based UI enforcement verified) - ---- - -## 🎯 Key Design Decisions - -**Fixtures over repeated setup code** — `loginAs()` and `loginAsRole()` are called in dozens of tests. If the user menu selector changes, one function update fixes all tests. Copy-pasted login code means updating every spec file. - -**`clearAuthTokens()` in `beforeEach`** — sessionStorage persists within a browser context. Without explicitly clearing tokens between tests, a passing test could leave auth state that causes the next test to start already-logged-in, bypassing the login flow it was supposed to test. - -**`Promise.race()` for redirect detection** — the IdentityServer hostname can vary between development environments. Racing between `waitForURL(identityServerRegex)` and `waitForSelector('input[name="Username"]')` means the test passes on both the default config and custom hostnames. - -**Three role accounts, not one** — using separate accounts per role means tests don't need to log out and back in to switch roles. Each test starts fresh with `beforeEach`, so `loginAsRole(page, 'employee')` simply logs in as the employee account without any state from a previous admin session. - -**`retries: process.env.CI ? 2 : 0`** — E2E tests against OAuth are inherently more variable than unit tests (network, timing, external servers). On CI, two retries give flaky tests a second chance. Locally, no retries so failures are immediately visible. - -## 🌟 Why This Matters - -The first Playwright test is the hardest. Once `loginAsRole()`, centralized config, and the OIDC browser flow are working, every subsequent test in the suite builds on that foundation. The setup cost is paid once; the benefit compounds across hundreds of tests. - -The approach — completing a real OIDC browser flow rather than mocking it — means the tests validate the full authentication stack: Angular's `angular-oauth2-oidc` library, IdentityServer's PKCE flow, and the token storage. A mock would miss all of that. The same pattern applies to any Angular app using any OAuth 2.0 provider. - -**Transferable skills:** - -* **Playwright setup for Angular OAuth apps** — Applicable to any Angular app using `angular-oauth2-oidc` or a similar OIDC library -* **Centralized test configuration** — Foundation for managing base URLs and credentials across a test suite without hardcoding -* **Real OIDC browser flow automation** — Pattern for authenticating through any OAuth 2.0 / OIDC provider in E2E tests - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #playwright #angular #testing #e2e #oauth2 #oidc #typescript #dotnet #fullstack #materialdesign #webdevelopment #qa #automation #identityserver #browsertesting diff --git a/blogs/series-4-playwright-testing/4.2-playwright-page-object-model.md b/blogs/series-4-playwright-testing/4.2-playwright-page-object-model.md deleted file mode 100644 index 93e8b6f..0000000 --- a/blogs/series-4-playwright-testing/4.2-playwright-page-object-model.md +++ /dev/null @@ -1,650 +0,0 @@ -# Stop Copy-Pasting Selectors: The Page Object Model for Angular Material - -## How BaseListPage and BaseFormPage Eliminate Selector Duplication Across Your Entire Test Suite - -Every Playwright tutorial shows you this: - -```typescript -await page.locator('button').filter({ hasText: /create|add|new/i }).first().click(); -await page.locator('input[formControlName="firstName"]').fill('John'); -await page.locator('mat-select[formControlName="positionId"]').click(); -``` - -That works fine for one test. But what happens when you have twenty tests for employees, fifteen for departments, and ten for positions — all clicking the same buttons and filling the same fields? The moment the selector changes, you're doing a grep-and-replace across every spec file. - -The **Page Object Model** (POM) solves this. Instead of putting selectors in tests, you put them in a class. Tests call methods like `employeeForm.fillForm(data)` — and if the selector changes, you fix it in one place. - -This article walks through the POM implementation in the **AngularNetTutorial** project: two abstract base classes that handle everything common to all list pages and all form pages, and two thin entity classes that add only what's specific to employees. - -![Employee Form Page](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-form.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into the Page Object Model pattern applied to an Angular Material app — and shows how a two-level inheritance hierarchy keeps selectors in one place.** - ---- - -## 📚 What You'll Learn - -* What the Page Object Model is and why it matters for Angular Material apps -* The `BaseListPage` class — shared logic for every list/table page in the app -* The `BaseFormPage` class — shared logic for every create/edit form -* How `EmployeeListPage` extends `BaseListPage` in under 50 lines -* How `EmployeeFormPage` uses `formControlName` selectors and a shared `selectDropdown()` helper -* The three-fallback `verifySubmissionSuccess()` pattern for resilient test assertions -* The `getRow(index + 1)` trick for skipping Angular Material's header row -* How tests look before and after applying POM - ---- - -## 🤔 The Problem: Selector Sprawl - -Without POM, your test file looks like this: - -```typescript -// Before: selectors scattered across the test -test('should create employee', async ({ page }) => { - await page.locator('button').filter({ hasText: /create/i }).first().click(); - await page.locator('input[formControlName="firstName"]').fill('John'); - await page.locator('input[formControlName="lastName"]').fill('Doe'); - await page.locator('input[formControlName="email"]').fill('john@example.com'); - await page.locator('mat-select[formControlName="positionId"]').click(); - await page.locator('mat-option').nth(1).click(); - await page.locator('mat-select[formControlName="departmentId"]').click(); - await page.locator('mat-option').nth(1).click(); - await page.locator('button').filter({ hasText: /save|create/i }).first().click(); -}); -``` - -Copy this across ten tests and you have ten places to update when `positionId` changes to `positionID` or the team decides to use `name="position"` instead. - -With POM: - -```typescript -// After: test reads like a user story -test('should create employee', async ({ page }) => { - const form = new EmployeeFormPage(page); - await form.fillForm({ firstName: 'John', lastName: 'Doe', email: 'john@example.com', - position: 1, department: 1, gender: 1 }); - await form.submit(); - const result = await form.verifySubmissionSuccess(); - expect(result.success).toBe(true); -}); -``` - -The selector lives in `EmployeeFormPage`. The test reads like a user story. - ---- - -## 🗂️ POM Structure - -The page objects live in `Tests/AngularNetTutorial-Playwright/page-objects/`: - -``` -page-objects/ -├── base-list.page.ts ← shared logic for ALL list pages -├── base-form.page.ts ← shared logic for ALL form pages -├── employee-list.page.ts ← employee-specific extension of BaseListPage -└── employee-form.page.ts ← employee-specific extension of BaseFormPage -``` - -The inheritance hierarchy: - -``` -BaseListPage - └── EmployeeListPage - -BaseFormPage - └── EmployeeFormPage -``` - -New entities (Departments, Positions) follow the same pattern — extend the base, add only what's specific. - ---- - -## 📋 BaseListPage — Shared Logic for Every Table - -`BaseListPage` is the parent class for every page in the app that shows a data table. You construct it once with the entity URL and name, and it gives you all the locators and actions you need. - -### Constructor: Three Parameters - -```typescript -export class BaseListPage { - constructor(page: Page, url: string, entityName: string) { - this.page = page; - this.url = url; - this.entityName = entityName; - - this.pageTitle = page.locator('h1, h2, h3') - .filter({ hasText: new RegExp(entityName, 'i') }); - this.table = page.locator('table, mat-table').first(); - this.rows = page.locator('tr, mat-row'); - - this.searchInput = page.locator( - 'input[placeholder*="Search"], input[name*="search"]' - ).first(); - - this.createButton = page.locator('button') - .filter({ hasText: /create|add|new/i }).first(); - - this.nextPageButton = page.locator('button[aria-label*="Next"]').first(); - this.previousPageButton = page.locator('button[aria-label*="Previous"]').first(); - this.pageSizeSelector = page.locator('mat-select[aria-label*="Items per page"]'); - } -} -``` - -**All locators are defined once here.** Every subclass — `EmployeeListPage`, `DepartmentListPage`, `PositionListPage` — inherits them automatically. The `entityName` parameter drives the page title check: for employees it becomes `/employees/i`, for departments `/departments/i`. - -### The Header Row Skip - -Angular Material tables always have a header row. When you ask for `tr` or `mat-row`, the first result is the column headers — not data. `BaseListPage` handles this transparently: - -```typescript -/** - * Get a data row by zero-based index (automatically skips the header row). - */ -getRow(index: number): Locator { - return this.rows.nth(index + 1); // +1 to skip

/ header mat-row -} -``` - -Your tests use `getRow(0)` for the first data row, `getRow(1)` for the second. The `+1` offset is invisible to callers — it's encapsulated in the base class where it belongs. - -### Row Count - -```typescript -async getRowCount(): Promise { - const count = await this.rows.count(); - return count > 1 ? count - 1 : count; // subtract header row -} -``` - -Same idea: the header row is subtracted automatically. `getRowCount()` always returns the number of data rows. - -### Search - -```typescript -async search(searchText: string) { - const isVisible = await this.searchInput - .isVisible({ timeout: 2000 }).catch(() => false); - if (!isVisible) return; // No search input on this page — skip silently - await this.searchInput.fill(searchText); - await this.page.waitForTimeout(1000); // debounce delay -} -``` - -The `isVisible` guard means this method is safe to call on pages that don't have a search box — it silently no-ops instead of throwing. The 1-second wait handles Angular Material's debounce on search inputs. - -### Permission Checks - -```typescript -async hasCreatePermission(): Promise { - return await this.createButton - .isVisible({ timeout: 2000 }).catch(() => false); -} - -async hasEditPermission(): Promise { - const editButton = this.rows.nth(1).locator('button') - .filter({ hasText: /edit/i }); - return await editButton.isVisible({ timeout: 2000 }).catch(() => false); -} - -async hasDeletePermission(): Promise { - const deleteButton = this.rows.nth(1).locator('button') - .filter({ hasText: /delete/i }); - return await deleteButton.isVisible({ timeout: 2000 }).catch(() => false); -} -``` - -Role-based UI tests become one-liners: - -```typescript -// Employee role (read-only) should NOT see Create -expect(await employeeList.hasCreatePermission()).toBe(false); - -// HRAdmin should see Delete buttons -expect(await employeeList.hasDeletePermission()).toBe(true); -``` - -### Pagination - -```typescript -async changePageSize(size: number) { - await this.pageSizeSelector.click(); - await this.page.waitForTimeout(500); - await this.page.locator('mat-option, option') - .filter({ hasText: new RegExp(`^${size}$`) }) - .first() - .click(); - await this.page.waitForTimeout(1000); -} -``` - -The regex `^${size}$` matches exactly `10`, `25`, or `50` — not a partial match like "100" matching "10". All pagination operations wait for Angular Material's mat-option overlay to open before clicking. - ---- - -## 📝 BaseFormPage — Shared Logic for Every Form - -`BaseFormPage` handles everything common to create/edit forms: waiting for the form to appear, submitting, detecting validation errors, and verifying the result. - -### Constructor - -```typescript -export class BaseFormPage { - constructor(page: Page, listPath: string) { - this.page = page; - this.listPath = listPath; // e.g. '/employees' - - this.form = page.locator('form, mat-dialog form').first(); - this.saveButton = page.locator('button') - .filter({ hasText: /save|submit|create|update/i }).first(); - this.cancelButton = page.locator('button') - .filter({ hasText: /cancel|back|close/i }).first(); - - this.validationErrors = page.locator( - 'mat-error, .mat-error, .mat-mdc-form-field-error, .error, [role="alert"]' - ); - - this.dialog = page.locator('mat-dialog, .modal, [role="dialog"]'); - } -} -``` - -The `listPath` parameter is stored for use in `verifySubmissionSuccess()` — it's how the base class knows where to look after a successful submit. - -### Generic Dropdown Helper - -Angular Material's `mat-select` requires a two-step interaction: click to open the overlay, then click an option. The `selectDropdown()` helper encapsulates this: - -```typescript -protected async selectDropdown(selectLocator: Locator, value: string | number) { - const isVisible = await selectLocator - .isVisible({ timeout: 2000 }).catch(() => false); - if (!isVisible) return; - - await selectLocator.click(); - await this.page.waitForTimeout(500); // wait for overlay to open - - if (typeof value === 'number') { - await this.page.locator('mat-option, option').nth(value).click(); - } else { - await this.page.locator('mat-option, option') - .filter({ hasText: new RegExp(value, 'i') }) - .first() - .click(); - } - - await this.page.waitForTimeout(500); -} -``` - -**Two selection modes:** - -* Pass a **number** (e.g. `1`) to select by position — index 0 is the blank placeholder, index 1 is the first real option -* Pass a **string** (e.g. `'Engineering'`) to select by text match - -This method is `protected` — subclasses call it directly, but tests don't access it. In `EmployeeFormPage`: - -```typescript -async selectPosition(positionName: string | number = 1) { - await this.selectDropdown(this.positionSelect, positionName); -} -``` - -### The Three-Fallback verifySubmissionSuccess() - -This is the most interesting method in the base class. The dev environment occasionally returns a 401 from the API even when the form data was accepted — so a strict "look for a success toast" test would be flaky. The solution is three fallback checks in priority order: - -```typescript -async verifySubmissionSuccess(): Promise<{ - success: boolean; - method: 'message' | 'redirect' | 'formFilled' -}> { - await this.page.waitForTimeout(3000); - - // 1. Did a success snackbar appear? - const hasSuccess = await this.waitForSuccessNotification(); - if (hasSuccess) return { success: true, method: 'message' }; - - // 2. Did the page redirect to the list? - const isOnListPage = this.page.url().includes(this.listPath) - && !this.page.url().includes('/create'); - if (isOnListPage) return { success: true, method: 'redirect' }; - - // 3. Are the form fields still populated? - // (API error workaround — form stays filled when request was sent) - const formFilled = await this.isFormStillFilled(); - if (formFilled) return { success: true, method: 'formFilled' }; - - return { success: false, method: 'formFilled' }; -} -``` - -**Why three fallbacks?** - -* **`message`** — the happy path: Angular shows a `mat-snack-bar` saying "Employee created" -* **`redirect`** — also happy path: form submits, app navigates back to `/employees` -* **`formFilled`** — the pragmatic workaround: in the dev environment, the API sometimes returns 401 but the form was filled and submitted, so the test passes on the basis that the UI did its job - -The returned `method` tells you which path was taken — useful for debugging: - -```typescript -const result = await employeeForm.verifySubmissionSuccess(); -expect(result.success).toBe(true); -// result.method will be 'message', 'redirect', or 'formFilled' -``` - -### Overridable isFormStillFilled() - -The base class has a generic fallback: - -```typescript -// BaseFormPage — generic default -protected async isFormStillFilled(): Promise { - const inputs = this.page.locator('form input[type="text"]'); - const count = await inputs.count(); - if (count > 0) { - const value = await inputs.first().inputValue().catch(() => ''); - return value.length > 0; - } - return false; -} -``` - -`EmployeeFormPage` overrides this with employee-specific logic: - -```typescript -// EmployeeFormPage — knows which fields to check -protected async isFormStillFilled(): Promise { - const firstNameValue = await this.page - .getByLabel('First Name').inputValue().catch(() => ''); - const lastNameValue = await this.page - .getByLabel('Last Name').inputValue().catch(() => ''); - return firstNameValue.length > 0 && lastNameValue.length > 0; -} -``` - -This is the **Template Method** pattern: the base class defines the algorithm (`verifySubmissionSuccess` calls `isFormStillFilled`), and subclasses provide the entity-specific implementation. - ---- - -## 👤 EmployeeListPage — A Minimal Subclass - -With all the logic in `BaseListPage`, the employee-specific class is tiny: - -```typescript -export class EmployeeListPage extends BaseListPage { - constructor(page: Page) { - super(page, '/employees', 'employees'); - } - - // Employee-named aliases for readability - async getEmployeeCount(): Promise { - return this.getRowCount(); - } - - getEmployeeRow(index: number): Locator { - return this.getRow(index); - } - - getEmployeeByName(name: string): Locator { - return this.getRowByText(name); - } - - async clickEmployee(index: number) { - await this.clickRow(index); - } -} -``` - -**That's it.** The constructor passes `'/employees'` and `'employees'` to the base. The remaining methods are domain-specific aliases — `getEmployeeCount()` reads better in an employee test than `getRowCount()`, even though they do the same thing. - -Adding `DepartmentListPage` requires exactly two lines: - -```typescript -export class DepartmentListPage extends BaseListPage { - constructor(page: Page) { super(page, '/departments', 'departments'); } -} -``` - ---- - -## 📋 EmployeeFormPage — Entity-Specific Fields - -`EmployeeFormPage` adds the employee-specific field locators and `fillForm()`: - -### Field Locators Using formControlName - -```typescript -export class EmployeeFormPage extends BaseFormPage { - readonly firstNameInput: Locator; - readonly lastNameInput: Locator; - readonly emailInput: Locator; - readonly positionSelect: Locator; - readonly departmentSelect: Locator; - readonly genderSelect: Locator; - - constructor(page: Page) { - super(page, '/employees'); - - this.firstNameInput = page.locator( - 'input[name*="firstName"], input[formControlName="firstName"]' - ); - this.positionSelect = page.locator( - 'mat-select[formControlName="positionId"], select[name*="position"]' - ); - // ... etc. - } -} -``` - -**Why use `formControlName` as a selector?** - -Angular's reactive forms bind each input to a control name via `[formControlName]="'firstName'"`. This attribute appears directly in the DOM — it doesn't change unless you rename the form control in the TypeScript. It's more stable than placeholder text or `id` attributes, which the template team might refactor at any time. - -The dual selector `input[name*="firstName"], input[formControlName="firstName"]` handles both template-driven and reactive forms with one locator. - -### fillForm() — The Convenience Method - -```typescript -async fillForm(employeeData: { - firstName: string; - lastName: string; - email: string; - employeeNumber?: string; - phoneNumber?: string; - dateOfBirth?: string; - salary?: number; - position?: string | number; - department?: string | number; - gender?: string | number; -}) { - await this.fillFirstName(employeeData.firstName); - await this.fillLastName(employeeData.lastName); - await this.fillEmail(employeeData.email); - - if (employeeData.employeeNumber) await this.fillEmployeeNumber(employeeData.employeeNumber); - if (employeeData.dateOfBirth) await this.fillDateOfBirth(employeeData.dateOfBirth); - if (employeeData.phoneNumber) await this.fillPhoneNumber(employeeData.phoneNumber); - if (employeeData.salary) await this.fillSalary(employeeData.salary); - if (employeeData.position !== undefined) await this.selectPosition(employeeData.position); - if (employeeData.department !== undefined) await this.selectDepartment(employeeData.department); - if (employeeData.gender !== undefined) await this.selectGender(employeeData.gender); -} -``` - -Three fields are required (`firstName`, `lastName`, `email`). Everything else is optional — the `if` guards skip fields that aren't provided. This makes partial fills natural: - -```typescript -// Fill only required fields -await form.fillForm({ firstName: 'John', lastName: 'Doe', email: 'j@test.com' }); - -// Full form fill -await form.fillForm({ - firstName: 'John', lastName: 'Doe', email: 'j@test.com', - salary: 75000, position: 1, department: 1, gender: 1, - dateOfBirth: '01/01/1990', phoneNumber: '555-0100', -}); -``` - ---- - -## 🔬 Before and After: A Full Test Comparison - -Here's the "create employee" test from `employee-smoke.spec.ts` — before and after applying POM. - -**Without POM:** -```typescript -test('should create employee', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - await page.locator('button').filter({ hasText: /create/i }).first().click(); - await page.waitForTimeout(1000); - - await page.locator('input[formControlName="firstName"]').fill('John'); - await page.locator('input[formControlName="lastName"]').fill('Doe'); - await page.locator('input[formControlName="email"]').fill('john@test.com'); - await page.locator('mat-select[formControlName="positionId"]').click(); - await page.locator('mat-option').nth(1).click(); - await page.locator('mat-select[formControlName="departmentId"]').click(); - await page.locator('mat-option').nth(1).click(); - await page.locator('mat-select[formControlName="gender"]').click(); - await page.locator('mat-option').nth(1).click(); - await page.locator('button').filter({ hasText: /save|create/i }).first().click(); - await page.waitForTimeout(3000); - - // Was it successful? Hard to tell... -}); -``` - -**With POM (actual code from the project):** -```typescript -test('should create new employee', async ({ page }) => { - const employeeData = createEmployeeData({ - firstName: 'John', lastName: 'Doe', salary: 75000, - }); - - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const createButton = page.locator('button') - .filter({ hasText: /create|add.*employee|new/i }); - await createButton.first().click(); - - const employeeForm = new EmployeeFormPage(page); - await employeeForm.waitForForm(); - await employeeForm.fillForm({ - firstName: employeeData.firstName, - lastName: employeeData.lastName, - email: employeeData.email, - employeeNumber: employeeData.employeeNumber, - dateOfBirth: '01/01/1990', - phoneNumber: employeeData.phoneNumber, - salary: employeeData.salary, - department: 1, - position: 1, - gender: 1, - }); - - await employeeForm.submit(); - const result = await employeeForm.verifySubmissionSuccess(); - expect(result.success).toBe(true); -}); -``` - -The test reads like documentation. A new developer can understand what it does without knowing any Playwright APIs. - ---- - -## 🔑 Key Design Decisions - -**Why `protected` on `selectDropdown()`?** - -Subclasses call `selectDropdown()` internally (e.g. `selectPosition()` calls it), but tests shouldn't need it. The `protected` modifier enforces this — entity-specific methods like `selectPosition()` are the public API. - -**Why store `listPath` in the base?** - -`verifySubmissionSuccess()` checks whether the URL changed to the list page after submit. Without `listPath`, every subclass would have to duplicate this redirect-detection logic. - -**Why `index + 1` in `getRow()`?** - -Angular Material renders `` as the first `mat-row` in the DOM. If you use `nth(0)`, you click the column headers. The `+1` offset is invisible to tests but essential for correctness. Centralizing it in the base class means you only have to remember this once. - ---- - -## 🔗 Adding a New Entity Page Object - -To add `DepartmentListPage` and `DepartmentFormPage`: - -**List page** (a two-line class): -```typescript -import { Page } from '@playwright/test'; -import { BaseListPage } from './base-list.page'; - -export class DepartmentListPage extends BaseListPage { - constructor(page: Page) { - super(page, '/departments', 'departments'); - } -} -``` - -**Form page** (add the department-specific fields): -```typescript -import { Page, Locator } from '@playwright/test'; -import { BaseFormPage } from './base-form.page'; - -export class DepartmentFormPage extends BaseFormPage { - readonly nameInput: Locator; - - constructor(page: Page) { - super(page, '/departments'); - this.nameInput = page.locator('input[formControlName="name"]'); - } - - async fillName(name: string) { - await this.nameInput.fill(name); - } -} -``` - -All pagination, search, permission checks, form submission, and success verification come from the base classes — for free. - ---- - -## 🌟 Why This Matters - -The Page Object Model is the most impactful investment you can make in a test suite's long-term maintainability. A selector change in a component template is a one-file update in the Page Object — not a search-and-replace across twenty test files. The `BaseListPage` / `BaseFormPage` hierarchy means common actions like "click edit", "verify table has rows", and "submit form" are written once and inherited everywhere. - -The two-level inheritance pattern — abstract base with generic actions, concrete page with feature-specific selectors — applies to any Angular Material application. Any team that writes more than five Playwright tests benefits from this structure immediately. - -**Transferable skills:** - -* **Abstract base page classes** — Applicable to any Playwright test suite for Angular Material applications -* **Two-level Page Object inheritance** — Foundation for sharing generic list/form actions across all feature pages -* **`formControlName` selectors** — Pattern for reliably targeting Angular reactive form fields regardless of DOM structure - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #playwright #angular #angularmaterial #testautomation #pageobjectmodel #e2etesting #typescript #webdevelopment #qa #testing #softwaredevelopment #dotnet #oauth2 #fullstack diff --git a/blogs/series-4-playwright-testing/4.3-playwright-role-based-testing.md b/blogs/series-4-playwright-testing/4.3-playwright-role-based-testing.md deleted file mode 100644 index a8f32b1..0000000 --- a/blogs/series-4-playwright-testing/4.3-playwright-role-based-testing.md +++ /dev/null @@ -1,592 +0,0 @@ -# One Feature, Three Users: Testing Role-Based Access Control with Playwright - -## How to Verify That Employees See Read-Only Views, Managers Can Create, and HRAdmins Can Delete — Without Guessing - -Role-based access control (RBAC) is one of the hardest things to test with confidence. Unit tests can verify that a guard function returns `false` for the wrong role. But only an E2E test can verify that: - -* The **Create button is invisible** to an Employee but visible to a Manager -* A Manager who **navigates directly to `/positions/create`** gets redirected — even though the "Add Position" button is visible in the UI -* An HRAdmin sees **Delete buttons** that no other role sees -* Switching users mid-session actually **clears the previous role** and applies the new one - -This article walks through the RBAC test suite in the **AngularNetTutorial** project — three `test.describe` blocks (one per role), a cross-role verification test, and two workflow tests that simulate real daily tasks. After reading this, you'll know exactly what to test and how to structure it. - -![Employee List RBAC](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-list-page.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Playwright tests verify role-based access control across three user roles.** - ---- - -## 📚 What You'll Learn - -* The three roles in AngularNetTutorial — Employee, Manager, HRAdmin — and their permission matrix -* How Angular implements RBAC at two layers: route guards and template directives -* Why template visibility and route guards need to be tested **separately** -* The `test.describe` nesting pattern for role-organized tests -* Walking through Employee, Manager, and HRAdmin test blocks test by test -* The cross-role verification test — login as all three roles in sequence -* The Position RBAC edge case: button visible but route guarded -* Workflow tests that simulate real daily tasks - ---- - -## 🔐 The Three Roles - -The app has three test users, each representing a different access level: - -**Employee** (username: `antoinette16`) -* Read-only access to all modules -* Can view employee list, department list, dashboard -* Cannot see Create, Edit, or Delete buttons anywhere - -**Manager** (username: `rosamond33`) -* Can create and edit employees and departments -* Can view positions and salary ranges -* Cannot delete records -* Cannot create or edit positions and salary ranges (route guarded) - -**HRAdmin** (username: `ashtyn1`) -* Full access to all modules -* Can create, edit, and delete everything -* Sees all action buttons - -In `config/test-users.json`, these permissions are declared explicitly: - -```json -{ - "employee": { "canCreate": false, "canEdit": false, "canDelete": false }, - "manager": { "canCreate": true, "canEdit": true, "canDelete": false }, - "hradmin": { "canCreate": true, "canEdit": true, "canDelete": true } -} -``` - ---- - -## 🏗️ How Angular Enforces RBAC - -The app uses two independent layers of access control. Testing both matters. - -### Layer 1 — Route Guards - -Angular route guards prevent navigation to protected routes. The positions module uses `hrAdminGuard`: - -``` -/positions — no guard (all authenticated users can view the list) -/positions/create — hrAdminGuard (only HRAdmin can reach this route) -/positions/edit/:id — hrAdminGuard (only HRAdmin can edit) -``` - -When a Manager navigates to `/positions/create`, the guard redirects them before the page renders. The test verifies this by checking the URL after navigation. - -### Layer 2 — Template Directives (`*appHasRole`) - -The template uses structural directives to show/hide UI elements based on role: - -```html - - - - - -``` - -This creates an important nuance: a Manager **sees the "Add Position" button** (template allows it), but clicking it navigates to `/positions/create`, which the **route guard blocks**. Both layers need tests. - ---- - -## 🗂️ Test File Structure - -The RBAC tests are organized into two files: - -**`tests/auth/role-based-access.spec.ts`** — General RBAC tests for all modules: - -```typescript -test.describe('Role-Based Access Control', () => { - test.describe('Employee Role', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'employee'); - }); - // ... employee tests - }); - - test.describe('Manager Role', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'manager'); - }); - // ... manager tests - }); - - test.describe('HRAdmin Role', () => { - test.beforeEach(async ({ page }) => { - await loginAsRole(page, 'hradmin'); - }); - // ... hradmin tests - }); - - test.describe('Cross-Role Verification', () => { - // Tests that switch between roles within a single test - }); -}); -``` - -**`tests/position-management/position-rbac.spec.ts`** — Detailed RBAC tests specific to the positions module, including the route guard / template directive distinction. - ---- - -## 👤 Employee Role Tests - -The `beforeEach` logs in as `antoinette16`. Every test in this block starts with an authenticated Employee session. - -### Verify the list is accessible - -```typescript -test('should allow Employee to view employee list', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const employeeTable = page.locator('table, mat-table'); - await expect(employeeTable.first()).toBeVisible({ timeout: 5000 }); -}); -``` - -Employees should be able to see data — just not modify it. - -### Verify no Create button - -```typescript -test('should NOT show Create button to Employee', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const createButton = page.locator('button').filter({ hasText: /create|add.*employee|new/i }); - const hasCreateButton = await createButton.isVisible({ timeout: 2000 }).catch(() => false); - - expect(hasCreateButton).toBe(false); -}); -``` - -The `catch(() => false)` pattern handles the case where the locator throws (no elements found). The assertion is the same in both cases: the button must not be visible. - -### Verify no Edit or Delete buttons - -```typescript -test('should NOT show edit buttons to Employee', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const editButtons = page.locator('button').filter({ hasText: /edit/i }); - const hasEditButtons = await editButtons.first().isVisible({ timeout: 2000 }).catch(() => false); - - expect(hasEditButtons).toBe(false); -}); -``` - -A matching Delete test follows the same pattern. - -### Verify direct URL access is blocked - -```typescript -test('should NOT allow Employee to access create form', async ({ page }) => { - // Bypass the UI and navigate directly - await page.goto('/employees/create'); - await page.waitForLoadState('networkidle'); - - const isOnCreatePage = page.url().includes('employees/create'); - const accessDenied = await page.locator( - 'text=/access.*denied|forbidden|unauthorized/i' - ).isVisible({ timeout: 2000 }).catch(() => false); - - expect(!isOnCreatePage || accessDenied).toBe(true); -}); -``` - -This tests a security concern: what happens when a savvy user types `/employees/create` directly into the browser? The app should redirect them or show an access denied message. The assertion `!isOnCreatePage || accessDenied` passes if either condition holds — the route guard redirected them, or an access denied message appeared. - ---- - -## 👔 Manager Role Tests - -### Can create and edit - -```typescript -test('should allow Manager to create employees', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const createButton = page.locator('button').filter({ hasText: /create|add.*employee|new/i }); - await expect(createButton.first()).toBeVisible({ timeout: 3000 }); -}); - -test('should allow Manager to edit employees', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const editButtons = page.locator('button, a').filter({ hasText: /edit/i }); - const hasEditButtons = await editButtons.first().isVisible({ timeout: 2000 }).catch(() => false); - - expect(hasEditButtons).toBe(true); -}); -``` - -### Cannot delete - -```typescript -test('should NOT show delete buttons to Manager', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const deleteButtons = page.locator('button').filter({ hasText: /delete/i }); - const hasDeleteButtons = await deleteButtons.first().isVisible({ timeout: 2000 }).catch(() => false); - - expect(hasDeleteButtons).toBe(false); -}); -``` - -This is the test that confirms the hierarchy: Manager can create and edit, but only HRAdmin can delete. - -### Positions page — the route guard edge case - -```typescript -test('should not allow Manager to create positions (route guarded)', async ({ page }) => { - await loginAsRole(page, 'manager'); - - // Attempt direct navigation to the guarded route - await page.goto('/positions/create'); - await page.waitForLoadState('networkidle'); - - const isOnCreatePage = page.url().includes('/positions/create'); - const accessDenied = await page.locator( - 'text=/access.*denied|forbidden|unauthorized|no.*permission/i' - ).isVisible({ timeout: 2000 }).catch(() => false); - - expect(!isOnCreatePage || accessDenied).toBe(true); -}); -``` - -```typescript -test('should show edit but not delete buttons for Manager on positions list', async ({ page }) => { - await loginAsRole(page, 'manager'); - const list = new PositionListPage(page); - await list.goto(); - - // Manager sees Edit (*appHasRole="['HRAdmin', 'Manager']") - const hasEditButtons = await list.hasEditPermission(); - // Manager does NOT see Delete (*appHasRole="['HRAdmin']") - const hasDeleteButtons = await list.hasDeletePermission(); - - expect(hasEditButtons).toBe(true); - expect(hasDeleteButtons).toBe(false); -}); -``` - -This test captures the nuance: the template shows Edit to Managers, but not Delete. Both directions need explicit assertions. - -### Menu visibility - -```typescript -test('should show Manager appropriate menu items', async ({ page }) => { - await page.goto('/dashboard'); - await page.waitForLoadState('networkidle'); - - const employeesMenu = page.locator('a, mat-list-item, button').filter({ hasText: /employees/i }); - const departmentsMenu = page.locator('a, mat-list-item, button').filter({ hasText: /departments/i }); - - const hasEmployees = await employeesMenu.first().isVisible({ timeout: 5000 }).catch(() => false); - const hasDepartments = await departmentsMenu.first().isVisible({ timeout: 5000 }).catch(() => false); - - expect(hasEmployees || hasDepartments).toBe(true); - - // Positions should NOT appear in the sidebar for Manager - const positionsMenu = page.locator('a, mat-list-item').filter({ hasText: /^positions$/i }); - const hasPositions = await positionsMenu.isVisible({ timeout: 2000 }).catch(() => false); - - expect(hasPositions).toBe(false); -}); -``` - -Sidebar navigation items are controlled by the same `*appHasRole` directive. This verifies that the Positions link doesn't appear for Manager — they can view the positions list directly, but it's not a menu item they're expected to use. - ---- - -## 🛡️ HRAdmin Role Tests - -### Full access to all modules - -```typescript -test('should manage all modules as HRAdmin', async ({ page }) => { - await loginAsRole(page, 'hradmin'); - - const modules = [ - { name: 'employees', path: '/employees' }, - { name: 'departments', path: '/departments' }, - { name: 'positions', path: '/positions' }, - { name: 'salary-ranges', path: '/salary-ranges' }, - ]; - - for (const module of modules) { - await page.goto(module.path); - await page.waitForLoadState('networkidle'); - - const accessDenied = await page.locator( - 'text=/access.*denied|forbidden|unauthorized/i' - ).isVisible({ timeout: 2000 }).catch(() => false); - - const hasTable = await page.locator('table, mat-table') - .isVisible({ timeout: 3000 }).catch(() => false); - - // Must have table and not have access denied - expect(!accessDenied || hasTable).toBe(true); - } -}); -``` - -This loop visits every module and verifies none of them shows an access denied message. A single test covers all four modules. - -### Delete buttons are visible - -```typescript -test('should allow HRAdmin to delete records', async ({ page }) => { - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const deleteButtons = page.locator('button').filter({ hasText: /delete/i }); - const hasDeleteButtons = await deleteButtons.first().isVisible({ timeout: 2000 }).catch(() => false); - - expect(hasDeleteButtons).toBe(true); -}); -``` - -### Full positions access via Page Object - -```typescript -test('should allow HRAdmin full access to positions', async ({ page }) => { - await loginAsRole(page, 'hradmin'); - const list = new PositionListPage(page); - await list.goto(); - - await expect(list.pageTitle.first()).toBeVisible({ timeout: 5000 }); - - const canCreate = await list.hasCreatePermission(); - expect(canCreate).toBe(true); - - const rowCount = await list.getRowCount(); - if (rowCount > 0) { - const hasEditButton = await list.hasEditPermission(); - const hasDeleteButton = await list.hasDeletePermission(); - expect(hasEditButton || hasDeleteButton).toBe(true); - } -}); -``` - -Using the `PositionListPage` Page Object (itself an extension of `BaseListPage`) keeps this test clean — `hasCreatePermission()`, `hasEditPermission()`, and `hasDeletePermission()` all resolve to single method calls. - ---- - -## 🔄 Cross-Role Verification Test - -The most important RBAC test in the suite: - -```typescript -test('should enforce different permissions across roles', async ({ page }) => { - // Step 1: Login as Employee - await loginAsRole(page, 'employee'); - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const employeeHasCreate = await page.locator('button') - .filter({ hasText: /create|add.*employee/i }) - .isVisible({ timeout: 3000 }).catch(() => false); - - // Step 2: Switch to Manager (MUST logout first) - await logout(page); - await page.waitForTimeout(1000); - await loginAsRole(page, 'manager'); - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const managerHasCreate = await page.locator('button') - .filter({ hasText: /create|add.*employee/i }) - .isVisible({ timeout: 5000 }).catch(() => false); - - // Step 3: Switch to HRAdmin - await logout(page); - await page.waitForTimeout(1000); - await loginAsRole(page, 'hradmin'); - await page.goto('/employees'); - await page.waitForLoadState('networkidle'); - - const adminHasCreate = await page.locator('button') - .filter({ hasText: /create|add.*employee/i }) - .isVisible({ timeout: 5000 }).catch(() => false); - - const adminHasEditIcons = await page.locator( - 'button mat-icon:has-text("edit"), button:has(mat-icon):has-text("edit")' - ).first().isVisible({ timeout: 3000 }).catch(() => false); - - // Assert the permission hierarchy - expect(employeeHasCreate).toBe(false); // Employee: no create - expect(managerHasCreate).toBe(true); // Manager: can create - expect(adminHasCreate).toBe(true); // HRAdmin: can create - expect(adminHasEditIcons).toBe(true); // HRAdmin: edit icons visible -}); -``` - -**Why this test matters:** It's possible to have three individual tests pass (one per role) while still having a bug where switching roles doesn't clear the previous session. This test exercises the full lifecycle — login, assert, logout, login as different role, assert again — in one session. - -**Why `await logout(page)` before each `loginAsRole()`:** The Angular app stores the OIDC session in browser storage. Without logging out, the previous role's tokens and permissions might persist. The `loginAs()` fixture checks for existing authentication and clears tokens, but the explicit logout provides an extra safety net. - ---- - -## 🗓️ Workflow Tests: Real RBAC in Action - -Beyond permission checks, the test suite includes two workflow tests that simulate real daily tasks. - -### Manager Daily Tasks - -`tests/workflows/manager-daily-tasks.spec.ts` simulates a Manager's full workday: - -```typescript -test('should complete typical manager daily workflow', async ({ page }) => { - test.setTimeout(60000); // Complex workflow needs more time - - // Step 1: Login as Manager - await loginAsRole(page, 'manager'); - - // Step 2: Review employee list - await page.goto('/employees'); - const employeeTable = page.locator('table, mat-table'); - await expect(employeeTable.first()).toBeVisible(); - - // Step 3: Create new employee using EmployeeFormPage - const employeeForm = new EmployeeFormPage(page); - const createButton = page.locator('button').filter({ hasText: /create|add.*employee|new/i }); - await createButton.first().click(); - - const newEmployeeData = createEmployeeData({ - firstName: 'ManagerTask', lastName: `New${Date.now()}`, - }); - - await employeeForm.fillForm({ - firstName: newEmployeeData.firstName, - lastName: newEmployeeData.lastName, - email: newEmployeeData.email, - department: 1, position: 1, - }); - - await employeeForm.submit(); - const result = await employeeForm.verifySubmissionSuccess(); - expect(result.success).toBe(true); - - // Step 4: Update existing employee (phone number change) - // ... navigate back, find employee, edit phone ... - - // Step 5: Create new department - // ... navigate to /departments, fill form, submit ... - - // Step 7: Logout and verify - await logout(page); - const authenticated = await isAuthenticated(page); - expect(authenticated).toBe(false); -}); -``` - -This test uses `createEmployeeData()` from `data.fixtures.ts` to generate unique test data — a factory function that produces realistic employee records with random phone numbers, dates, and employee numbers. - -### HRAdmin Operations - -`tests/workflows/hradmin-operations.spec.ts` simulates HRAdmin's administrative tasks — creating a salary range, a position linked to that range, and an employee in the new position: - -```typescript -test('should complete full HRAdmin workflow with relationships', async ({ page }) => { - test.setTimeout(60000); - - await loginAsRole(page, 'hradmin'); - - // Create salary range (direct selectors — no POM for salary ranges yet) - await page.goto('/salary-ranges'); - const salaryData = createSalaryRangeData({ name: `HRAdmin_Range_${Date.now()}` }); - await nameInput.fill(salaryData.name); - await minSalaryInput.fill(salaryData.minSalary.toString()); - await maxSalaryInput.fill(salaryData.maxSalary.toString()); - // ... - - // Create position using PositionFormPage - const positionForm = new PositionFormPage(page); - const positionData = createPositionData({ title: `HRAdminPosition_${Date.now()}` }); - await positionForm.fillForm({ title: positionData.title, description: positionData.description }); - await positionForm.submit(); - expect((await positionForm.verifySubmissionSuccess()).success).toBe(true); - - // Create employee in the new position using EmployeeFormPage - const employeeForm = new EmployeeFormPage(page); - await employeeForm.fillForm({ ... }); - await positionSelect.click(); // Select the most recently created position - await positionOptions.last().click(); - await employeeForm.submit(); - expect((await employeeForm.verifySubmissionSuccess()).success).toBe(true); - - // Verify the employee appears in the list - await page.goto('/employees'); - // Search and click to open detail ... - - // Logout and verify clean state - await logout(page); - const authenticated = await isAuthenticated(page); - expect(authenticated).toBe(false); -}); -``` - -This workflow test also validates **data relationships** — the employee is created in a specific position, and the test then opens the employee record to verify the position is assigned. - ---- - -## 🔑 Key Design Decisions - -**Test RBAC at two layers independently.** Template directives hide buttons. Route guards block navigation. A bug in either layer is a security issue. Test both: "is the button visible?" and "does direct URL navigation get blocked?" - -**Use `isVisible({ timeout }).catch(() => false)` for absence checks.** When you're asserting that something should NOT be visible, the locator might find zero elements and throw. The `.catch(() => false)` makes absence and invisibility equivalent — both return `false`. - -**Always logout between role switches.** OIDC tokens persist in browser storage. Without clearing them, switching roles can leave the previous role's permissions active. The `loginAs()` fixture calls `clearAuthTokens()`, but an explicit `logout()` ensures the Angular app also transitions to Guest state. - -**Use `test.setTimeout()` for workflow tests.** A workflow that logs in, creates three records, and logs out can easily take 30–60 seconds. The default Playwright timeout is typically 30 seconds. Add `test.setTimeout(60000)` at the start of complex workflow tests. - -**Nest `test.describe` by role.** Using `test.describe('Employee Role', () => { test.beforeEach(...login as employee...) })` means the login happens once per test, the role context is clear from the block name, and you never accidentally run an Employee test with Manager credentials. - ---- - -## 🌟 Why This Matters - -Role-based access control is one of the hardest things to test with confidence. Unit tests can verify that a guard function returns `false` — but only E2E tests can verify that the right buttons appear for the right users, that direct URL navigation is blocked, and that switching users mid-session actually clears the previous role. The three-`describe`-block pattern makes this systematic. - -The cross-role test — login as Employee, assert no Create button; switch to Manager, assert Create button appears; switch to HRAdmin, assert Delete button appears — is the test that proves the RBAC system as a whole is working, not just individual pieces in isolation. - -**Transferable skills:** - -* **Role-separated `test.describe` blocks** — Applicable to any Playwright test suite for applications with multiple user roles -* **Programmatic role switching with explicit logout** — Foundation for testing state transitions between different user sessions -* **UI permission verification at two layers** — Pattern for testing both template directive visibility and route guard enforcement independently - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #playwright #angular #rbac #accesscontrol #testautomation #e2etesting #typescript #oauth2 #identityserver #rolebased #security #webdevelopment #qa #testing #softwaredevelopment diff --git a/blogs/series-4-playwright-testing/4.4-playwright-jwt-token-testing.md b/blogs/series-4-playwright-testing/4.4-playwright-jwt-token-testing.md deleted file mode 100644 index 42dd73a..0000000 --- a/blogs/series-4-playwright-testing/4.4-playwright-jwt-token-testing.md +++ /dev/null @@ -1,568 +0,0 @@ -# How to Extract and Verify JWT Tokens in Playwright Tests - -## Decode the Token, Check the Claims, and Confirm the Right Scopes Are Granted - -Your login test goes green. The user lands on the dashboard. But how do you know the token issued by IdentityServer actually contains the right roles and scopes? - -You could inspect it manually in a browser. But that doesn't scale — and it doesn't catch regressions when IdentityServer configuration changes. What you want is a test that: - -1. Logs in through the real OIDC flow -2. Extracts the actual JWT access token -3. Decodes it (no library required — one `Buffer.from()` call) -4. Asserts that `Manager` role is present, `exp` is in the future, and the API scope is included - -This article walks through the JWT token testing suite in **AngularNetTutorial** — two extraction approaches, the decode pattern, and the claims tests for all three roles. - -![Auth Token Flow](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-list-page.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Playwright tests extract real JWT tokens from a running Angular app and verify their contents.** - ---- - -## 📚 What You'll Learn - -* The JWT structure — header, payload, signature — and why the payload can be decoded without the secret -* Two extraction methods: reading from browser storage vs. navigating to the Profile page -* How to decode a JWT in Node.js with one `Buffer.from()` call — no library needed -* Which claims to assert: `sub`, `exp`, `iat`, `iss`, `aud`, `role`, `scope` -* Verifying the token is not expired -* Testing that different roles produce different tokens with different claims -* Token tamper detection: swapping the signature -* Using the extracted token with Playwright's `request` fixture for direct API calls - ---- - -## 🔑 Why Test the Token Contents? - -The login test proves the user can authenticate. The token claims tests prove the authentication server issued the **correct token** for that user. - -Claims tests catch a specific class of bugs: - -* IdentityServer configuration changed and the `role` claim is now missing -* A new scope was added to the API but not to the client's `AllowedScopes` -* Token expiration was accidentally set to 0 -* The `aud` (audience) claim no longer matches what the API expects -* The issuer URL changed (breaks `.NET` JWT validation) - -None of these show up as a login failure — the user lands on the dashboard just fine. But the API calls start returning 401. - ---- - -## 🧩 JWT Structure - -A JWT has three base64url-encoded sections separated by dots: - -``` -eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 ← Header (algorithm + type) -. -eyJzdWIiOiIxMjM0NTYiLCJyb2xlIjoiTWFuYWdlciIsInNjb3BlIjoiLi4uIn0 ← Payload (claims) -. -SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature (server-signed, unreadable without key) -``` - -The **payload** contains the claims — and it's just base64 encoded, not encrypted. Anyone who has the token can read the claims. The **signature** is what proves the token was issued by your IdentityServer and hasn't been tampered with. - -In tests, you only need to decode the payload to verify claims: - -```typescript -const parts = token.split('.'); // ['header', 'payload', 'signature'] -const payload = JSON.parse( - Buffer.from(parts[1], 'base64').toString() -); -// payload is now a plain JS object with all the claims -``` - -No `jsonwebtoken` package, no `atob()` shim — just Node.js built-ins. - ---- - -## 🔍 Two Token Extraction Methods - -The `auth.fixtures.ts` file provides two functions for getting the token after login. - -### Method 1 — `getStoredToken()`: Read from Browser Storage - -After a successful OIDC login, the `angular-oauth2-oidc` library stores the access token in `sessionStorage` under the key `access_token`: - -```typescript -export async function getStoredToken(page: Page): Promise { - // Try sessionStorage first (angular-oauth2-oidc stores tokens here by default) - const sessionStorageToken = await page.evaluate(() => { - // Direct key lookup - let token = sessionStorage.getItem('access_token'); - if (token && token.startsWith('eyJ')) return token; - - // Fallback: search for keys containing 'access_token' or 'oidc' - const keys = Object.keys(sessionStorage); - for (const key of keys) { - if (key.includes('access_token') || key.includes('oidc')) { - const value = sessionStorage.getItem(key); - if (value?.startsWith('eyJ')) return value; - // Try as JSON wrapper (some OIDC libraries wrap tokens) - try { - const parsed = JSON.parse(value!); - if (parsed.access_token) return parsed.access_token; - } catch { } - } - } - return null; - }); - - // Try localStorage as fallback - if (!sessionStorageToken) { /* similar search in localStorage */ } - - return sessionStorageToken; -} -``` - -**How it works:** After login, Playwright calls `page.evaluate()` to run code inside the browser's JavaScript context — exactly as if you opened DevTools and typed in the console. The function reads `sessionStorage.getItem('access_token')` and returns the JWT string. - -**When to use:** Fast — no extra page navigation required. Use this when you want the token immediately after login. - -### Method 2 — `getTokenFromProfile()`: Navigate to Profile Page - -The Angular app has a Profile page that displays the ID token and access token. This is the approach used for most of the claims tests because it reliably extracts the token even when the storage key name varies: - -```typescript -export async function getTokenFromProfile(page: Page): Promise { - // Click user icon → Profile → Access Token tab → Show Raw Token - const userIcon = page.locator( - 'button[aria-label="User menu"], header button:has(mat-icon)' - ).last(); - await userIcon.click(); - - const profileOption = page.locator( - 'button:has-text("Profile"), [role="menuitem"]:has-text("Profile")' - ).first(); - await profileOption.click(); - await page.waitForLoadState('networkidle'); - - const accessTokenTab = page.locator( - '[role="tab"]:has-text("Access Token")' - ).first(); - await accessTokenTab.click(); - - const showTokenButton = page.locator('button:has-text("Show Raw Token")').first(); - await showTokenButton.click(); - await page.waitForTimeout(1000); - - // Extract JWT pattern: eyJ[base64].[base64].[base64] - const pageContent = await page.content(); - const tokenMatches = pageContent.match( - /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g - ); - - // Return the longest match — access tokens are longer than ID tokens - if (tokenMatches) { - return tokenMatches.sort((a, b) => b.length - a.length)[0]; - } - return null; -} -``` - -**When to use:** More robust — works regardless of which storage key the OIDC library uses. Use this when claims accuracy is more important than speed. - -### The Diagnostic Test: Comparing Both Methods - -`tests/diagnostic-token-storage.spec.ts` was written to discover which approach works in the specific environment: - -```typescript -test('Compare token extraction: localStorage vs Profile page', async ({ page }) => { - await loginAsRole(page, 'manager'); - - const storedToken = await getStoredToken(page); - const profileToken = await getTokenFromProfile(page); - - console.log('Both methods returned tokens:', !!storedToken && !!profileToken); - console.log('Tokens are the same:', storedToken === profileToken); - - // At least one method should work - expect(storedToken || profileToken).toBeTruthy(); -}); -``` - -Run this first in a new environment. The console output tells you which method works and whether the two methods return the same token. - ---- - -## 🧪 Core Claims Tests - -All claims tests follow the same pattern: - -```typescript -test('should include proper claims in token', async ({ page }) => { - await loginAsRole(page, 'manager'); - const token = await getTokenFromProfile(page); - - expect(token).toBeTruthy(); - - // Decode JWT payload - const parts = token!.split('.'); - expect(parts.length).toBe(3); // Must be exactly 3 parts - - const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); -``` - -Every test starts here. Three assertions before we even look at claims: - -* `token` is truthy — extraction worked -* `parts.length === 3` — it's a valid JWT structure -* `Buffer.from` succeeds — payload is valid base64 - -### Subject and Timing Claims - -```typescript -// Subject — the unique user identifier -expect(payload.sub || payload.userId || payload.nameid).toBeDefined(); - -// Expiration — token must have an expiry -expect(payload.exp).toBeDefined(); - -// Issued at or Not before -expect(payload.iat || payload.nbf).toBeDefined(); - -// Token must not be expired -const now = Math.floor(Date.now() / 1000); // Unix timestamp in seconds -expect(payload.exp).toBeGreaterThan(now); -``` - -The `payload.exp` check is the most important of these. JWT times are Unix timestamps (seconds since epoch). The `.exp > now` assertion catches tokens that were accidentally issued with a zero lifetime or a past expiration date. - -### Issuer Claim - -```typescript -test('should have proper token issuer claim', async ({ page }) => { - await loginAsRole(page, 'manager'); - const token = await getTokenFromProfile(page); - const payload = JSON.parse(Buffer.from(token!.split('.')[1], 'base64').toString()); - - // iss must match the running IdentityServer URL - expect(payload.iss).toBeDefined(); - expect(payload.iss).toContain(new URL(APP_URLS.identityServer).host); -}); -``` - -The `.NET` API validates the `iss` claim against its configured `Authority`. If the IdentityServer URL changes (e.g. from `localhost:44310` to a production domain), this test catches the mismatch before it causes 401 errors in production. - -### Audience Claim - -```typescript -test('should have proper token audience claim', async ({ page }) => { - await loginAsRole(page, 'manager'); - const token = await getTokenFromProfile(page); - const payload = JSON.parse(Buffer.from(token!.split('.')[1], 'base64').toString()); - - if (payload.aud) { - // aud can be a string or array of strings - const audienceString = JSON.stringify(payload.aud); - expect( - audienceString.includes('api') || - audienceString.includes('talentmanagement') || - audienceString.includes('app.api') - ).toBe(true); - } -}); -``` - -The `aud` claim identifies which API this token is for. The `.NET` API checks that the token's audience matches `app.api.talentmanagement.read` or `app.api.talentmanagement.write`. - -### Role Claims by Role - -The most valuable claim tests verify that each role gets the right `role` value in the token: - -```typescript -test('should include role/scope claims for Manager', async ({ page }) => { - await loginAsRole(page, 'manager'); - const token = await getTokenFromProfile(page); - const payload = JSON.parse(Buffer.from(token!.split('.')[1], 'base64').toString()); - - const hasRoleClaim = payload.role || payload.roles || payload.scope; - expect(hasRoleClaim).toBeDefined(); - - const rolesString = JSON.stringify(payload); - expect( - rolesString.includes('Manager') || - rolesString.includes('manager') || - rolesString.includes('write') || - rolesString.includes('app.api.talentmanagement') - ).toBe(true); -}); -``` - -```typescript -test('should include role/scope claims for HRAdmin', async ({ page }) => { - await loginAsRole(page, 'hradmin'); - const token = await getTokenFromProfile(page); - const payload = JSON.parse(Buffer.from(token!.split('.')[1], 'base64').toString()); - - const rolesString = JSON.stringify(payload); - expect( - rolesString.includes('HRAdmin') || - rolesString.includes('admin') || - rolesString.includes('delete') || - rolesString.includes('app.api.talentmanagement') - ).toBe(true); -}); -``` - -```typescript -test('should include role/scope claims for Employee', async ({ page }) => { - await loginAsRole(page, 'employee'); - const token = await getTokenFromProfile(page); - const payload = JSON.parse(Buffer.from(token!.split('.')[1], 'base64').toString()); - - const rolesString = JSON.stringify(payload); - expect( - rolesString.includes('Employee') || - rolesString.includes('employee') || - rolesString.includes('read') || - rolesString.includes('app.api.talentmanagement') - ).toBe(true); -}); -``` - -**Why `JSON.stringify(payload)`?** The role claim can appear as `payload.role` (string), `payload.roles` (array), or embedded in `payload.scope` (space-separated string). `JSON.stringify` converts all structures to a single searchable string, so `includes('Manager')` finds it regardless of how it's structured. - -### Scope Claims - -```typescript -test('should verify token has correct scopes for API access', async ({ page }) => { - await loginAsRole(page, 'hradmin'); - const token = await getTokenFromProfile(page); - const payload = JSON.parse(Buffer.from(token!.split('.')[1], 'base64').toString()); - - expect(payload.scope).toBeDefined(); - - // scope can be a string ('read write') or array (['read', 'write']) - const scopes = typeof payload.scope === 'string' - ? payload.scope.split(' ') - : payload.scope; - - const hasApiScope = scopes.some((scope: string) => - scope.includes('app.api.talentmanagement') || - scope.includes('talentmanagement') - ); - - expect(hasApiScope).toBe(true); -}); -``` - -The scopes in the token must match what the `.NET` API's `[Authorize]` attributes require. IdentityServer's `AllowedScopes` on the client, Angular's `scope` in `environment.ts`, and the API's policy configuration must all agree. - ---- - -## 🔄 Different Roles, Different Tokens - -```typescript -test('should use different tokens for different roles', async ({ page }) => { - // Get Manager token - await loginAsRole(page, 'manager'); - const managerToken = await getTokenFromProfile(page); - - await logout(page); - - // Get Employee token - await loginAsRole(page, 'employee'); - const employeeToken = await getTokenFromProfile(page); - - // Tokens must be different (different sub + different claims) - expect(managerToken).toBeTruthy(); - expect(employeeToken).toBeTruthy(); - expect(managerToken).not.toBe(employeeToken); - - // Decode and compare role claims - const managerPayload = JSON.parse( - Buffer.from(managerToken!.split('.')[1], 'base64').toString() - ); - const employeePayload = JSON.parse( - Buffer.from(employeeToken!.split('.')[1], 'base64').toString() - ); - - expect(managerPayload.role).toContain('Manager'); - expect(employeePayload.role).toContain('Employee'); -}); -``` - -This test catches a subtle bug: if role assignment in IdentityServer breaks, all users might receive the same claims regardless of their actual role. The token would look valid, login would succeed, but RBAC would stop working. - ---- - -## 🔧 Token Tamper Detection - -```typescript -test('should validate token signature', async ({ page, request }) => { - await loginAsRole(page, 'manager'); - const validToken = await getTokenFromProfile(page); - - // Tamper with the token: replace the signature with garbage - const parts = validToken!.split('.'); - const tamperedToken = `${parts[0]}.${parts[1]}.tamperedsignature`; - - const response = await request.get(`${APP_URLS.api}/employees`, { - headers: { - 'Authorization': `Bearer ${tamperedToken}`, - 'Accept': 'application/json', - }, - ignoreHTTPSErrors: true, - }); - - // When API auth is fully enabled: expect 401 - // In dev with anonymous access: expect 200 or 401 - expect([200, 401]).toContain(response.status()); -}); -``` - -A tampered token has a valid header and payload but an invalid signature. The `.NET` API's JWT validation middleware verifies the signature using IdentityServer's public key (fetched from `/.well-known/openid-configuration`). A tampered signature should always return 401. - -The dual assertion `expect([200, 401]).toContain(response.status())` acknowledges that the dev environment currently allows anonymous access to the API. When API authentication is enabled in production, this test should be updated to expect `401` only. - ---- - -## 📡 Using the Token for Direct API Calls - -Once you have the token, you can use Playwright's `request` fixture to call the API directly — bypassing the Angular UI entirely: - -```typescript -test('should validate token on API request', async ({ page, request }) => { - await loginAsRole(page, 'manager'); - const token = await getTokenFromProfile(page); - - const response = await request.get(`${APP_URLS.api}/employees`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json', - }, - ignoreHTTPSErrors: true, // Required for self-signed dev certs - }); - - expect(response.status()).toBe(200); - - const data = await response.json(); - expect(data).toBeDefined(); -}); -``` - -```typescript -test('should call API with HRAdmin token for full access', async ({ page, request }) => { - await loginAsRole(page, 'hradmin'); - const token = await getTokenFromProfile(page); - - const endpoints = [ - '/employees', - '/departments', - '/positions', - '/salaryranges', - ]; - - for (const endpoint of endpoints) { - const response = await request.get(`${APP_URLS.api}${endpoint}`, { - headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, - ignoreHTTPSErrors: true, - }); - - expect(response.status()).toBe(200); - } -}); -``` - -**Why `ignoreHTTPSErrors: true`?** The dev API uses a self-signed certificate at `https://localhost:44378`. Without this flag, Playwright would reject the connection. This is also set globally in `playwright.config.ts` so you typically don't need to set it per-request. - -The `request` fixture in Playwright is independent of the `page` — it's a bare HTTP client that doesn't use a browser context. You get the token from the browser (via `page`), then use it in API calls (via `request`). - ---- - -## ⚠️ The Password Grant Limitation - -The test file includes this note: - -```typescript -/** - * Note: These tests use the Profile Page approach because IdentityServer - * password grant is not configured for programmatic token acquisition - * (returns "unauthorized_client"). - */ -``` - -You might expect to get tokens by calling IdentityServer's `/connect/token` endpoint directly with `grant_type: password`. The `auth.fixtures.ts` file has `getApiToken()` for exactly this: - -```typescript -export async function getApiToken(request, username, password) { - const response = await request.post(`${APP_URLS.identityServer}/connect/token`, { - form: { - grant_type: 'password', - client_id: 'TalentManagement', - client_secret: 'secret', - scope: 'openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write', - username, - password, - }, - ignoreHTTPSErrors: true, - }); - // ... -} -``` - -However, the AngularNetTutorial IdentityServer is configured for **PKCE only** — the `TalentManagement` client doesn't allow the Resource Owner Password Credentials grant. That's intentional: PKCE is more secure and is the recommended flow for single-page applications. - -The result is that `getApiToken()` returns `unauthorized_client`. The Profile Page approach (`getTokenFromProfile()`) is the correct way to get a real token in this setup — it goes through the actual PKCE flow. - -**If you need password grant enabled**, the seed data file (`identityserverdata.json`) does not include it for the `TalentManagement` client. You can enable it at runtime via the IdentityServer Admin UI (`https://localhost:44303`): navigate to **Clients → TalentManagement → Basics → Allowed Grant Types** and add `password`. Note that this change is not persisted back to the seed file — if the database is recreated, the grant type will be lost and must be re-added. - -Before doing so, consider these design implications: - -* **Add `offline_access` to AllowedScopes** if you also need refresh tokens — the password flow requires it explicitly. -* **Password grant (Resource Owner Password Credentials) is generally discouraged for new applications.** It requires the client to handle user credentials directly, bypassing the security isolation that OAuth 2.0 is designed to provide. -* **Avoid mixing `authorization_code` and `password` on the same client.** The `TalentManagement` client is a browser SPA using `authorization_code` + PKCE. Adding `password` to it mixes trust levels. The better design is a separate machine/trusted client dedicated to programmatic token acquisition (e.g. for test automation or CI pipelines). - ---- - -## 🔑 Key Design Decisions - -**Decode without a library.** `Buffer.from(parts[1], 'base64').toString()` gives you the raw JSON. `JSON.parse()` gives you the claims object. No `jsonwebtoken`, `jwt-decode`, or other dependencies required. - -**Stringify before searching claims.** `JSON.stringify(payload)` converts the entire claims object to a string. Then `includes('Manager')` finds the role regardless of whether it's in `payload.role`, `payload.roles[0]`, or `payload.scope`. This makes tests resilient to IdentityServer claim structure variations. - -**Always check `payload.exp > now`.** Token expiry bugs are silent — the token looks valid, but every API call returns 401. This one assertion catches the entire class of misconfigured token lifetime bugs. - -**The `request` fixture is independent of the browser.** Login via `page`, get the token, then pass it to `request.get()`. This lets you test the API with real, browser-issued tokens without going through the Angular UI for each API call. - -**Test that different roles get different tokens.** If role assignment breaks in IdentityServer, everyone gets the same claims. This test catches that regression. - ---- - -## 🌟 Why This Matters - -JWT token testing fills the gap between "login works" and "the API will accept this token with the right permissions." When IdentityServer configuration changes — a missing `role` claim, a wrong audience, an expired token lifetime — these tests catch it before the Angular UI does. The `payload.exp > now` assertion alone prevents an entire class of silent authentication failures. - -The `Buffer.from(parts[1], 'base64').toString()` decode pattern works with any JWT from any OAuth 2.0 provider — Okta, Auth0, Azure AD, or any OIDC-compliant server. No library required, no lock-in. - -**Transferable skills:** - -* **Client-side JWT decoding with `Buffer.from()`** — Applicable to any Playwright test suite that needs to verify token contents from any OAuth 2.0 provider -* **Token claim assertions** — Foundation for verifying `sub`, `exp`, `iss`, `aud`, `role`, and `scope` claims in automated tests -* **Session storage token extraction** — Pattern for accessing OIDC tokens stored by `angular-oauth2-oidc` or similar libraries - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #playwright #jwt #oauth2 #identityserver #testautomation #e2etesting #typescript #security #webdevelopment #angular #dotnet #authentication #claims #oidc #apitoken diff --git a/blogs/series-4-playwright-testing/4.5-playwright-api-testing.md b/blogs/series-4-playwright-testing/4.5-playwright-api-testing.md deleted file mode 100644 index 8b251ff..0000000 --- a/blogs/series-4-playwright-testing/4.5-playwright-api-testing.md +++ /dev/null @@ -1,715 +0,0 @@ -# Skip the UI: Test Your .NET API Directly with Playwright's Request Fixture - -## Authenticate via Browser, Extract the Token, and Call API Endpoints Programmatically - -Every time you test a feature through the Angular UI, you're testing three things at once: the Angular component, the HTTP call, and the .NET API endpoint. That's great for end-to-end confidence — but it means a slow Playwright login flow before every API assertion. - -Playwright's `request` fixture gives you a bare HTTP client that shares nothing with a browser. No DOM, no Angular rendering, no routing. Just authenticated HTTP calls directly to `https://localhost:44378/api/v1`. - -The challenge in an OAuth 2.0 app is getting the token programmatically. This article shows how the **AngularNetTutorial** project solves it — by launching a temporary headless browser, completing the real OIDC flow, extracting the token, and then using it for direct API calls with full CRUD coverage. - -![API Testing](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-api-endpoints.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into how Playwright's `request` fixture enables direct .NET API testing with real OAuth 2.0 tokens.** - ---- - -## 📚 What You'll Learn - -* What the `request` fixture is and how it differs from the `page` fixture -* The `getTokenForRole()` pattern — launching a headless browser to acquire a real OAuth token -* The `api.fixtures.ts` helper library — `createEmployee()`, `deleteEmployee()`, `makeAuthenticatedRequest()` -* `test.beforeAll` for token acquisition and `test.afterEach` for cleanup -* Walking through Employee API tests: GET list, GET by ID, POST create, PUT update, DELETE -* Response structure handling — `data.data || data.items || data` -* Graceful skip when services aren't running -* Pagination and search query parameters -* Cache header inspection tests -* The `ignoreHTTPSErrors` requirement for self-signed dev certificates - ---- - -## 🔧 The `request` Fixture - -In Playwright, every test function receives fixtures as parameters. The most common is `page` — a full browser tab. The `request` fixture is different: it's a lightweight HTTP client with no browser attached. - -```typescript -test('should GET list of employees', async ({ request }) => { - // No browser, no DOM — just an HTTP client - const response = await request.get('https://localhost:44378/api/v1/employees', { - headers: { Authorization: `Bearer ${token}` }, - ignoreHTTPSErrors: true, - }); - expect(response.status()).toBe(200); -}); -``` - -The `request` fixture supports all HTTP methods: `request.get()`, `request.post()`, `request.put()`, `request.delete()`. Responses have `response.status()`, `response.json()`, `response.text()`, and `response.headers()`. - -You can use both fixtures in the same test: - -```typescript -test('combined: login via browser, call API directly', async ({ page, request }) => { - // page: full browser for OIDC login - await loginAsRole(page, 'manager'); - const token = await getTokenFromProfile(page); - - // request: bare HTTP client for API call - const response = await request.get(`${APP_URLS.api}/employees`, { - headers: { Authorization: `Bearer ${token}` }, - ignoreHTTPSErrors: true, - }); - expect(response.status()).toBe(200); -}); -``` - ---- - -## 🔑 getTokenForRole() — Headless Browser Token Acquisition - -The core challenge: API tests use the `request` fixture only (no browser), but the OAuth 2.0 PKCE flow requires a browser. The solution in `api.fixtures.ts` is `getTokenForRole()` — a function that spins up a temporary headless Chromium instance, completes the OIDC flow, extracts the token, then tears down the browser: - -```typescript -export async function getTokenForRole( - request: APIRequestContext, - role: 'employee' | 'manager' | 'hradmin' -): Promise { - // Spin up a temporary browser just for token acquisition - const browser = await chromium.launch(); - const context = await browser.newContext({ ignoreHTTPSErrors: true }); - const page = await context.newPage(); - - try { - // Complete the full OIDC browser flow - await loginAsRole(page, role); - - // Extract the access token from the Profile page - const token = await getTokenFromProfile(page); - - if (!token) { - throw new Error(`Failed to extract token for role: ${role}`); - } - - return token; - } finally { - // Always clean up — browser closes whether or not extraction succeeded - await context.close(); - await browser.close(); - } -} -``` - -**Why a temporary browser?** IdentityServer is configured for PKCE only — there's no Resource Owner Password Credentials grant available for programmatic token acquisition. The only way to get a real token is through the browser-based OIDC flow. `getTokenForRole()` wraps that complexity so API tests can call it as a simple function. - -**Why in `test.beforeAll`?** Token acquisition takes 10–15 seconds (full login flow). Running it once per test suite rather than once per test keeps the suite fast: - -```typescript -test.describe('Employee API', () => { - let authToken: string | null = null; - let authFailed = false; - - test.beforeAll(async ({ request }) => { - try { - // Race the token acquisition against a 25-second timeout - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Token acquisition timeout')), 25000) - ); - - authToken = await Promise.race([ - getTokenForRole(request, 'manager'), - timeoutPromise, - ]); - authFailed = false; - } catch (error) { - authFailed = true; - console.log('Failed to acquire auth token — services may not be running.'); - } - }); -``` - -The `Promise.race` with a timeout prevents the suite from hanging forever if IdentityServer isn't running. If acquisition fails, `authFailed` is set to `true` and every test skips gracefully: - -```typescript -test('should GET list of employees', async ({ request }) => { - if (authFailed || !authToken) test.skip(); - // ... rest of test -}); -``` - ---- - -## 🧹 test.afterEach for Cleanup - -API tests that create records must also delete them. Otherwise, each test run accumulates test data in the database. The `test.afterEach` hook handles this: - -```typescript -let testEmployeeId: number; - -test.afterEach(async ({ request }) => { - if (testEmployeeId && authToken) { - try { - await request.delete(`${baseURL}/employees/${testEmployeeId}`, { - headers: { Authorization: `Bearer ${authToken}` }, - ignoreHTTPSErrors: true, - }); - } catch { - // Ignore cleanup errors — don't fail the test because of cleanup - } - testEmployeeId = 0; // Reset for next test - } -}); -``` - -Tests that create an employee store the ID in `testEmployeeId`. `afterEach` deletes it regardless of whether the test passed or failed. - ---- - -## 📦 The api.fixtures.ts Helper Library - -Rather than writing raw HTTP calls in every test, `api.fixtures.ts` provides typed helpers: - -```typescript -// makeAuthenticatedRequest() — the internal wrapper -async function makeAuthenticatedRequest( - request: APIRequestContext, - token: string, - method: 'GET' | 'POST' | 'PUT' | 'DELETE', - endpoint: string, - data?: any -) { - const options = { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - ignoreHTTPSErrors: true, - ...(data ? { data } : {}), - }; - - switch (method) { - case 'GET': return await request.get(`${API_BASE_URL}${endpoint}`, options); - case 'POST': return await request.post(`${API_BASE_URL}${endpoint}`, options); - case 'PUT': return await request.put(`${API_BASE_URL}${endpoint}`, options); - case 'DELETE': return await request.delete(`${API_BASE_URL}${endpoint}`, options); - } -} -``` - -Built on top of this, the public helpers are strongly typed: - -```typescript -// Create an employee -export async function createEmployee( - request: APIRequestContext, token: string, data: EmployeeData -): Promise - -// Delete an employee (handles 404 gracefully) -export async function deleteEmployee( - request: APIRequestContext, token: string, id: number -): Promise - -// Get employee by ID -export async function getEmployee( - request: APIRequestContext, token: string, id: number -): Promise - -// Bulk cleanup: delete multiple IDs in parallel -export async function cleanupEmployees( - request: APIRequestContext, token: string, ids: number[] -): Promise -``` - -The `cleanupEmployees()` function uses `Promise.all` with `.catch(() => {})` on each deletion — cleanup errors are suppressed so a failed delete doesn't fail the test suite. - ---- - -## 🧪 Employee API Tests — Full CRUD Coverage - -### GET List - -```typescript -test('should GET list of employees', async ({ request }) => { - if (authFailed || !authToken) test.skip(); - - const response = await request.get(`${baseURL}/employees`, { - headers: { Authorization: `Bearer ${authToken}`, Accept: 'application/json' }, - ignoreHTTPSErrors: true, - }); - - expect(response.status()).toBe(200); - - const data = await response.json(); - expect(data).toBeDefined(); - - // Handle flexible response shapes - if (Array.isArray(data)) { - expect(data.length).toBeGreaterThanOrEqual(0); - } else if (data.data && Array.isArray(data.data)) { - expect(data.data.length).toBeGreaterThanOrEqual(0); - } else if (data.items && Array.isArray(data.items)) { - expect(data.items.length).toBeGreaterThanOrEqual(0); - } -}); -``` - -**Why three shape checks?** .NET Clean Architecture APIs can return data in different shapes depending on whether they use a wrapper (`{ data: [...] }`), pagination (`{ items: [...], totalCount: N }`), or a plain array. The three-branch check makes the test work regardless of the response structure. - -### POST Create → GET Verify Cycle - -```typescript -test('should POST create new employee with token', async ({ request }) => { - if (authFailed || !authToken) test.skip(); - - const employeeData = createEmployeeData({ - firstName: 'APITest', - lastName: `Create${Date.now()}`, - email: `api.create.${Date.now()}@example.com`, - }); - - const response = await request.post(`${baseURL}/employees`, { - headers: { - Authorization: `Bearer ${authToken}`, - 'Content-Type': 'application/json', - }, - ignoreHTTPSErrors: true, - data: employeeData, - }); - - // 201 Created (success) or 400 (validation failure — acceptable) - expect([201, 400]).toContain(response.status()); - - if (response.status() === 201) { - const created = await response.json(); - // Normalize the response shape - const createdEmployee = created.data || created.result || created; - - testEmployeeId = createdEmployee.id || createdEmployee.employeeId || created.id; - - if (testEmployeeId) { - expect(testEmployeeId).toBeGreaterThan(0); - if (createdEmployee.firstName) { - expect(createdEmployee.firstName).toBe(employeeData.firstName); - } - } - } -}); -``` - -`createEmployeeData()` from `data.fixtures.ts` generates a complete employee record with `Date.now()` suffixes on the name and email to ensure uniqueness across runs. - -### GET by ID - -```typescript -test('should GET employee by ID', async ({ request }) => { - if (authFailed || !authToken) test.skip(); - - // Create test employee first - const employeeData = createEmployeeData({ firstName: 'APITest', lastName: `GetById${Date.now()}` }); - - const createResponse = await request.post(`${baseURL}/employees`, { - headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' }, - ignoreHTTPSErrors: true, - data: employeeData, - }); - - const created = await createResponse.json(); - const createdData = created.data || created.result || created; - testEmployeeId = createdData.id || createdData.employeeId || created.id; - - if (!testEmployeeId || createResponse.status() !== 201) { - console.log('Employee creation failed, skipping GET test'); - return; // Soft skip: don't fail the test suite - } - - // Now GET the employee back - const response = await request.get(`${baseURL}/employees/${testEmployeeId}`, { - headers: { Authorization: `Bearer ${authToken}` }, - ignoreHTTPSErrors: true, - }); - - expect(response.status()).toBe(200); - - const employee = await response.json(); - const employeeData2 = employee.data || employee; - - expect(employeeData2.firstName).toBe(employeeData.firstName); - expect(employeeData2.lastName).toBe(employeeData.lastName); - expect(employeeData2.email).toBe(employeeData.email); -}); -``` - -This is the **POST → GET** cycle: create a record, retrieve it by the returned ID, verify the fields match. The `testEmployeeId` assignment ensures cleanup happens in `afterEach`. - -### PUT Update → GET Verify Cycle - -```typescript -test('should PUT update employee with token', async ({ request }) => { - // 1. Create employee - const createResponse = await request.post(...); - testEmployeeId = /* extract ID */; - - if (!testEmployeeId) { return; } - - // 2. Update the employee - const updatedData = { ...employeeData, id: testEmployeeId, firstName: 'UpdatedFirstName' }; - - const response = await request.put(`${baseURL}/employees/${testEmployeeId}`, { - headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' }, - ignoreHTTPSErrors: true, - data: updatedData, - }); - - expect([200, 204, 400]).toContain(response.status()); - - // 3. GET and verify the update took effect - if (response.status() === 200 || response.status() === 204) { - const getResponse = await request.get(`${baseURL}/employees/${testEmployeeId}`, { ... }); - - if (getResponse.status() === 200) { - const employee = await getResponse.json(); - const employeeData2 = employee.data || employee; - if (employeeData2.firstName) { - expect(employeeData2.firstName).toBe('UpdatedFirstName'); - } - } - } -}); -``` - -The **PUT → GET** cycle: update a field, re-fetch, verify the change persisted. - -### DELETE → 404 Verify Cycle - -```typescript -test('should DELETE employee with admin token', async ({ request }) => { - // Get HRAdmin token specifically (only HRAdmin can delete) - let adminToken: string | null = null; - try { - adminToken = await Promise.race([ - getTokenForRole(request, 'hradmin'), - timeoutPromise, - ]); - } catch { - console.log('Failed to acquire admin token, skipping DELETE test'); - return; - } - - // Create employee to delete - const createResponse = await request.post(`${baseURL}/employees`, { - headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' }, - ignoreHTTPSErrors: true, - data: createEmployeeData({ firstName: 'APITest', lastName: `Delete${Date.now()}` }), - }); - - const employeeId = /* extract ID */; - - // DELETE the employee - const response = await request.delete(`${baseURL}/employees/${employeeId}`, { - headers: { Authorization: `Bearer ${adminToken}` }, - ignoreHTTPSErrors: true, - }); - - expect([200, 204]).toContain(response.status()); - - // Verify deletion: GET should return 404 - if (response.status() === 200 || response.status() === 204) { - const getResponse = await request.get(`${baseURL}/employees/${employeeId}`, { ... }); - expect(getResponse.status()).toBe(404); - testEmployeeId = 0; // Prevent afterEach from trying to delete again - } -}); -``` - -**Why use the HRAdmin token for DELETE?** The Manager token doesn't have delete permission. Rather than reusing `authToken` (which is a Manager token), this test acquires a separate HRAdmin token. After confirming deletion with `204`, it fetches the record and confirms `404 Not Found`. - -### Error Cases - -```typescript -// 400 for invalid data -test('should return 400 Bad Request with invalid data', async ({ request }) => { - const response = await request.post(`${baseURL}/employees`, { - headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' }, - ignoreHTTPSErrors: true, - data: { firstName: '', lastName: '', email: 'invalid-email' }, - }); - - expect([400, 422]).toContain(response.status()); - - const error = await response.json(); - expect(error).toBeDefined(); - - if (error.errors) { - expect(error.errors).toBeDefined(); // Validation problem details - } else if (error.message) { - expect(error.message).toBeDefined(); - } -}); - -// 404 for non-existent ID -test('should return 404 Not Found for invalid employee ID', async ({ request }) => { - const response = await request.get(`${baseURL}/employees/999999999`, { - headers: { Authorization: `Bearer ${authToken}` }, - ignoreHTTPSErrors: true, - }); - - expect([400, 404]).toContain(response.status()); -}); - -// 403 for insufficient role -test('should return 403 Forbidden with wrong role for delete', async ({ request }) => { - // Get Employee token (read-only role) - const employeeToken = await getTokenForRole(request, 'employee'); - - const response = await request.delete(`${baseURL}/employees/1`, { - headers: { Authorization: `Bearer ${employeeToken}` }, - ignoreHTTPSErrors: true, - }); - - expect([401, 403]).toContain(response.status()); -}); -``` - ---- - -## 📋 Department API Tests — Same Pattern - -`tests/api/departments-api.spec.ts` follows the identical structure but adds an interesting test: - -```typescript -test('should handle duplicate department names', async ({ request }) => { - // Create first department - const firstResponse = await request.post(`${baseURL}/departments`, { - data: { name: `API_Duplicate_${Date.now()}`, description: 'Original' }, - // ... - }); - expect(firstResponse.status()).toBe(201); - - // Try to create a department with the same name - const duplicateResponse = await request.post(`${baseURL}/departments`, { - data: { name: departmentData.name, description: 'Duplicate attempt' }, - // ... - }); - - // 400/409 if duplicates are rejected; 201 if the API allows them - expect([201, 400, 409, 422]).toContain(duplicateResponse.status()); -}); -``` - -This tests the API's business rule: is a duplicate department name a validation error? The four-value assertion acknowledges that the behavior might change as the API evolves — the test documents what's acceptable rather than hardcoding one outcome. - ---- - -## 🔍 Cache Behavior Testing - -`tests/api/cache-api.spec.ts` tests cache behavior using the `request` fixture — something impossible to verify through the Angular UI. - -**How the API actually caches:** The API uses **EasyCaching** with custom diagnostic headers rather than standard HTTP ETag / If-None-Match. Responses include `X-Cache-Status: HIT` or `MISS`, `X-Cache-Key`, and `X-Cache-Duration-Ms`. The `cache-api.spec.ts` tests were written generically — tests that check for ETag pass via their fallback path since ETag is not implemented. - -**The three meaningful tests:** - -**1. Concurrent request consistency** - -```typescript -test('should handle concurrent cache requests correctly', async ({ request }) => { - const requests = Array(5).fill(null).map(() => - request.get(`${baseURL}/employees`, { - headers: { Authorization: `Bearer ${authToken}`, 'Accept': 'application/json' }, - ignoreHTTPSErrors: true, - }) - ); - - const responses = await Promise.all(requests); - - responses.forEach(response => { - expect(response.status()).toBe(200); - }); - - const bodies = await Promise.all(responses.map(r => r.json())); - if (Array.isArray(bodies[0])) { - const firstCount = bodies[0].length; - bodies.forEach(body => expect(body.length).toBe(firstCount)); - } -}); -``` - -`Promise.all` fires all five requests simultaneously. This confirms the cache serves all concurrent readers consistently without data races. - -**2. Cache invalidation on write** - -```typescript -test('should invalidate cache on data modification', async ({ request }) => { - // Read before write - const response1 = await request.get(`${baseURL}/employees`, { - headers: { Authorization: `Bearer ${authToken}`, 'Accept': 'application/json' }, - ignoreHTTPSErrors: true, - }); - expect(response1.status()).toBe(200); - - // Write — triggers EmployeeChangedEvent → CacheInvalidationEventHandler - const createResponse = await request.post(`${baseURL}/employees`, { - headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' }, - ignoreHTTPSErrors: true, - data: { - firstName: 'Cache', lastName: `Test${Date.now()}`, - email: `cache.test.${Date.now()}@example.com`, - gender: 0, employeeNumber: `CACHE${Date.now()}`, - }, - }); - - if (createResponse.status() === 201) { - // Read after write — should return fresh data, not stale cache - const response2 = await request.get(`${baseURL}/employees`, { - headers: { Authorization: `Bearer ${authToken}`, 'Accept': 'application/json' }, - ignoreHTTPSErrors: true, - }); - expect(response2.status()).toBe(200); - // Cleanup follows... - } -}); -``` - -When `POST /employees` returns 201, `EmployeeChangedEvent` fires automatically. `CacheInvalidationEventHandler` clears the `Employees:GetAll` cache key. The second GET returns fresh data — not the cached version from before the write. - -**3. Admin endpoints** - -```typescript -// POST /api/v1/cache/invalidate — manual cache clear (HRAdmin only) -const invalidateResponse = await request.post(`${baseURL}/cache/invalidate`, { - headers: { Authorization: `Bearer ${authToken}` }, - ignoreHTTPSErrors: true, -}); -// 200/204 = cleared; 404/405 = endpoint found but role denied -expect([200, 204, 404, 405]).toContain(invalidateResponse.status()); - -// GET /api/v1/cache/stats — hit/miss metrics (HRAdmin only) -const statsResponse = await request.get(`${baseURL}/cache/stats`, { - headers: { Authorization: `Bearer ${authToken}` }, - ignoreHTTPSErrors: true, -}); -expect([200, 404, 405]).toContain(statsResponse.status()); -``` - -Both endpoints exist in `CacheController.cs`. The test accepts 404/405 as valid because they require HRAdmin role — a Manager token returns 403 or 405. - ---- - -## ⚙️ The `ignoreHTTPSErrors` Requirement - -All API calls in these tests include `ignoreHTTPSErrors: true`. The dev API runs at `https://localhost:44378` with a self-signed certificate that browsers (and Playwright) would normally reject. - -This is set globally in `playwright.config.ts`: - -```typescript -use: { - baseURL: 'http://localhost:4200', - ignoreHTTPSErrors: true, // Required for self-signed dev API certificate - // ... -} -``` - -And explicitly on each `request` call as a defensive measure. In production, certificates are signed by a trusted CA and this flag is not needed. - ---- - -## 📡 Pagination and Search - -```typescript -test('should support pagination parameters', async ({ request }) => { - const response = await request.get(`${baseURL}/employees?page=1&pageSize=10`, { - headers: { Authorization: `Bearer ${authToken}` }, - ignoreHTTPSErrors: true, - }); - - expect(response.status()).toBe(200); - - const data = await response.json(); - - if (data.totalCount !== undefined || data.total !== undefined) { - expect(typeof (data.totalCount || data.total)).toBe('number'); - } - - if (data.pageSize !== undefined) { - expect(data.pageSize).toBeLessThanOrEqual(10); - } -}); - -test('should support search/filter parameters', async ({ request }) => { - const response = await request.get(`${baseURL}/employees?search=test`, { ... }); - - expect(response.status()).toBe(200); - - const data = await response.json(); - if (Array.isArray(data)) { - expect(Array.isArray(data)).toBe(true); - } else if (data.data || data.items) { - expect(Array.isArray(data.data || data.items)).toBe(true); - } -}); -``` - -These tests verify that the API accepts pagination and search parameters without errors — even if the test dataset happens to return zero results for the search term. - ---- - -## 🔑 Key Design Decisions - -**`test.beforeAll` for token, `test.afterEach` for cleanup.** Token acquisition is slow — do it once per suite. Record cleanup is per-test — do it after every test, even failing ones. - -**`Promise.race` with timeout for token acquisition.** If IdentityServer isn't running, `getTokenForRole()` will hang. The timeout race prevents the suite from blocking indefinitely. - -**`authFailed` guard on every test.** When services aren't running, every test should skip cleanly rather than fail with cryptic errors. `if (authFailed || !authToken) test.skip()` at the top of each test achieves this. - -**Flexible response shape handling.** `.NET` Clean Architecture APIs often wrap responses: `{ data: {...} }`, `{ items: [...], totalCount: N }`, or plain arrays. The `data.data || data.items || data` pattern normalizes these so tests don't break when the response envelope changes. - -**Separate tokens for different permission levels.** The `beforeAll` acquires a Manager token. Tests that need admin permissions (delete) acquire a separate HRAdmin token inline. This mirrors real-world API security: different operations require different access levels. - -**`content-type` header verification.** - -```typescript -test('should return proper content-type header', async ({ request }) => { - const response = await request.get(`${baseURL}/employees`, { ... }); - const contentType = response.headers()['content-type']; - expect(contentType).toContain('application/json'); -}); -``` - -This simple test catches a common misconfiguration: the API returning HTML error pages instead of JSON when something goes wrong internally. - ---- - -## 🌟 Why This Matters - -Playwright's `request` fixture turns an E2E test suite into a full API test suite — without a second testing framework. The `getTokenForRole()` pattern (headless browser for OIDC token acquisition, then bare HTTP client for API calls) solves the hardest problem in OAuth-protected API testing: getting a real token programmatically when PKCE is the only allowed grant. - -The complete CRUD cycle — GET list, POST create, GET verify, PUT update, GET verify, DELETE, GET 404 — with `test.afterEach` cleanup is a pattern that works for any REST API, regardless of backend framework or authentication provider. - -**Transferable skills:** - -* **Playwright `request` fixture for API testing** — Applicable to any REST API test suite that needs real OAuth 2.0 tokens -* **`getTokenForRole()` headless browser pattern** — Foundation for PKCE token acquisition in environments where the Resource Owner Password Credentials grant is not available -* **Full CRUD verification cycles** — Pattern for comprehensive API testing with cleanup that prevents test data accumulation - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #playwright #dotnet #restapi #testautomation #e2etesting #typescript #webdevelopment #qa #testing #oauth2 #jwt #apitest #crud #angular #cleanarchitecture diff --git a/blogs/series-5-devops-data/5.1-database-seeding.md b/blogs/series-5-devops-data/5.1-database-seeding.md deleted file mode 100644 index 682c660..0000000 --- a/blogs/series-5-devops-data/5.1-database-seeding.md +++ /dev/null @@ -1,655 +0,0 @@ -# 1,000 Test Employees in 3 Seconds: Database Seeding for Development and Testing - -## How the Bogus Library Generates Reproducible Fake Data That Makes Your App Look Real From Day One - -The first time a developer runs a new project, they usually see an empty table. Empty tables make demos unconvincing and make UI bugs invisible — you can't see pagination problems without enough rows to paginate, and you can't test search without data to find. - -The **AngularNetTutorial** API solves this with automatic database seeding: 1,000 employees, 100 positions, 10 departments, and 5 salary ranges generated on first startup in about three seconds. Every record has a realistic name, valid email, phone in `(555)-123-4567` format, and gender-consistent prefix. The data looks like a real HR system. - -This article walks through the seeding architecture — the Bogus library, the `DatabaseSeeder` class, the `DbInitializer` entry point, and the `Program.cs` logic that decides when to seed. - -![Employee List with Seeded Data](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-list-page.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article dives deep into the Bogus-powered database seeding system that populates the app with 1,000 realistic employees on first startup.** - ---- - -## 📚 What You'll Learn - -* Why hand-written seed data doesn't scale — and what Bogus provides instead -* The `DatabaseSeeder` class — four generators with a shared seed value for reproducibility -* `GenerateEmployees()` — gender-aware names, derived emails, FK assignment via `PickRandom` -* `GenerateDepartments()` — Commerce.Department() for realistic team names -* `GeneratePositions()` — the `GroupBy` deduplication trick -* `GenerateSalaryRanges()` — simple min/max ranges with `f.Random.Number()` -* `EmployeeBogusConfig` — the `AutoFaker` approach for unit test fixtures -* `PositionInsertBogusConfig` — the `Faker` approach with FK injection -* `DbInitializer.SeedData()` — the one-liner entry point -* `Program.cs` startup logic — `EnsureCreated`, `SkipDbSeed`, and the `needsSeed` check -* The `InsertMockPositionCommand` — CQRS-based on-demand seeding via API endpoint -* The seed value `1969` — why reproducibility matters for debugging - ---- - -## 🤔 The Problem With Hand-Written Seed Data - -The naive approach is a static seed file: - -```csharp -// The naive approach — doesn't scale -dbContext.Employees.Add(new Employee { - Id = Guid.NewGuid(), - FirstName = "John", - LastName = "Doe", - Email = "john.doe@example.com", -}); -dbContext.Employees.Add(new Employee { - Id = Guid.NewGuid(), - FirstName = "Jane", - LastName = "Smith", - Email = "jane.smith@example.com", -}); -// ... 998 more records ... -``` - -Maintenance is painful. The names are obviously fake. Everyone uses the same five test names. There's no variety in salary, department, or position. And when you add a new required field to the entity, you have to update every record manually. - -The **Bogus** library (`NuGet: Bogus`) solves all of this. It's a .NET port of the popular JavaScript `faker.js` library — a fluent API for generating realistic fake data with one line per field. - ---- - -## 📦 The Bogus Library - -Bogus uses a fluent `Faker` API where you define rules for each property: - -```csharp -var faker = new Faker() - .UseSeed(1969) // reproducible output - .RuleFor(r => r.Id, f => Guid.NewGuid()) - .RuleFor(r => r.Gender, f => f.PickRandom()) - .RuleFor(r => r.Name, (f, r) => new PersonName( - f.Name.FirstName((Name.Gender)r.Gender), - f.Name.FirstName((Name.Gender)r.Gender), - f.Name.LastName((Name.Gender)r.Gender))) - .RuleFor(r => r.Email, (f, p) => f.Internet.Email(p.Name.FirstName, p.Name.LastName)) - .RuleFor(r => r.Phone, f => f.Phone.PhoneNumber("(###)-###-####")); - -var employees = faker.Generate(1000); -``` - -Key features: - -* **`.UseSeed(1969)`** — same seed always produces the same output. Run it today and tomorrow and get identical records. -* **`(f, r) => ...`** — the two-parameter lambda lets later rules reference earlier ones. The email rule uses the already-generated `Name` to build a realistic address like `john.doe@example.com`. -* **`f.PickRandom()`** — picks a random value from a C# enum. -* **`f.Name.FirstName((Name.Gender)r.Gender)`** — generates a first name appropriate for the already-chosen gender. Mary and John instead of John and John. -* **`f.Generate(1000)`** — runs all rules 1,000 times and returns a `List`. - ---- - -## 🏗️ The Seeding Architecture - -The seeding system has three layers: - -``` -Program.cs ← decides WHEN to seed (startup + conditions) - └── DbInitializer ← decides WHAT to seed (delegates to DatabaseSeeder) - └── DatabaseSeeder ← decides HOW to seed (Bogus Faker rules) -``` - -### DatabaseSeeder — the Core - -```csharp -public class DatabaseSeeder -{ - public IReadOnlyCollection Departments { get; } - public IReadOnlyCollection Employees { get; } - public IReadOnlyCollection Positions { get; } - public IReadOnlyCollection SalaryRanges { get; } - - public DatabaseSeeder( - int rowDepartments = 10, - int rowSalaryRanges = 5, - int rowPositions = 100, - int rowEmployees = 1000, - int seedValue = 1969) - { - // Order matters: each collection feeds into the next - Departments = GenerateDepartments(rowDepartments, seedValue); - SalaryRanges = GenerateSalaryRanges(rowSalaryRanges, seedValue); - Positions = GeneratePositions(rowPositions, seedValue, Departments, SalaryRanges); - Employees = GenerateEmployees(rowEmployees, seedValue, Positions, Departments); - } -} -``` - -**Generation order matters.** Departments and SalaryRanges exist first. Positions reference them. Employees reference Positions and Departments. This mirrors the foreign key dependency graph. - -**Defaults are just defaults.** You can spin up a larger dataset: - -```csharp -var seeder = new DatabaseSeeder(rowEmployees: 5000, rowDepartments: 20); -``` - -Or a minimal dataset for fast tests: - -```csharp -var seeder = new DatabaseSeeder(rowEmployees: 10, rowPositions: 5, rowDepartments: 3); -``` - ---- - -## 🏢 GenerateDepartments() - -```csharp -private static IReadOnlyCollection GenerateDepartments(int rowCount, int seedValue) -{ - var faker = new Faker() - .UseSeed(seedValue) - .RuleFor(r => r.Id, f => Guid.NewGuid()) - .RuleFor(r => r.Name, f => new DepartmentName(f.Commerce.Department())) - .RuleFor(r => r.Created, f => f.Date.Past(f.Random.Number(1, 5), DateTime.Now)) - .RuleFor(r => r.CreatedBy, f => f.Internet.UserName()); - - return faker.Generate(rowCount); -} -``` - -`f.Commerce.Department()` produces names like "Toys", "Movies", "Computers", "Electronics" — a Bogus commerce category list. These make the UI look real without being the same generic "HR Department" repeated ten times. - -`DepartmentName` is a **Value Object** from the Domain layer — the entity uses an owned type rather than a raw string, which is why the rule wraps the string in `new DepartmentName(...)`. - -`f.Date.Past(f.Random.Number(1, 5), DateTime.Now)` generates a creation date 1–5 years in the past. Each department was "created" at a slightly different time, making the audit trail look realistic. - ---- - -## 💰 GenerateSalaryRanges() - -```csharp -private static IReadOnlyCollection GenerateSalaryRanges(int rowCount, int seedValue) -{ - var faker = new Faker() - .UseSeed(seedValue) - .RuleFor(r => r.Id, f => Guid.NewGuid()) - .RuleFor(r => r.Name, f => f.Name.JobDescriptor()) - .RuleFor(r => r.MinSalary, f => f.Random.Number(30000, 40000)) - .RuleFor(r => r.MaxSalary, f => f.Random.Number(80000, 100000)) - .RuleFor(r => r.Created, f => f.Date.Past(f.Random.Number(1, 5), DateTime.Now)) - .RuleFor(r => r.CreatedBy, f => f.Internet.UserName()); - - return faker.Generate(rowCount); -} -``` - -`f.Name.JobDescriptor()` produces terms like "Senior", "Lead", "Principal", "Dynamic" — which makes range names read like "Senior Level" or "Dynamic Package". `MinSalary` and `MaxSalary` are independent random numbers here; in a real system you'd add a rule to ensure `MaxSalary > MinSalary`, but for seed data the rough ranges are acceptable. - ---- - -## 📋 GeneratePositions() — The Deduplication Trick - -```csharp -private static IReadOnlyCollection GeneratePositions( - int rowCount, int seedValue, - IEnumerable departments, - IEnumerable salaryRanges) -{ - var faker = new Faker() - .UseSeed(seedValue) - .RuleFor(r => r.Id, f => Guid.NewGuid()) - .RuleFor(o => o.PositionTitle, f => new PositionTitle(f.Name.JobTitle())) - .RuleFor(o => o.PositionNumber, f => f.Commerce.Ean13()) - .RuleFor(o => o.PositionDescription, f => f.Lorem.Paragraphs(2)) - .RuleFor(r => r.DepartmentId, f => f.PickRandom(departments).Id) - .RuleFor(r => r.SalaryRangeId, f => f.PickRandom(salaryRanges).Id) - .RuleFor(r => r.Created, f => f.Date.Past(f.Random.Number(1, 5), DateTime.Now)) - .RuleFor(r => r.CreatedBy, f => f.Internet.UserName()); - - // Generate, then deduplicate by department + salary range combination - return faker.Generate(rowCount) - .GroupBy(r => new { r.DepartmentId, r.SalaryRangeId }) - .Select(r => r.First()) - .ToList(); -} -``` - -The `.GroupBy(...).Select(r => r.First())` chain is important. With 10 departments and 5 salary ranges, there are only 50 possible `(Department, SalaryRange)` combinations. Generating 100 positions and then deduplicating ensures you get at most one position per combination — which makes more business sense than having multiple identical position slots. - -`f.PickRandom(departments).Id` — this is how foreign keys are assigned in Bogus. `departments` is the already-generated collection. `PickRandom()` selects one at random. `.Id` extracts the Guid. The result is a valid FK reference to a real department. - -`f.Name.JobTitle()` produces titles like "Lead Assurance Administrator", "Senior Accounts Technician", "Product Implementation Analyst" — Bogus job titles that read convincingly. - -`f.Lorem.Paragraphs(2)` generates two paragraphs of lorem ipsum for the description — quick and good enough for seed data. - ---- - -## 👤 GenerateEmployees() — Gender-Aware Names - -```csharp -private static IReadOnlyCollection GenerateEmployees( - int rowCount, int seedValue, - IEnumerable positions, - IEnumerable departments) -{ - var faker = new Faker() - .UseSeed(seedValue) - .RuleFor(r => r.Id, f => Guid.NewGuid()) - .RuleFor(r => r.Gender, f => f.PickRandom()) - .RuleFor(r => r.EmployeeNumber, f => f.Commerce.Ean13()) - .RuleFor(r => r.Salary, f => f.Random.Number(20000, 110000)) - .RuleFor(r => r.Prefix, (f, r) => f.Name.Prefix((Name.Gender)r.Gender)) - .RuleFor(r => r.Name, (f, r) => new PersonName( - f.Name.FirstName((Name.Gender)r.Gender), - f.Name.FirstName((Name.Gender)r.Gender), - f.Name.LastName((Name.Gender)r.Gender))) - .RuleFor(r => r.Birthday, f => f.Person.DateOfBirth) - .RuleFor(r => r.Email, (f, p) => f.Internet.Email(p.Name.FirstName, p.Name.LastName)) - .RuleFor(r => r.Phone, f => f.Phone.PhoneNumber("(###)-###-####")) - .RuleFor(r => r.PositionId, f => f.PickRandom(positions).Id) - .RuleFor(r => r.DepartmentId, f => f.PickRandom(departments).Id) - .RuleFor(r => r.Created, f => f.Date.Past(f.Random.Number(1, 5), DateTime.Now)) - .RuleFor(r => r.CreatedBy, f => f.Internet.UserName()); - - return faker.Generate(rowCount); -} -``` - -Four things worth calling out: - -**Gender determines prefix and name.** `Gender` is generated first. `Prefix` uses `(f, r) => f.Name.Prefix((Name.Gender)r.Gender)` — the `r` parameter is the Employee being built, so `r.Gender` is the already-assigned gender. Same for the `Name` rule: `FirstName((Name.Gender)r.Gender)` generates a gender-appropriate first name. Female employees get "Ms." and names like "Emily", male employees get "Mr." and names like "James". - -**Email is derived from name.** `f.Internet.Email(p.Name.FirstName, p.Name.LastName)` builds an email like `emily.johnson@example.com` from the already-generated name. The `p` parameter is the Employee being built (same as `r` in other rules). This produces email addresses that actually correspond to the name in the same record — much more realistic than random strings. - -**`f.Person.DateOfBirth`** — Bogus has a `.Person` helper that generates a coherent "person" object. `DateOfBirth` from this helper produces a date that corresponds to an adult, which is more appropriate than `f.Date.Past(100)` which could produce a birthday from the 1900s. - -**`f.Commerce.Ean13()`** — generates a 13-digit EAN barcode number for the employee number field. These look like legitimate ID numbers without actually being sequential integers that might conflict with real IDs. - ---- - -## 🔧 EmployeeBogusConfig — AutoFaker for Unit Tests - -While `DatabaseSeeder` uses `Faker` (fully explicit rules), there's also an `EmployeeBogusConfig` using `AutoFaker` for a different use case: - -```csharp -public class EmployeeBogusConfig : AutoFaker -{ - public EmployeeBogusConfig() - { - Randomizer.Seed = new Random(8675309); // static seed - - RuleFor(p => p.Id, f => Guid.NewGuid()); - RuleFor(p => p.Name, f => new PersonName( - f.Name.FirstName(), f.Name.FirstName(), f.Name.LastName())); - RuleFor(p => p.Prefix, f => f.Name.Prefix()); - RuleFor(p => p.Email, (f, p) => f.Internet.Email(p.Name.FirstName, p.Name.LastName)); - RuleFor(p => p.Birthday, f => f.Date.Past(18)); - RuleFor(p => p.Gender, f => f.PickRandom()); - RuleFor(p => p.EmployeeNumber, f => f.Commerce.Ean13()); - RuleFor(p => p.Phone, f => f.Phone.PhoneNumber("(###)-###-####")); - - // Note: FK placeholders (not real IDs) - RuleFor(p => p.PositionId, f => Guid.NewGuid()); - RuleFor(p => p.DepartmentId, f => Guid.NewGuid()); - } -} -``` - -**`AutoFaker` vs `Faker`:** -* `AutoFaker` auto-generates values for all properties not explicitly configured, using conventions (e.g., any `string` gets a random string, any `int` gets a random int). You only write rules for fields that need special logic. -* `Faker` requires explicit rules for every property — nothing is auto-filled. - -`EmployeeBogusConfig` uses `AutoFaker` and is intended for unit tests where you need quick Employee instances with FK placeholders. The `DatabaseSeeder` uses `Faker` because it needs precise control over FK assignment (real references to generated departments and positions, not random Guids). - ---- - -## 📌 PositionInsertBogusConfig — FK Injection at Construction - -```csharp -public class PositionInsertBogusConfig : Faker -{ - public PositionInsertBogusConfig( - IEnumerable departments, - IEnumerable salaryRanges) - { - RuleFor(o => o.Id, f => Guid.NewGuid()); - RuleFor(o => o.PositionTitle, f => new PositionTitle(f.Name.JobTitle())); - RuleFor(o => o.PositionNumber, f => f.Commerce.Ean13()); - RuleFor(o => o.PositionDescription, f => f.Name.JobDescriptor()); - // Real FK references injected via constructor - RuleFor(o => o.DepartmentId, f => f.PickRandom(departments).Id); - RuleFor(o => o.SalaryRangeId, f => f.PickRandom(salaryRanges).Id); - RuleFor(o => o.Created, f => f.Date.Past(1)); - RuleFor(o => o.CreatedBy, f => f.Name.FullName()); - RuleFor(o => o.LastModified, f => f.Date.Recent(1)); - RuleFor(o => o.LastModifiedBy, f => f.Name.FullName()); - } -} -``` - -The constructor takes real `departments` and `salaryRanges` collections. The `PickRandom` rules pick from those real collections at generation time. This is the pattern for **runtime seeding** via the `InsertMockPositionCommand` — you fetch existing entities from the database, pass them to the config, and generate positions that reference real rows. - ---- - -## 🚀 DbInitializer — The One-Liner Entry Point - -```csharp -public static class DbInitializer -{ - public static void SeedData(ApplicationDbContext dbContext) - { - var databaseSeeder = new DatabaseSeeder(); - - dbContext.Departments.AddRange(databaseSeeder.Departments); - dbContext.SalaryRanges.AddRange(databaseSeeder.SalaryRanges); - dbContext.Positions.AddRange(databaseSeeder.Positions); - dbContext.Employees.AddRange(databaseSeeder.Employees); - - dbContext.SaveChanges(); - } -} -``` - -Simple. `DatabaseSeeder` generates all the data. `DbInitializer` adds it to the DbContext and saves. `SaveChanges()` inserts everything in a single transaction — all 1,115+ records in one database roundtrip. - ---- - -## ⚙️ Program.cs — When to Seed - -```csharp -if (app.Environment.IsDevelopment()) -{ - using var scope = app.Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - // Create the database if it doesn't exist (SQLite/in-memory: always) - dbContext.Database.EnsureCreated(); - - // Check the configuration flag - var skipDbSeed = builder.Configuration.GetValue("SkipDbSeed"); - - // Only seed if both conditions are true - var needsSeed = !dbContext.Departments.Any() || !dbContext.Employees.Any(); - - if (!skipDbSeed && needsSeed) - { - DbInitializer.SeedData(dbContext); - } -} -``` - -Three conditions control seeding: - -**Development only.** The entire block is inside `if (app.Environment.IsDevelopment())`. Production never seeds automatically. - -**`SkipDbSeed` flag.** In `appsettings.Development.json`, set `"SkipDbSeed": true` to start the API with an empty database — useful when you want to test the "first time" experience or run integration tests that need a clean state: - -```json -{ - "SkipDbSeed": false -} -``` - -**`needsSeed` check.** `!dbContext.Departments.Any() || !dbContext.Employees.Any()` only seeds if either departments or employees table is empty. This prevents re-seeding on every startup and doubling the data with each API restart. - ---- - -## 🌱 InsertMockPositionCommand — On-Demand Seeding via API - -Beyond startup seeding, there's a CQRS command for adding more positions at runtime: - -```csharp -public class SeedPositionCommandHandler : IRequestHandler -{ - public async Task Handle(InsertMockPositionCommand request, CancellationToken cancellationToken) - { - // Fetch existing data from the database - var departments = await _departmentRepository.GetAllAsync(); - var salaryRanges = await _salaryRangeRepository.GetAllAsync(); - - // Use PositionInsertBogusConfig with real FK data - return await _positionRepository.SeedDataAsync( - new PositionInsertBogusConfig(departments, salaryRanges), - request.RowCount - ); - } -} -``` - -This is useful for: - -* Adding more test data without restarting the API -* Seeding specific row counts in different environments -* Integration test setup — seed exactly the records you need via an API call - -The command is dispatched via `POST /api/v1/positions/AddMock`. It requires a valid Bearer token (`[Authorize]`) and accepts `{ "RowCount": N }` as a JSON body. It can be called from Playwright tests, CI scripts, or Swagger UI. - ---- - -## 🔢 Why Seed Value 1969? - -```csharp -.UseSeed(seedValue) // default: 1969 -``` - -The seed value makes generation **deterministic**. Given the same seed, Bogus always generates the same sequence. This matters for several reasons: - -* **Debugging reproducibility.** When a bug appears in test data, you can regenerate the exact same dataset to reproduce it. -* **Test stability.** Integration tests that check specific records ("employee with ID X should have name Y") work because the same seed always produces the same output. -* **Team consistency.** Every developer who runs `dotnet run` gets the same test employees with the same names and emails. - -1969 is just a memorable number (the year of the moon landing). Any integer works — the important thing is that it's documented and consistent. - ---- - -## 🧪 Playwright Seeding via the API - -`Tests/AngularNetTutorial-Playwright/tests/seed.spec.ts` is a placeholder for E2E-driven seeding. The startup seeder gives every developer a consistent base of 1,000 employees — but sometimes an E2E test needs something more specific: a known number of positions, a particular department, or a clean slate before a destructive test run. That's what this file is for. - -### Why E2E Tests Need Their Own Seeding - -The startup seeder runs once on first launch. After that, tests that create, update, or delete records permanently change the database. If three tests run in sequence and test 2 deletes a record that test 3 expects to find, test 3 fails intermittently depending on run order. E2E-driven seeding solves this by resetting or expanding the dataset to a known state before the tests that depend on it. - -Three common patterns: - -* **Add before** — seed extra records before a test that needs more data than startup seeding provides (e.g., pagination tests that need 200+ positions) -* **Reset between** — call the seed endpoint in `beforeEach` to restore a known state before each destructive test -* **Seed in global setup** — run once before the entire test suite to guarantee a baseline without relying on startup seeding - -### The Playwright `request` Fixture - -Playwright provides a `request` fixture — an `APIRequestContext` that makes HTTP calls without launching a browser. It is the right tool for calling the seed API in tests because: - -* No browser overhead — seed calls complete in milliseconds -* Available in `beforeAll`, `beforeEach`, and in `globalSetup` -* Uses the same `baseURL` from `playwright.config.ts` -* Returns a full `APIResponse` with status, headers, and body - -### Calling the Seed Endpoint - -The `InsertMockPositionCommand` is exposed as `POST /api/v1/positions/AddMock`. It requires a Bearer token (`[Authorize]`) and accepts a JSON body. Here is a complete implementation of `seed.spec.ts`: - -```typescript -// Tests/AngularNetTutorial-Playwright/tests/seed.spec.ts -import { test, expect } from '@playwright/test'; - -const API_BASE = process.env.API_URL ?? 'https://localhost:44378'; - -test.describe('Database seeding via API', () => { - - test('seed positions', async ({ request }) => { - // Acquire a Bearer token first (use your test credentials) - const tokenResponse = await request.post(`${API_BASE}/api/v1/account/authenticate`, { - data: { userName: 'ashtyn1', password: 'Pa$$word123' }, - }); - const { jwToken } = await tokenResponse.json(); - - // POST /api/v1/positions/AddMock with JSON body - // InsertMockPositionCommand generates positions with valid FK references - const response = await request.post(`${API_BASE}/api/v1/positions/AddMock`, { - headers: { Authorization: `Bearer ${jwToken}` }, - data: { RowCount: 25 }, - }); - - expect(response.status()).toBe(200); - - const result = await response.json(); - console.log(`Seeded ${result.data} positions`); - expect(result.succeeded).toBe(true); - }); - -}); -``` - -**`POST /api/v1/positions/AddMock`** — the actual route registered by the `[Route("AddMock")]` attribute on `PositionsController`. The endpoint is protected with `[Authorize]`, so a valid Bearer token is required. - -**`data: { RowCount: 25 }`** — Playwright serializes this as a JSON body. `InsertMockPositionCommand` binds from the request body (POST default). Property name matches the C# property: `RowCount` (Pascal case). - -**`result.data`** — the endpoint returns a `Result` wrapper with `{ succeeded: true, data: 25, ... }`, not a plain integer. Access the row count via `result.data`. - -### Seeding in `beforeAll` for a Test Suite - -When multiple tests in the same file need the seeded data, call the seed endpoint in `beforeAll` so it runs once before the suite rather than repeating it in every test: - -```typescript -// Tests/AngularNetTutorial-Playwright/tests/positions.spec.ts -import { test, expect } from '@playwright/test'; - -const API_BASE = process.env.API_URL ?? 'https://localhost:44378'; - -test.describe('Positions list', () => { - - test.beforeAll(async ({ request }) => { - // Acquire a Bearer token - const tokenResponse = await request.post(`${API_BASE}/api/v1/account/authenticate`, { - data: { userName: 'ashtyn1', password: 'Pa$$word123' }, - }); - const { jwToken } = await tokenResponse.json(); - - // Ensure at least 50 positions exist before any test in this suite runs - await request.post(`${API_BASE}/api/v1/positions/AddMock`, { - headers: { Authorization: `Bearer ${jwToken}` }, - data: { RowCount: 50 }, - }); - }); - - test('shows positions in the table', async ({ page }) => { - await page.goto('/positions'); - await expect(page.getByRole('row')).toHaveCount.greaterThan(10); - }); - - test('pagination works with enough rows', async ({ page }) => { - await page.goto('/positions'); - await expect(page.getByTestId('next-page-btn')).toBeEnabled(); - }); - -}); -``` - -### Seeding in Global Setup - -For seeding that should run once before the **entire Playwright suite** — not just one file — use a `globalSetup` script. `playwright.config.ts` already has the hook point: - -```typescript -// playwright.config.ts -export default defineConfig({ - globalSetup: './global-setup.ts', - // ... -}); -``` - -```typescript -// Tests/AngularNetTutorial-Playwright/global-setup.ts -import { request } from '@playwright/test'; - -const API_BASE = process.env.API_URL ?? 'https://localhost:44378'; - -async function globalSetup() { - const apiContext = await request.newContext({ baseURL: API_BASE }); - - // Acquire a Bearer token using test credentials - const tokenResponse = await apiContext.post('/api/v1/account/authenticate', { - data: { userName: 'ashtyn1', password: 'Pa$$word123' }, - }); - if (!tokenResponse.ok()) { - throw new Error(`Auth failed: ${tokenResponse.status()} ${await tokenResponse.text()}`); - } - const { jwToken } = await tokenResponse.json(); - - // POST /api/v1/positions/AddMock with Bearer token and JSON body - const response = await apiContext.post('/api/v1/positions/AddMock', { - headers: { Authorization: `Bearer ${jwToken}` }, - data: { RowCount: 100 }, - }); - - if (!response.ok()) { - throw new Error(`Seed failed: ${response.status()} ${await response.text()}`); - } - - console.log('Global seed complete'); - await apiContext.dispose(); -} - -export default globalSetup; -``` - -`request.newContext()` in `globalSetup` creates a standalone API context outside of any test. The auth call acquires a short-lived JWT using the same test credentials used in browser-based tests. `await apiContext.dispose()` releases it when done. Throwing on either failure (auth or seed) fails the entire run immediately with a clear message rather than silently running tests against an unseeded database. - -### When to Use Each Approach - -* **`seed.spec.ts` standalone test** — run manually from Swagger or CI when you need to top up data in a shared environment -* **`beforeAll` in a spec file** — guarantee a data baseline for one specific test suite without affecting others -* **`globalSetup`** — guarantee the baseline for the entire run; appropriate when all tests depend on the same starting state - ---- - -## 🔑 Key Design Decisions - -**`Faker` for seeding, `AutoFaker` for unit tests.** `DatabaseSeeder` uses `Faker` with explicit rules for every field — giving precise control over FK assignment and field-to-field dependencies. `EmployeeBogusConfig` uses `AutoFaker` which auto-fills unspecified fields — faster to write for unit test fixtures where exact values don't matter. - -**`.UseSeed()` makes generation deterministic.** Every developer who runs `dotnet run` gets the exact same 1,000 employees with the same names, emails, and FK assignments. Seed value `1969` is just a memorable constant — any integer works. Determinism is what makes automated tests reliable: if a test checks for a specific employee record, it can rely on that record always being there. - -**FK generation order mirrors the dependency graph.** `Departments` and `SalaryRanges` are generated first because `Positions` references them, and `Employees` reference both `Positions` and `Departments`. Reversing this order would produce FK values pointing to records that don't exist yet. The constructor enforces the correct sequence. - -**`SkipDbSeed` flag enables clean-state testing.** Setting `"SkipDbSeed": true` in `appsettings.Development.json` starts the API with an empty database. This is useful for integration tests that need to control exactly what data exists, or for testing the "empty state" UI experience without deleting and recreating the database. - -**Development-only seeding with an idempotency check.** The entire seed block is inside `if (app.Environment.IsDevelopment())` — production never auto-seeds. The `needsSeed` check (`!dbContext.Departments.Any() || !dbContext.Employees.Any()`) ensures the seeder only runs on an empty database, so restarting the API doesn't duplicate records. - -**`InsertMockPositionCommand` for runtime seeding.** The CQRS `POST /api/v1/positions/AddMock` endpoint lets you add more positions without restarting the API. It fetches existing departments and salary ranges from the database and passes them to `PositionInsertBogusConfig` — so every generated position has valid FK references to real rows. This pattern extends to any entity that has FK dependencies. - ---- - -## 🌟 Why This Matters - -Empty databases make demos unconvincing and make UI bugs invisible — you can't find pagination problems without enough rows to paginate. Starting with a Bogus-powered seeder that generates 1,000 realistic employees in three seconds means the application looks like a real HR system from day one, not a toy. - -The `DatabaseSeeder` architecture — with `UseSeed()` for reproducibility, FK-aware generation order, and the `SkipDbSeed` flag for clean-state tests — applies directly to any .NET application using Entity Framework Core. The same pattern works for any domain: products, orders, customers, or any entity graph with foreign key relationships. - -**Transferable skills:** - -* **Bogus `Faker` with `.UseSeed()`** — Applicable to any .NET application that needs reproducible, realistic test data -* **`DbInitializer` pattern** — Foundation for environment-gated, idempotent database seeding in any EF Core project -* **FK-aware generation order** — Pattern for generating related entities in dependency order so foreign key references are always valid - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #dotnet #csharp #bogus #fakerdata #databaseseeding #testdata #efcore #cleanarchitecture #webdevelopment #backend #testing #devtools #asp.net #entityframework #softwaredevelopment diff --git a/blogs/series-5-devops-data/5.2-cicd-github-actions.md b/blogs/series-5-devops-data/5.2-cicd-github-actions.md deleted file mode 100644 index da24fc1..0000000 --- a/blogs/series-5-devops-data/5.2-cicd-github-actions.md +++ /dev/null @@ -1,526 +0,0 @@ -# Run Your Playwright Tests Automatically: CI/CD for a Full-Stack Angular/.NET App - -## GitHub Actions Workflow That Runs All Three Browsers, Publishes JUnit Reports, and Comments Results on Every Pull Request - -Most CI guides show you how to run Playwright tests against a single server. This project is different: before a single test can run, three services must be up — IdentityServer at port 44310, the .NET API at port 44378, and the Angular app at port 4200. OIDC login flows through all of them. Skip any one and every test fails. - -This article walks through the GitHub Actions workflow in **AngularNetTutorial** — what it does correctly out of the box, how `playwright.config.ts` adapts to CI, and what you need to add to run the full three-service stack automatically. - -![GitHub Actions Playwright Run](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/employee-list-page.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article covers the GitHub Actions CI/CD workflow for running Playwright E2E tests across three browsers with artifact upload, JUnit reporting, and PR comments.** - ---- - -## 📚 What You'll Learn - -* The full workflow file — three jobs, what each does -* Matrix strategy for running Chromium, Firefox, and WebKit in parallel -* How `process.env.CI` changes `playwright.config.ts` behavior: retries, workers, and `forbidOnly` -* Four reporters: HTML, JSON, JUnit, and list -* Artifact upload for reports and failure evidence (traces, screenshots, videos) -* `dorny/test-reporter` for publishing JUnit results to the GitHub Actions UI -* Auto-commenting test results on pull requests with `actions/github-script` -* The three-service challenge: what the workflow needs to start IdentityServer, API, and Angular -* `webServer` configuration and the `reuseExistingServer` flag -* Self-signed certificate handling in headless CI - ---- - -## 📄 The Workflow File - -The workflow lives at `Tests/AngularNetTutorial-Playwright/.github/workflows/playwright.yml`: - -```yaml -name: Playwright Tests - -on: - push: - branches: [ main, master, develop ] - pull_request: - branches: [ main, master, develop ] - workflow_dispatch: -``` - -**Three triggers:** - -* **`push`** — runs on every push to `main`, `master`, or `develop` -* **`pull_request`** — runs on every PR targeting those branches (including the PR comment job) -* **`workflow_dispatch`** — lets you trigger the workflow manually from the GitHub Actions UI, useful for on-demand runs without a commit - ---- - -## 🏗️ Three Jobs - -The workflow has three jobs that run in sequence: - -``` -test (matrix: chromium × firefox × webkit) - │ - ├── report (always, after test) - └── comment-pr (pull_request only, after test) -``` - -### Job 1 — `test`: Run Tests on All Browsers - -```yaml -jobs: - test: - name: Run Playwright Tests - timeout-minutes: 60 - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - project: [chromium, firefox, webkit] -``` - -**`timeout-minutes: 60`** — full E2E suites including OIDC login flows can be slow. 60 minutes provides enough headroom. - -**`fail-fast: false`** — without this, if Chromium tests fail, GitHub Actions would cancel the Firefox and WebKit runs. With `fail-fast: false`, all three browsers run to completion regardless of failures. This is important: a test that fails on Firefox but passes on Chromium is a real cross-browser bug worth knowing about. - -**Matrix strategy** creates three parallel jobs — one per browser — each running independently. Total elapsed time is roughly the time for one browser run, not three. - -### Steps Inside the test Job - -```yaml -steps: - - name: Checkout code - 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: Install Playwright Browsers - run: npx playwright install --with-deps ${{ matrix.project }} - - - name: Run Playwright tests - run: npx playwright test --project=${{ matrix.project }} - env: - CI: true -``` - -**`npm ci`** instead of `npm install` — `npm ci` uses the exact versions from `package-lock.json` without updating it. Reproducible installs, faster in CI. - -**`npx playwright install --with-deps ${{ matrix.project }}`** — installs only the browser for the current matrix value (`chromium`, `firefox`, or `webkit`), plus its OS-level dependencies (fonts, libraries). Installing all three on every runner would triple the time; the matrix handles parallelism instead. - -**`env: CI: true`** — this single environment variable changes several behaviors in `playwright.config.ts`. - -### Artifact Upload - -```yaml -- name: Upload HTML Report - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report-${{ matrix.project }} - path: playwright-report/ - retention-days: 30 - -- name: Upload Test Artifacts (on failure) - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-artifacts-${{ matrix.project }} - path: | - test-results/ - playwright-report/ - retention-days: 30 - -- name: Upload JUnit Results - uses: actions/upload-artifact@v4 - if: always() - with: - name: junit-results-${{ matrix.project }} - path: test-results/junit.xml - retention-days: 30 -``` - -**Three upload steps with different conditions:** - -* **HTML report** — `if: always()` — uploaded regardless of test outcome. The HTML report is the developer-friendly view of results. -* **Test artifacts** — `if: failure()` — uploaded only when tests fail. Includes traces (`.zip` files that let you replay failed tests step-by-step in Playwright's trace viewer), screenshots, and videos. The `retain-on-failure` settings in `playwright.config.ts` ensure these are captured. -* **JUnit XML** — `if: always()` — needed by the `report` job. Named with `${{ matrix.project }}` so all three browsers' results are available. - ---- - -## ⚙️ How playwright.config.ts Adapts to CI - -The `CI` environment variable changes three settings: - -```typescript -export default defineConfig({ - // Fail the build if test.only was accidentally left in - forbidOnly: !!process.env.CI, - - // Retry failed tests twice on CI (no retries locally) - retries: process.env.CI ? 2 : 0, - - // Use a single worker on CI (prevent resource contention) - workers: process.env.CI ? 1 : undefined, -``` - -**`forbidOnly: true` on CI** — `test.only()` is useful locally for focusing on one test, but if someone commits it accidentally, only that test runs and everything else is skipped. `forbidOnly: true` makes the CI run fail immediately if any `test.only()` is found in the codebase. - -**`retries: 2`** — CI runners have more environmental instability than local machines (network latency, shared resources, timing). Two retries allow genuinely flaky tests to self-heal without masking real failures. Playwright marks a test as `flaky` if it failed then passed on retry, which is visible in the HTML report. - -**`workers: 1`** — locally, `undefined` lets Playwright use as many workers as your CPU has cores, running tests in parallel. On CI, a single worker prevents tests from interfering with each other via shared state (browser storage, the Angular app's state, etc.). This makes CI runs slower but more stable. - -### Four Reporters - -```typescript -reporter: [ - ['html', { outputFolder: 'playwright-report', open: 'never' }], - ['json', { outputFile: 'test-results/results.json' }], - ['junit', { outputFile: 'test-results/junit.xml' }], - ['list'], // Console output during test execution -], -``` - -* **`html`** — generates a rich interactive report at `playwright-report/index.html`. `open: 'never'` prevents it from automatically opening a browser (which would fail on a headless CI runner). -* **`json`** — machine-readable results for programmatic analysis or custom dashboards. -* **`junit`** — XML format compatible with most CI test reporting tools, including the `dorny/test-reporter` action used in the `report` job. -* **`list`** — prints test names and results to the console as they run, visible in the Actions log. - -### Trace, Video, and Screenshot - -```typescript -use: { - trace: 'on-first-retry', // Capture trace on first retry - video: 'retain-on-failure', // Keep video only for failed tests - screenshot: 'only-on-failure', // Capture screenshot on failure -} -``` - -These three settings work with the `test artifacts` upload step: - -* **Traces** — generated on the first retry of a failing test. A trace is a zip file containing every browser action (network requests, DOM changes, screenshots at each step). Open with `npx playwright show-trace trace.zip` for a step-by-step replay. -* **Videos** — recorded for every test, but only kept when a test fails. Passing tests' videos are discarded to save storage. -* **Screenshots** — captured automatically at the moment a test fails. - ---- - -## 📊 Job 2 — `report`: Publish JUnit Results - -```yaml -report: - name: Publish Test Report - needs: test - runs-on: ubuntu-latest - if: always() - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: all-reports - - - name: Publish Test Report - uses: dorny/test-reporter@v1 - if: always() - with: - name: Playwright Test Results - path: 'all-reports/junit-results-*/junit.xml' - reporter: java-junit - fail-on-error: false -``` - -`dorny/test-reporter` reads the JUnit XML files from all three browsers (the glob `junit-results-*/junit.xml` matches all three) and publishes them as a "Check" on the commit or PR. The result appears as a collapsible section in the GitHub Actions UI showing pass/fail counts per test, browser, and file — without needing to download and open the HTML report. - -`fail-on-error: false` means a failure in the reporting step doesn't mark the overall workflow as failed. - ---- - -## 💬 Job 3 — `comment-pr`: Auto-Comment on Pull Requests - -```yaml -comment-pr: - name: Comment on PR - needs: test - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && always() - permissions: - pull-requests: write - - steps: - - name: Comment test results on PR - uses: actions/github-script@v7 - with: - script: | - let comment = '## 🎭 Playwright Test Results\n\n'; - comment += '| Browser | Status |\n'; - comment += '|---------|--------|\n'; - - const browsers = ['chromium', 'firefox', 'webkit']; - const status = '${{ needs.test.result }}' === 'success' ? '✅ Passed' : '❌ Failed'; - - for (const browser of browsers) { - comment += `| ${browser} | ${status} |\n`; - } - - comment += '\n📊 [View detailed report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})'; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: comment - }); -``` - -On every pull request, this job posts a comment with a browser-by-browser status table and a link to the full HTML report. The reviewer sees immediately whether all three browsers passed without leaving the PR page. - -`permissions: pull-requests: write` is required for the bot to post comments. This follows GitHub's least-privilege principle — the job declares only the permissions it needs. - ---- - -## 🚧 The Three-Service Challenge - -The workflow as written has a critical gap: **it doesn't start IdentityServer, the .NET API, or the Angular app**. Before any Playwright test can run, all three must be reachable. Without them, every test fails at the first `page.goto('/')`. - -There are three approaches to running the full stack in CI: - -### Approach 1 — Playwright `webServer` Configuration - -`playwright.config.ts` has a commented-out `webServer` block: - -```typescript -// webServer: { -// command: 'npm run start', -// url: 'http://localhost:4200', -// reuseExistingServer: !process.env.CI, -// timeout: 120 * 1000, -// }, -``` - -Uncomment and expand this to start all three services before tests: - -```typescript -webServer: [ - { - command: 'cd ../../TokenService/Duende-IdentityServer/src/Duende.STS.Identity && dotnet run', - url: 'https://localhost:44310', - reuseExistingServer: !process.env.CI, - timeout: 120_000, - ignoreHTTPSErrors: true, - }, - { - command: 'cd ../../ApiResources/TalentManagement-API && dotnet run', - url: 'https://localhost:44378/health', - reuseExistingServer: !process.env.CI, - timeout: 120_000, - ignoreHTTPSErrors: true, - }, - { - command: 'cd ../../Clients/TalentManagement-Angular-Material/talent-management && npm start', - url: 'http://localhost:4200', - reuseExistingServer: !process.env.CI, - timeout: 120_000, - }, -], -``` - -Playwright waits for each URL to respond before running tests. `reuseExistingServer: !process.env.CI` means locally, if the service is already running, it reuses it; in CI, it always starts fresh. - -**Workflow additions** for Approach 1: - -```yaml -- name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - -- name: Restore .NET dependencies - run: | - cd ../../ApiResources/TalentManagement-API - dotnet restore - cd ../../TokenService/Duende-IdentityServer - dotnet restore - -- name: Setup Node.js for Angular - uses: actions/setup-node@v4 - with: - node-version: '20' - -- name: Install Angular dependencies - run: | - cd ../../Clients/TalentManagement-Angular-Material/talent-management - npm ci -``` - -### Approach 2 — Docker Compose - -All three services have Docker support. A `docker-compose.yml` at the repository root could start all three together: - -```yaml -# In the workflow -- name: Start services with Docker Compose - run: docker compose up -d - -- name: Wait for services to be ready - run: | - npx wait-on https://localhost:44310 --timeout 60000 --httpTimeout 60000 - npx wait-on https://localhost:44378/health --timeout 60000 --httpTimeout 60000 - npx wait-on http://localhost:4200 --timeout 120000 -``` - -This is cleaner for CI because Docker handles service isolation and startup order. The downside is longer image pull times on the first run. - -### Approach 3 — Separate Service Jobs - -Use GitHub Actions service containers or separate jobs to start each service, then run tests in the final job once all dependencies are healthy. This requires converting the services to containerized applications and is the most complex but most scalable approach. - ---- - -## 🔐 Self-Signed Certificates in CI - -The dev stack uses self-signed HTTPS certificates. On a CI runner, there's no browser trust store with those certificates. Two places handle this: - -**In `playwright.config.ts`:** -```typescript -use: { - ignoreHTTPSErrors: true, -} -``` -Already set globally. - -**In webServer config:** -```typescript -{ - ignoreHTTPSErrors: true, // required for HTTPS health check URLs -} -``` - -**In .NET startup:** When running the API in CI, the Kestrel development certificate may not be trusted. Either use HTTP in CI (change the `launchSettings.json` profile) or add a step to trust the .NET dev certificate: - -```yaml -- name: Trust .NET development certificate - run: dotnet dev-certs https --trust -``` - -On Linux CI runners, `--trust` is a no-op — Linux doesn't have a system certificate store that `dotnet dev-certs` manages. Instead, set an environment variable to skip certificate validation at the .NET level, or switch the API to HTTP for CI. - ---- - -## 🎯 Running Specific Projects - -The matrix runs all three browsers. For faster feedback on branches where you only need smoke tests, you can target a single browser: - -```bash -# In the workflow, use the matrix value -npx playwright test --project=${{ matrix.project }} - -# Or skip the matrix and always run chromium only -npx playwright test --project=chromium -``` - -The `playwright.config.ts` also defines an `api` project for direct API tests that don't need a browser: - -```typescript -{ - name: 'api', - testMatch: /tests\/api\/.*\.spec\.ts/, - use: { - baseURL: APP_URLS.api, - extraHTTPHeaders: { 'Accept': 'application/json' }, - }, -}, -``` - -Add `api` to the matrix to run API tests in parallel with browser tests: - -```yaml -matrix: - project: [chromium, firefox, webkit, api] -``` - ---- - -## 📁 What the Workflow Produces - -After a successful run, the Actions page shows: - -**Artifacts:** -* `playwright-report-chromium` — HTML report, downloadable as a zip -* `playwright-report-firefox` -* `playwright-report-webkit` -* `junit-results-chromium` — JUnit XML, consumed by `dorny/test-reporter` -* `junit-results-firefox` -* `junit-results-webkit` -* `test-artifacts-*` — traces, screenshots, videos (only uploaded on failure) - -**Checks:** -* `Playwright Test Results` — the `dorny/test-reporter` check, shows pass/fail counts directly in the PR checks section - -**PR Comment** (on pull_request trigger): -``` -## 🎭 Playwright Test Results - -| Browser | Status | -|---------|--------| -| chromium | ✅ Passed | -| firefox | ✅ Passed | -| webkit | ✅ Passed | - -📊 View detailed report -``` - ---- - -## 🔑 Key Design Decisions - -**`fail-fast: false` with browser matrix.** Cross-browser bugs only surface when all browsers run. Cancelling Firefox because Chromium failed hides real issues. - -**Upload artifacts `if: always()`** for reports. If tests fail, you still need the report to diagnose why. Upload conditions match the value of the artifacts: reports always, failure evidence only on failure. - -**`forbidOnly: !!process.env.CI`** catches the accidental `test.only()` commit before it silently skips most of the test suite. - -**`retries: 2` compensates for CI instability** without hiding real bugs. Playwright marks retried-then-passed tests as `flaky` in the HTML report, giving you visibility into which tests need stabilization. - -**`workers: 1` on CI prevents test interference.** OIDC sessions, browser storage, and Angular state are shared resources. Parallel workers can corrupt each other's sessions. Single-worker CI trades speed for stability. - ---- - -## 🌟 Why This Matters - -A Playwright test suite that only runs locally isn't a CI/CD test suite — it's a manual checklist. GitHub Actions with browser matrix, artifact upload, JUnit reporting, and PR comments turns the same tests into an automated quality gate that runs on every push. The `process.env.CI` adaptations — `forbidOnly`, `retries: 2`, `workers: 1` — make the same test suite stable in CI without changing a single test. - -The three-service startup challenge — IdentityServer, .NET API, and Angular must all be running before the first test — is the configuration problem that stops most teams from running E2E tests in CI. The `webServer` array approach in `playwright.config.ts` solves it declaratively, making the service startup reproducible across any developer's CI environment. - -**Transferable skills:** - -* **GitHub Actions browser matrix for Playwright** — Applicable to any Playwright test suite that needs cross-browser CI coverage -* **Multi-service startup with `webServer` array** — Foundation for running E2E tests in CI against a full stack of interdependent services -* **JUnit XML reporting with PR comments** — Pattern for surfacing test results directly in GitHub pull requests without leaving the review UI - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #githubactions #playwright #cicd #testautomation #angular #dotnet #devops #e2etesting #typescript #continuousintegration #oauth2 #webdevelopment #qa #testing #automation diff --git a/blogs/series-5-devops-data/5.3-azure-subscription-setup.md b/blogs/series-5-devops-data/5.3-azure-subscription-setup.md deleted file mode 100644 index ce104c0..0000000 --- a/blogs/series-5-devops-data/5.3-azure-subscription-setup.md +++ /dev/null @@ -1,298 +0,0 @@ -# Your First Azure Deployment: Setting Up a Visual Studio Subscription - -## Activate Your $50 Monthly Credit, Install the Azure CLI, and Understand What Fits in Your Budget - -Most Azure tutorials assume you already have an account, a subscription, and money to spend. If you have a Visual Studio Professional or Enterprise subscription, you already have a monthly Azure credit — and it's enough to run this entire three-tier application. You just have to activate it. - -This article walks through activating the benefit, setting a spending limit so you can never accidentally overspend, installing the Azure CLI and Bicep, and understanding exactly which Azure resources fit within a $50/month budget. - -![Azure Portal Dashboard](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-api-endpoints.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article is the first in the Azure Deployment sub-series — it gets your Azure environment ready before any infrastructure is provisioned.** - ---- - -## 📚 What You'll Learn - -* **Visual Studio Azure benefit** — where the credit comes from and how to activate it -* **Spending limit** — the one setting that prevents accidental charges beyond your credit -* **Azure CLI** — how to install it and verify you're targeting the right subscription -* **Bicep CLI** — the infrastructure-as-code tool used in the next article -* **Cost profile** — what the full three-tier stack costs per month and why it fits in $50 - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Visual Studio Professional or Enterprise subscription** — includes Azure monthly credit -* **Windows 10/11 or macOS** — Azure CLI runs on both -* **winget (Windows) or Homebrew (macOS)** — for CLI installation - -**Not sure if you have a Visual Studio subscription?** Check at [my.visualstudio.com](https://my.visualstudio.com). If your employer or school provides it, your subscription is listed there. - ---- - -## 🎯 The Problem - -Azure has a free trial, but it expires after 30 days and caps some services. If you have a Visual Studio Professional subscription, you have a better option: a monthly Azure credit that renews every month and never expires as long as your subscription is active. - -**Common pain points for first-time Azure users:** - -* **Credit vs. billing confusion** — the benefit doesn't automatically prevent charges above the credit limit unless you set a spending limit -* **Too many subscription types** — Pay-As-You-Go, Free Trial, and Visual Studio Dev Essentials all look similar in the portal but behave differently -* **Azure CLI not installed** — the remaining articles in this sub-series use `az` commands; the portal won't be enough -* **Wrong subscription selected** — if you have multiple subscriptions, CLI commands may target the wrong one - ---- - -## 💡 The Solution - -Activate the Visual Studio Azure benefit, set a $0 spending limit above the credit, install the tools, and verify you're pointing at the right subscription. This takes about 15 minutes and you only do it once. - -**What you get:** - -* ✅ **Visual Studio Professional** — $50/month Azure credit -* ✅ **Visual Studio Enterprise** — $150/month Azure credit -* ✅ **Credit renews monthly** — never expires while subscription is active -* ✅ **Spending limit prevents overruns** — services pause when credit runs out, no bill - ---- - -## 🚀 How It Works - -### Step 1: Check Your Visual Studio Subscription - -Go to [my.visualstudio.com/benefits](https://my.visualstudio.com/benefits) and log in with the account associated with your Visual Studio subscription. - -Look for the **Azure** tile under the "Tools" section. It shows your monthly credit amount: - -* **Professional** — $50/month -* **Enterprise** — $150/month -* **Dev Essentials** — $200 one-time free trial (not a monthly credit) - -If you do not see an Azure tile, your subscription level may not include the Azure benefit. Check [Visual Studio subscription comparison](https://visualstudio.microsoft.com/vs/compare/) to confirm. - -### Step 2: Activate the Azure Benefit - -Click **Activate** on the Azure tile at my.visualstudio.com/benefits. - -This redirects you to the Azure sign-up page. Use the **same Microsoft account** that holds your Visual Studio subscription — mixing accounts creates a separate, unlinked subscription. - -After activation, you land in the Azure Portal at [portal.azure.com](https://portal.azure.com). Your subscription appears in **Subscriptions** and has a name like `Visual Studio Professional`. - -**Wait for:** The subscription status to show **Active** before proceeding. - -### Step 3: Set a Spending Limit - -This is the most important step. By default, Azure pauses services when the monthly credit runs out — but the setting must be confirmed. - -In the Azure Portal: - -``` -Search bar → "Subscriptions" → select your Visual Studio subscription -→ "Cost Management" section → "Spending limit" -→ Confirm spending limit is ON (set to $0 above credit) -``` - -**What happens when credit runs out:** -* Services are suspended (not deleted) — you get an email warning first -* Everything restarts automatically when the new monthly credit is applied -* No credit card is charged unless you explicitly remove the spending limit - -> **Never remove the spending limit** unless you intend to pay beyond the credit. For this tutorial, the full stack costs approximately $23/month — well within the $50 credit. - -### Step 4: Install the Azure CLI - -The Azure CLI (`az`) is the command-line tool used throughout this sub-series to deploy resources, run Bicep templates, and configure app settings. - -**Windows (winget):** - -```bash -winget install Microsoft.AzureCLI -``` - -**macOS (Homebrew):** - -```bash -brew install azure-cli -``` - -**Verify the installation:** - -```bash -az version -``` - -**Expected output:** a JSON block showing the CLI version (2.60 or higher recommended). - -### Step 5: Log In to Azure - -```bash -az login -``` - -A browser window opens to the Microsoft login page. Sign in with the same account used to activate the Azure benefit. After login, the terminal lists your available subscriptions. - -**Expected output:** - -```json -[ - { - "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "name": "Visual Studio Professional", - "state": "Enabled", - "isDefault": true - } -] -``` - -If `isDefault` is `true` for your Visual Studio subscription, you're ready. If you have multiple subscriptions and the wrong one is selected, set the correct one explicitly. - -### Step 6: Set the Target Subscription - -If you have more than one subscription, ensure every `az` command targets the Visual Studio subscription: - -```bash -# List all subscriptions -az account list --output table - -# Set the correct subscription by name -az account set --subscription "Visual Studio Professional" - -# Confirm the active subscription -az account show --query "{name:name, id:id, state:state}" --output table -``` - -**Why this matters:** Every Bicep deployment and role assignment in subsequent articles uses the active subscription. Targeting the wrong one wastes credit and may not have the right permissions. - -### Step 7: Install the Bicep CLI - -Bicep is the infrastructure-as-code language used in Article 5.4 to provision all Azure resources. It is included with Azure CLI 2.20 and later, but installing or updating it explicitly ensures you have the latest version: - -```bash -az bicep install - -# Verify -az bicep version -``` - -**Expected output:** `Bicep CLI version 0.28.x` or higher. - -### Step 8: Verify the Complete Setup - -Run these three commands to confirm everything is in place: - -```bash -# Azure CLI version -az version --query '"azure-cli"' --output tsv - -# Active subscription -az account show --query "{name:name, id:id}" --output table - -# Bicep version -az bicep version -``` - -All three should return values without errors. If `az account show` shows the wrong subscription, re-run Step 6. - ---- - -## 💻 Try It Yourself - -After completing the setup, explore your subscription from the terminal: - -```bash -# List resource groups (empty at this point — that's expected) -az group list --output table - -# Check available regions (useful for choosing where to deploy) -az account list-locations --query "[].{Name:name, DisplayName:displayName}" --output table | head -20 -``` - -No resources are provisioned yet. Article 5.4 writes the Bicep templates and runs the deployment command that creates all seven Azure resources at once. - ---- - -## 📊 Budget: What Fits in $50/Month - -The full three-tier stack uses these Azure resources. Here is the approximate monthly cost for the dev environment: - -**Compute:** - -* **App Service Plan B1** — ~$13.14/month (hosts both the .NET API and IdentityServer) -* Note: Azure charges at the App Service Plan level, not per Web App. Both apps share the plan at no additional cost. - -**Databases:** - -* **Azure SQL Basic tier (5 DTUs)** — ~$4.90/month per database -* Two databases (API + IdentityServer) — ~$9.80/month total - -**Frontend:** - -* **Azure Static Web Apps Free tier** — $0/month (built-in CDN and global distribution included) - -**Total estimate:** approximately **$23/month** — comfortably within the $50 Visual Studio Professional credit. - -**What would exceed the budget:** - -* Upgrading to B2 or B3 App Service Plan (doubles or quadruples the compute cost) -* Using Standard SQL tier instead of Basic (10× more expensive) -* Adding a second App Service Plan instead of sharing one -* Premium networking features (Private Link, VNet integration) - -The architecture in this sub-series is deliberately designed to stay within $23/month. Upgrading to a larger SQL tier or higher compute is straightforward later if the application grows. - ---- - -## 🔑 Key Design Decisions - -**Visual Studio subscription, not Pay-As-You-Go.** The VS subscription benefit provides a monthly credit with a spending limit guardrail. Pay-As-You-Go has no spending limit by default, which makes accidental overruns possible. For a development environment, the VS benefit is the safest starting point. - -**One subscription for dev.** Use a single Azure subscription for the dev environment. Do not mix dev and production resources in the same subscription — they share the credit and the spending limit. When a production environment is needed, a separate subscription (or a separate resource group under a paid subscription) keeps billing isolated. - -**Set the spending limit before deploying anything.** Provisioning resources before confirming the spending limit means any misconfiguration (leaving a resource running, choosing the wrong SKU) immediately starts consuming credit without a safety net. Confirm the limit first. - -**`az account set` before every session.** The active subscription persists between terminal sessions, but if you switch between subscriptions during the day, always confirm with `az account show` before running deployment commands. - -**eastus as the default region.** Azure pricing is consistent across major US regions (eastus, westus2, centralus). The articles in this sub-series use `eastus` as the default. Choose the region closest to your users for production. - ---- - -## 🌟 Why This Matters - -Getting the Azure environment right before writing a single line of infrastructure code prevents the most common source of frustration for first-time deployers: targeting the wrong subscription, hitting unexpected charges, or discovering a missing tool in the middle of a deployment. - -The Visual Studio Azure benefit is one of the most underused perks of a VS subscription. It provides a real Azure environment — not a sandbox with restricted services — that renews every month. The $50/month credit for Professional subscribers is enough to run a complete three-tier application stack indefinitely for development and demo purposes. - -**Transferable skills:** - -* **`az account set` + `az account show`** — the pattern for managing multiple subscriptions applies to any Azure project, not just this one -* **Spending limit strategy** — applicable to any team Azure account where you want cost guardrails during development -* **Bicep CLI installation** — a one-time prerequisite for any project that uses infrastructure-as-code - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #azure #dotnet #angular #visualstudio #devops #clouddeployment #azurecli #bicep #infrastructure #webdevelopment #fullstack #csharp #typescript #oauth2 #identityserver diff --git a/blogs/series-5-devops-data/5.4-azure-bicep-infrastructure.md b/blogs/series-5-devops-data/5.4-azure-bicep-infrastructure.md deleted file mode 100644 index 74e5a06..0000000 --- a/blogs/series-5-devops-data/5.4-azure-bicep-infrastructure.md +++ /dev/null @@ -1,595 +0,0 @@ -# Infrastructure as Code: Provision All Azure Resources with One Bicep Command - -## From Empty Subscription to App Services, SQL Server, Static Web App, and Key Vault in Minutes - -Clicking through the Azure Portal to create resources manually is slow, error-prone, and impossible to reproduce. Miss a setting on the SQL Server firewall and the API can't connect to the database. Store a connection string in a config file and it ends up in source control. Choose the wrong SKU on the App Service Plan and costs double. Do it again for a new environment and you do it from memory. - -Azure Bicep solves this. One file describes the desired state. One command creates everything. Run it again and Azure updates only what changed. The same template creates both a dev environment and a production environment identically. - -This article walks through the Bicep templates that provision the full Talent Management stack — App Service Plan, three Web Apps, a Static Web App, a shared SQL Server, two databases, and an Azure Key Vault — and runs the deployment via a GitHub Actions workflow. - -![Azure Resources Created](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-api-endpoints.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article provisions the Azure infrastructure for deploying the three-tier stack. Article 5.3 covers the subscription setup prerequisite.** - ---- - -## 📚 What You'll Learn - -* **What Bicep is** — declarative IaC that compiles to ARM JSON and deploys via Azure CLI -* **Module composition** — how `main.bicep` references five reusable modules -* **`@secure()` parameters** — how to pass SQL passwords without storing them in files -* **System-assigned managed identity** — how Web Apps get an Azure AD identity for passwordless access -* **Azure Key Vault** — how to store secrets centrally and grant apps access via RBAC -* **Key Vault references in App Service** — how to inject secrets into app settings without hardcoding values -* **CAF naming convention** — why resources are named `app-talent-api-dev` and not `myapp1` -* **Bicep outputs** — how deployed URLs flow into GitHub Actions workflows -* **GitHub Actions workflow** — how to deploy infrastructure on-demand via `workflow_dispatch` -* **Service principal permissions** — why Contributor alone isn't enough and how to grant role assignment capability -* **Estimated cost** — ~$23/month for the full dev stack, and how to reduce it to $0 - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 5.3 complete** — Azure CLI installed, logged in, correct subscription active -* **Resource group created** — `rg-talent-dev` in your target region -* **`SQL_ADMIN_PASSWORD` chosen** — a strong password, stored as a GitHub Secret - -**Create the resource group now:** - -```bash -az group create \ - --name rg-talent-dev \ - --location eastus -``` - ---- - -## 🎯 The Problem - -The Azure Portal is a point-and-click interface. Every decision you make — which SKU, which region, which firewall rule — exists only in Azure, not in source control. If a teammate needs to set up the same environment, they repeat every click from memory. If a resource is accidentally deleted, there is no record of its original configuration. - -Worse: connection strings. The typical approach puts database credentials directly into App Service configuration settings, visible to anyone with portal access. One leaked screenshot, one accidentally logged environment variable, and credentials are exposed. - -**Pain points with manual Portal deployment:** - -* **No repeatability** — no way to recreate an environment identically -* **No history** — changes to resources aren't tracked in git -* **No review** — no pull request for a resource configuration change -* **Slow** — creating nine resources manually takes 30+ minutes -* **Insecure** — secrets in App Service settings are visible in plain text in the Portal - ---- - -## 💡 The Solution - -Azure Bicep is a declarative language for defining Azure resources. You write what you want, not how to create it. Azure figures out the creation order, handles dependencies, and only updates what changed on subsequent runs. - -**Azure Key Vault** stores secrets (connection strings, API keys, passwords) in a managed vault. Instead of pasting a connection string into an App Service setting, you store it once in Key Vault and reference it with a `@Microsoft.KeyVault(...)` syntax. The App Service resolves the reference at runtime — the actual value is never visible in the Portal or in source control. - -**System-assigned managed identity** gives each Web App an Azure AD identity automatically — no service principal to create, no client secret to rotate. The Key Vault module grants each identity the `Key Vault Secrets User` role, which allows reading secrets with no credentials at all. - -**Key benefits:** - -* ✅ **Reproducible** — same template creates identical environments every time -* ✅ **Version-controlled** — infrastructure changes go through pull requests -* ✅ **Idempotent** — running the same template twice is safe -* ✅ **Fast** — one command replaces 30 minutes of Portal clicking -* ✅ **Secure** — secrets live in Key Vault, never in config files or App Service settings - ---- - -## 🚀 How It Works - -### The File Structure - -All infrastructure code lives in the `infra/` folder at the root of the repository: - -``` -infra/ -├── main.bicep ← entry point, composes all modules -├── modules/ -│ ├── appServicePlan.bicep ← B1 App Service Plan -│ ├── webApp.bicep ← reusable Web App with managed identity -│ ├── staticWebApp.bicep ← Angular client, Free tier -│ ├── sqlServer.bicep ← logical server + 2 databases -│ └── keyVault.bicep ← Key Vault + RBAC role assignments -└── parameters/ - └── dev.bicepparam ← dev environment parameter values -``` - -### Step 1: Understand main.bicep - -`main.bicep` is the entry point. It declares all parameters, calls each module, and exposes outputs that downstream workflows use. - -```bicep -// infra/main.bicep - -@description('SQL administrator password — pass at deploy time, never in parameters file') -@secure() -param sqlAdminPassword string - -@description('Name of the Azure Key Vault') -param keyVaultName string - -module appServicePlan 'modules/appServicePlan.bicep' = { ... } - -module apiApp 'modules/webApp.bicep' = { - name: 'apiApp' - params: { - webAppName: apiAppName - location: location - appServicePlanId: appServicePlan.outputs.id - } -} - -module identityApp 'modules/webApp.bicep' = { ... } -module identityAdminApp 'modules/webApp.bicep' = { ... } -module angularSwa 'modules/staticWebApp.bicep' = { ... } -module sqlServer 'modules/sqlServer.bicep' = { ... } - -module keyVault 'modules/keyVault.bicep' = { - name: 'keyVault' - params: { - keyVaultName: keyVaultName - location: location - readerPrincipalIds: [ - apiApp.outputs.principalId - identityApp.outputs.principalId - identityAdminApp.outputs.principalId - ] - } -} - -output apiAppUrl string = apiApp.outputs.url -output identityAppUrl string = identityApp.outputs.url -output identityAdminAppUrl string = identityAdminApp.outputs.url -output angularAppUrl string = angularSwa.outputs.url -output sqlServerFqdn string = sqlServer.outputs.sqlServerFqdn -output keyVaultUri string = keyVault.outputs.uri -``` - -**`@secure()` on `sqlAdminPassword`** — Bicep marks this parameter as sensitive. The value is never written to deployment logs, never stored in the parameters file, and never appears in `az deployment group show` output. - -**Module calls reference each other's outputs.** `apiApp` receives `appServicePlan.outputs.id`. The Key Vault module receives `apiApp.outputs.principalId`. Bicep resolves dependency order automatically. - -### Step 2: The App Service Plan Module - -```bicep -// infra/modules/appServicePlan.bicep - -resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = { - name: appServicePlanName - location: location - sku: { - name: 'B1' - tier: 'Basic' - } - properties: { - reserved: false // Windows (not Linux) - } -} - -output id string = appServicePlan.id -``` - -**B1 SKU** — the Basic B1 tier is the lowest dedicated (non-shared) App Service tier. It costs approximately $13/month and supports custom domains, always-on, and connection slots. The Free (F1) and Shared (D1) tiers are not suitable for production use. - -**One plan, three apps** — Azure charges at the App Service Plan level, not per Web App. The API, IdentityServer STS, and IdentityServer Admin all run on this same plan with no additional cost. - -### Step 3: The Web App Module (with Managed Identity) - -```bicep -// infra/modules/webApp.bicep - -resource webApp 'Microsoft.Web/sites@2023-01-01' = { - name: webAppName - location: location - identity: { - type: 'SystemAssigned' - } - properties: { - serverFarmId: appServicePlanId - httpsOnly: true - siteConfig: { - netFrameworkVersion: 'v10.0' - http20Enabled: true - minTlsVersion: '1.2' - ftpsState: 'Disabled' - } - } -} - -output id string = webApp.id -output url string = 'https://${webApp.properties.defaultHostName}' -output principalId string = webApp.identity.principalId -``` - -**`identity: { type: 'SystemAssigned' }`** — Azure creates an Azure AD service principal for this Web App automatically. The identity is tied to the app's lifecycle — it is created and deleted with the app. No client secret or certificate to manage. - -**`output principalId`** — exposes the managed identity's object ID so the Key Vault module can grant it access. This is the Bicep way of wiring resources together without hardcoding IDs. - -**`httpsOnly: true`** — redirects all HTTP traffic to HTTPS. Non-negotiable for an authentication-aware application. - -**`ftpsState: 'Disabled'`** — disables FTP/FTPS deployment. GitHub Actions is the only deployment path. - -### Step 4: The Static Web App Module - -```bicep -// infra/modules/staticWebApp.bicep - -resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = { - name: staticWebAppName - location: location - sku: { - name: 'Free' - tier: 'Free' - } - properties: { - buildProperties: { - skipGithubActionWorkflowGeneration: true - } - } -} - -output url string = 'https://${staticWebApp.properties.defaultHostname}' -``` - -**Free tier** — $0/month with built-in CDN, global distribution, custom domains, and managed TLS certificates. Purpose-built for SPAs like Angular. - -**`skipGithubActionWorkflowGeneration: true`** — prevents Azure from auto-generating a GitHub Actions workflow file. Article 5.7 writes a custom workflow that injects environment variables at build time. - -### Step 5: The SQL Server Module - -```bicep -// infra/modules/sqlServer.bicep - -resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = { - name: sqlServerName - location: location - properties: { - administratorLogin: sqlAdminLogin - administratorLoginPassword: sqlAdminPassword - minimalTlsVersion: '1.2' - } -} - -// Allow Azure services (App Service) to connect -resource allowAzureServices 'Microsoft.Sql/servers/firewallRules@2023-05-01-preview' = { - parent: sqlServer - name: 'AllowAllWindowsAzureIps' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } -} - -resource apiDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = { - parent: sqlServer - name: apiDbName - sku: { - name: 'Basic' - tier: 'Basic' - capacity: 5 // 5 DTUs - } -} -``` - -**`AllowAllWindowsAzureIps`** — the special Azure firewall rule (`0.0.0.0` to `0.0.0.0`) allows connections from all Azure services, including App Service. - -**Basic 5-DTU tier** — approximately $4.90/month per database. Sufficient for development and demo workloads. - -### Step 6: The Key Vault Module - -```bicep -// infra/modules/keyVault.bicep - -resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { - name: keyVaultName - location: location - properties: { - sku: { - family: 'A' - name: 'standard' - } - tenantId: subscription().tenantId - enableRbacAuthorization: true - enableSoftDelete: true - softDeleteRetentionInDays: 7 - } -} - -// Grant each managed identity the Key Vault Secrets User role (read secrets) -resource secretsUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (principalId, i) in readerPrincipalIds: { - scope: keyVault - name: guid(keyVault.id, principalId, '4633458b-17de-408a-b874-0445c86b69e6') - properties: { - // Key Vault Secrets User built-in role ID - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') - principalId: principalId - principalType: 'ServicePrincipal' - } -}] - -output id string = keyVault.id -output name string = keyVault.name -output uri string = keyVault.properties.vaultUri -``` - -**`enableRbacAuthorization: true`** — uses Azure RBAC for access control instead of the legacy vault access policies. RBAC is the recommended model — permissions are managed with the same `az role assignment` commands used everywhere else in Azure. - -**`Key Vault Secrets User` role** — grants read-only access to secret values. The role definition ID `4633458b-17de-408a-b874-0445c86b69e6` is a well-known built-in role. Each Web App's managed identity is granted this role scoped to the vault. - -**`enableSoftDelete: true`** — deleted secrets and vaults are retained for 7 days and can be recovered. Protects against accidental deletion of production secrets. - -**`guid(keyVault.id, principalId, roleId)`** — generates a deterministic, unique ID for each role assignment. Running the deployment twice does not create duplicate assignments. - -### Step 7: Storing Secrets in Key Vault - -After deployment, store your application secrets in Key Vault: - -```bash -# Store the API database connection string -az keyvault secret set \ - --vault-name kv-talent-dev \ - --name "ConnectionStrings--DefaultConnection" \ - --value "Server=sql-talent-dev.database.windows.net;Database=sqldb-talent-api-dev;User=sqladmin;Password=YourPassword;" - -# Store the IdentityServer database connection string -az keyvault secret set \ - --vault-name kv-talent-dev \ - --name "ConnectionStrings--ConfigurationDbConnection" \ - --value "Server=sql-talent-dev.database.windows.net;Database=sqldb-talent-ids-dev;User=sqladmin;Password=YourPassword;" -``` - -**Note on secret naming:** Azure Key Vault secret names use hyphens (`-`), not double-underscores (`__`). When the App Service reads a secret via a Key Vault reference, it maps `ConnectionStrings--DefaultConnection` to the `ConnectionStrings:DefaultConnection` .NET configuration key automatically. - -### Step 8: Using Key Vault References in App Service - -Once secrets are stored, configure App Service settings to reference them instead of hardcoding values: - -```bash -# Get the Key Vault URI -KV_URI=$(az keyvault show --name kv-talent-dev --query properties.vaultUri --output tsv) - -# Get the secret version (use latest — omit version for always-current) -SECRET_URI="${KV_URI}secrets/ConnectionStrings--DefaultConnection" - -# Set the App Service setting as a Key Vault reference -az webapp config appsettings set \ - --resource-group rg-talent-dev \ - --name app-talent-api-dev \ - --settings "ConnectionStrings__DefaultConnection=@Microsoft.KeyVault(SecretUri=${SECRET_URI})" -``` - -The `@Microsoft.KeyVault(SecretUri=...)` syntax tells App Service to resolve the value from Key Vault at runtime. In the Portal, the setting shows a Key Vault icon and a green check (✅) when the managed identity has access and the secret exists. The actual connection string value is never visible. - -**How it works end-to-end:** - -* The Web App's system-assigned managed identity authenticates to Azure AD automatically -* App Service resolves the `@Microsoft.KeyVault(...)` reference before injecting it into the app's environment -* The .NET app reads `ConnectionStrings:DefaultConnection` — it never knows Key Vault is involved -* Rotating the secret in Key Vault takes effect on next app restart, with no code changes - -### Step 9: The Parameters File - -```bicep -// infra/parameters/dev.bicepparam - -using '../main.bicep' - -param appServicePlanName = 'asp-talent-f1-dev' -param apiAppName = 'app-talent-api-dev' -param identityAppName = 'app-talent-ids-dev' -param identityAdminAppName = 'app-talent-admin-dev' -param staticWebAppName = 'swa-talent-ui-dev' -param sqlServerName = 'sql-talent-dev' -param apiDbName = 'sqldb-talent-api-dev' -param identityDbName = 'sqldb-talent-ids-dev' -param sqlAdminLogin = 'sqladmin' -param keyVaultName = 'kv-talent-dev' - -// sqlAdminPassword is NOT here — passed at deploy time via environment variable -param sqlAdminPassword = readEnvironmentVariable('SQL_ADMIN_PASSWORD') -``` - -**CAF naming convention** — Cloud Adoption Framework abbreviations prefix each resource name: `asp-` for App Service Plan, `app-` for Web App, `swa-` for Static Web App, `sql-` for SQL Server, `sqldb-` for SQL database, `kv-` for Key Vault. The pattern is `{type}-{workload}-{qualifier}-{env}`. - -**`readEnvironmentVariable('SQL_ADMIN_PASSWORD')`** — reads the password from the runner's environment at deploy time. The value never touches source control. - -### Step 10: Deploy via GitHub Actions - -The repository includes a manual GitHub Actions workflow to deploy infrastructure on-demand: - -```yaml -# .github/workflows/deploy-infra.yml - -name: Deploy Azure Infrastructure (Bicep) - -on: - workflow_dispatch: # manual trigger only - inputs: - environment: - description: 'Target environment' - default: 'dev' - type: choice - options: - - dev - -jobs: - deploy-infrastructure: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Deploy Bicep - env: - SQL_ADMIN_PASSWORD: ${{ secrets.SQL_ADMIN_PASSWORD }} - run: | - az deployment group create \ - --resource-group rg-talent-dev \ - --template-file infra/main.bicep \ - --parameters infra/parameters/dev.bicepparam \ - --output json -``` - -**`workflow_dispatch`** — infrastructure deployments are intentional, not automatic. Triggering on push would risk unintended resource changes every time a config file is committed. - -**`SQL_ADMIN_PASSWORD` as env var** — the `dev.bicepparam` file reads this environment variable via `readEnvironmentVariable()`. Setting it in the step's `env:` block makes it available to the Bicep CLI process. - -**To run it:** Go to **GitHub → Actions → Deploy Azure Infrastructure (Bicep) → Run workflow**. - -**Required GitHub Secrets:** - -* `AZURE_CLIENT_ID` — service principal client ID (from Article 5.5) -* `AZURE_TENANT_ID` — Azure AD tenant ID -* `AZURE_SUBSCRIPTION_ID` — Azure subscription ID -* `SQL_ADMIN_PASSWORD` — SQL administrator password - -### ⚠️ One-Time Prerequisite: Grant Role Assignment Permission - -The Bicep deployment creates RBAC role assignments (granting each Web App's managed identity access to Key Vault). By default, a service principal with **Contributor** role cannot create role assignments — it also needs **User Access Administrator**. - -**Grant it in the Azure Portal:** - -1. Go to **portal.azure.com → Resource Groups → rg-talent-dev** -2. Click **Access control (IAM)** in the left menu -3. Click **+ Add → Add role assignment** -4. Select role: **User Access Administrator** → click **Next** -5. Click **+ Select members**, search for your service principal (e.g. `github-angularnettutorial`), select it -6. Click **Next** — Azure shows a role assignment condition dialog with three options: - * Allow user to only assign selected roles to selected principals (fewer privileges) - * **Allow user to assign all roles except privileged administrator roles Owner, UAA, RBAC (Recommended)** ← select this - * Allow user to assign all roles (highly privileged) -7. Click **Review + assign** - -**Why the Recommended option?** The Bicep only assigns the `Key Vault Secrets User` role — a standard, non-privileged role. The recommended option allows assigning standard roles while blocking the highest-privilege roles (Owner, User Access Administrator itself, RBAC Administrator). This follows the principle of least privilege — enough permission to wire up managed identities, not enough to escalate to subscription owner. - ---- - -## 💻 Try It Yourself - -After deployment, verify all resources exist: - -```bash -az resource list \ - --resource-group rg-talent-dev \ - --output table -``` - -You should see nine resources: one App Service Plan, three Web Apps, one Static Site, one SQL Server, two SQL Databases, and one Key Vault. Azure also lists a `master` database — this is a system database automatically created by Azure SQL and does not count as one of your provisioned resources. - -Verify the managed identities were assigned to the Web Apps: - -```bash -az webapp identity show \ - --resource-group rg-talent-dev \ - --name app-talent-api-dev \ - --query "{principalId:principalId,tenantId:tenantId,type:type}" -``` - -Verify the Key Vault role assignments: - -```bash -az role assignment list \ - --scope $(az keyvault show --name kv-talent-dev --query id --output tsv) \ - --query "[].{Principal:principalName,Role:roleDefinitionName}" \ - --output table -``` - ---- - -## 💰 Estimated Monthly Cost - -All nine resources in the dev environment cost approximately **$23/month** (East US region): - -* **App Service Plan B1** (`asp-talent-f1-dev`) — ~$13.14/month -* **Web App — .NET API** (`app-talent-api-dev`) — $0 (included in plan) -* **Web App — IdentityServer STS** (`app-talent-ids-dev`) — $0 (included in plan) -* **Web App — IdentityServer Admin** (`app-talent-admin-dev`) — $0 (included in plan) -* **Static Web App — Angular** (`swa-talent-ui-dev`) — $0 (Free tier) -* **SQL Logical Server** (`sql-talent-dev`) — $0 (no charge for the server) -* **SQL Database — API** (`sqldb-talent-api-dev`) — ~$4.90/month (Basic 5 DTU) -* **SQL Database — IdentityServer** (`sqldb-talent-ids-dev`) — ~$4.90/month (Basic 5 DTU) -* **Key Vault** (`kv-talent-dev`) — ~$0/month for dev (Standard tier; charges per operation — negligible at dev scale) - -**Total: ~$23/month** - -**Key cost facts:** - -* The B1 App Service Plan covers all three Web Apps — Azure charges per plan, not per app -* Azure Static Web Apps Free tier includes CDN, global distribution, and managed TLS at no cost -* Both SQL databases share one logical SQL Server — no extra charge for the server itself -* Key Vault Standard tier charges ~$0.03 per 10,000 operations — effectively free for a dev environment - -**To eliminate cost entirely when not in use:** - -```bash -# Delete all resources in one command (can be recreated from Bicep in minutes) -az group delete --name rg-talent-dev --yes --no-wait -``` - -**Visual Studio subscribers:** The monthly Azure credit (~$150/month for Visual Studio Enterprise, ~$50/month for Visual Studio Professional) covers this entire stack with budget to spare. Visual Studio Community does not include Azure credits. - ---- - -## 🔑 Key Design Decisions - -**System-assigned managed identity over connection string secrets in App Service.** Pasting a connection string into an App Service configuration setting works, but the value is visible in plain text in the Portal to anyone with contributor access. Managed identity + Key Vault references mean the actual secret never appears in the Portal, never appears in deployment logs, and never needs rotation when team members change. - -**RBAC over vault access policies for Key Vault.** The legacy vault access policy model is a separate permission system that doesn't integrate with Azure RBAC tooling. `enableRbacAuthorization: true` means the same `az role assignment` commands, the same Portal IAM blade, and the same Bicep role assignment resources used everywhere else in Azure. - -**One shared App Service Plan for all three .NET apps.** Azure charges at the App Service Plan level, not per Web App. Running three Web Apps on one B1 plan costs the same as running one. Separate plans would triple the compute cost for no benefit at this scale. - -**`@secure()` for SQL password — never in the parameters file.** Bicep's `@secure()` decorator marks a parameter as sensitive. The value is excluded from deployment logs and history. Using `readEnvironmentVariable()` in the parameters file means the CI runner reads it from a GitHub Secret — the value never touches source control. - -**Bicep outputs over hardcoded URLs.** `main.bicep` outputs `apiAppUrl`, `identityAppUrl`, `identityAdminAppUrl`, `angularAppUrl`, and `keyVaultUri`. GitHub Actions workflows read these outputs instead of hardcoding URLs — if the resource name changes, the URLs update automatically. - -**`workflow_dispatch` only for infrastructure.** Application code deploys on push to master because it is low risk and frequent. Infrastructure changes are infrequent and higher risk — a misconfigured SQL firewall rule can take down the entire stack. Manual trigger forces intentionality. - ---- - -## 🌟 Why This Matters - -Infrastructure as Code is not just a DevOps practice — it is documentation. Every resource configuration, every SKU choice, every firewall rule, every RBAC assignment is in source control and can be reviewed, questioned, and approved through a pull request. The Bicep files in this article are the complete specification of what is running in Azure. - -The Key Vault + managed identity pattern is the production-grade way to handle secrets in Azure. No credentials in environment variables, no credentials in App Service settings, no credentials in source control. The application code reads configuration exactly as it always has — the `ConnectionStrings:DefaultConnection` key — without knowing or caring that the value came from Key Vault. - -**Transferable skills:** - -* **Bicep module composition** — applicable to any Azure project; the same pattern of `main.bicep` + modules scales to 50-resource enterprise deployments -* **Managed identity + Key Vault** — the standard zero-credential pattern for any Azure-hosted application -* **`@secure()` parameters** — the correct way to handle any secret in any Bicep template -* **CAF naming convention** — the standard naming pattern across every Azure project that follows Microsoft's well-architected framework - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #azure #bicep #infrastructureascode #keyvault #managedidentity #dotnet #angular #devops #azurecli #appservice #azuresql #staticwebapps #clouddeployment #github #githubactions #security #secretsmanagement #fullstack #csharp #zerotrust diff --git a/blogs/series-5-devops-data/5.5-azure-oidc-github-actions.md b/blogs/series-5-devops-data/5.5-azure-oidc-github-actions.md deleted file mode 100644 index 8dbbd97..0000000 --- a/blogs/series-5-devops-data/5.5-azure-oidc-github-actions.md +++ /dev/null @@ -1,338 +0,0 @@ -# Secure CI/CD: Connect GitHub Actions to Azure Without Passwords - -## One Script Wires Up Passwordless Deployment Using OIDC Federated Identity - -Every CI/CD tutorial eventually tells you to paste an Azure credential into GitHub Secrets. It works — until you forget to rotate it, until someone with access to the secret leaves the team, until it leaks in a log file, or until it simply expires at the worst moment. Stored secrets are a liability. - -GitHub Actions and Azure both support a better model: OIDC (OpenID Connect) federated identity. GitHub issues a short-lived cryptographic token for each job. Azure trusts that token if it comes from the right repository and branch. No password is ever created. Nothing expires. Nothing leaks. - -This article walks through the `infra/scripts/setup-oidc.sh` script that wires up this trust relationship in one run, then explains exactly how GitHub Actions uses the resulting secrets in every subsequent workflow. - -![Azure OIDC GitHub Actions](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-api-endpoints.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article sets up the Azure-GitHub trust relationship used by every deployment workflow in Articles 5.6 and 5.7.** - ---- - -## 📚 What You'll Learn - -* **Why OIDC is better than stored secrets** — short-lived tokens, no rotation, no leakage risk -* **The four Azure objects the script creates** — App Registration, Service Principal, Federated Credential, role assignment -* **What each line of `setup-oidc.sh` does** — step-by-step walkthrough -* **The four GitHub Secrets** — three written automatically, one added manually -* **How workflows use the secrets** — the `azure/login` action pattern - ---- - -## 📋 Prerequisites - -**Before running this script, you should have:** - -* **Article 5.3 complete** — Azure CLI installed, logged in, correct subscription active -* **Article 5.4 complete** — `rg-talent-dev` resource group exists -* **GitHub CLI installed** — the script uses `gh secret set` to write secrets - -**Install the GitHub CLI:** - -```bash -# Windows (winget) -winget install GitHub.cli - -# macOS (Homebrew) -brew install gh -``` - -**Log in to the GitHub CLI:** - -```bash -gh auth login -``` - -Follow the interactive prompts — choose GitHub.com, HTTPS, and authenticate via browser. After login, verify: - -```bash -gh auth status -``` - -**Expected output:** `Logged in to github.com as ` - ---- - -## 🎯 The Problem - -The traditional approach to authenticating GitHub Actions with Azure is to create a Service Principal, generate a client secret, and paste that secret into GitHub repository settings as `AZURE_CREDENTIALS`. This works but has serious drawbacks: - -* **Secrets expire** — Azure client secrets have a maximum lifetime of 2 years; workflows break silently when they expire -* **Secrets can leak** — anyone with repository admin access can read the secret; it can appear in workflow logs if accidentally echoed -* **Secrets require rotation** — rotating a secret means updating it in both Azure and GitHub; easy to forget -* **Audit trail is weak** — Azure logs show which app made a call but not which workflow or job - ---- - -## 💡 The Solution - -OIDC federated identity replaces the stored secret with a trust relationship. Instead of "here is a password that proves who I am," GitHub Actions says "here is a signed token from GitHub's identity provider — Azure, you already agreed to trust tokens that look like this." Azure validates the token signature and the claims inside it (which repository, which branch, which job), then issues a short-lived access token for that job only. - -**What this means in practice:** - -* ✅ **No password ever created** — nothing to rotate, nothing to leak, nothing to expire -* ✅ **Scoped to exact repo and branch** — a token from a fork or a different branch is rejected -* ✅ **Job-scoped access** — the access token expires when the job finishes (typically 5 minutes) -* ✅ **Full audit trail** — Azure logs show the exact GitHub Actions workflow run that made each API call - ---- - -## 🚀 How It Works - -### Understanding the Trust Chain - -Before running the script, it helps to understand the three entities involved: - -**GitHub:** Issues a signed JWT (JSON Web Token) to each workflow job. The token includes claims: the repository name, the branch, the workflow name, the run ID. GitHub signs it with a private key. - -**Azure Entra ID (formerly Azure Active Directory):** The identity provider for Azure. It stores the trust configuration — "I will accept tokens from GitHub, but only for this specific repository and branch." - -**The Federated Identity Credential:** The entry in Azure Entra ID that says "GitHub's identity provider at `https://token.actions.githubusercontent.com` is trusted for tokens with subject `repo:org/repo:ref:refs/heads/main`." - -When a GitHub Actions job runs, it requests a token from GitHub, presents it to Azure, Azure validates the claims, and returns a short-lived access token scoped to the resources the App Registration is authorized to manage. - -### Step 1: Edit the Configuration Block - -Open `infra/scripts/setup-oidc.sh` and update the six variables at the top: - -```bash -APP_NAME="github-actions-talent-dev" # Display name in Azure Entra ID -RESOURCE_GROUP="rg-talent-dev" # Resource group to manage -LOCATION="eastus" # Azure region -GITHUB_ORG="workcontrolgit" # Your GitHub username or org -GITHUB_REPO="AngularNetTutorial" # Your repository name -BRANCH="master" # Branch that triggers deployments -``` - -**`APP_NAME`** — this is the display name of the App Registration in Azure Entra ID. It appears in the Azure Portal under "App registrations" and in audit logs. Use a name that makes the purpose clear. - -**`BRANCH`** — the federated credential is scoped to this exact branch. Tokens from `develop`, pull request branches, or forks will not be trusted. For this tutorial, `master` is the deployment branch. Article 5.6 shows how to trigger on pushes to `master`. - -### Step 2: Run the Script - -```bash -chmod +x infra/scripts/setup-oidc.sh -./infra/scripts/setup-oidc.sh -``` - -The script prints its progress as it runs. The full execution takes approximately 30 seconds. - -### Step 3: What the Script Does — Line by Line - -**Step 1: Create the App Registration** - -```bash -APP_ID=$(az ad app create \ - --display-name "$APP_NAME" \ - --query appId \ - --output tsv) -``` - -An App Registration is the identity in Azure Entra ID. It is not a user account — it represents the application (GitHub Actions) that will act on behalf of a deployment pipeline. The `appId` is the client ID that GitHub Actions will use to identify itself to Azure. - -**Step 2: Create the Service Principal** - -```bash -az ad sp create --id "$APP_ID" --output none -``` - -The App Registration is the identity definition. The Service Principal is the instance of that identity in your Azure tenant. Role assignments (permissions) are attached to the Service Principal. An App Registration without a Service Principal cannot be granted access to any Azure resource. - -**Step 3: Add the Federated Identity Credential** - -```bash -az ad app federated-credential create \ - --id "$APP_ID" \ - --parameters '{ - "name": "github-actions-branch-master", - "issuer": "https://token.actions.githubusercontent.com", - "subject": "repo:workcontrolgit/AngularNetTutorial:ref:refs/heads/master", - "audiences": ["api://AzureADTokenExchange"] - }' -``` - -This is the trust configuration. It tells Azure Entra ID: - -* **`issuer`** — tokens must be signed by GitHub's OIDC identity provider -* **`subject`** — tokens must claim to be from exactly `repo:workcontrolgit/AngularNetTutorial:ref:refs/heads/master` — no other repo, no other branch -* **`audiences`** — the intended audience must be `api://AzureADTokenExchange` (Azure's OIDC exchange endpoint) - -A token that matches all three conditions will be accepted. A token from a fork, a pull request branch, or a different repository is rejected by subject mismatch — even if it was legitimately signed by GitHub. - -**Step 4a: Create the Resource Group** - -```bash -az group create \ - --name "$RESOURCE_GROUP" \ - --location "$LOCATION" -``` - -This is idempotent — running it when the group already exists is safe. It ensures the resource group exists before the role assignment is scoped to it. - -**Step 4b: Grant the Contributor Role** - -```bash -SUBSCRIPTION_ID=$(az account show --query id --output tsv) -SCOPE="/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}" - -az role assignment create \ - --assignee "$APP_ID" \ - --role "Contributor" \ - --scope "$SCOPE" -``` - -**Scoped to the resource group, not the subscription.** The Contributor role allows creating, updating, and deleting resources — but only within `rg-talent-dev`. The deployment identity cannot touch any other resource group, cannot create new subscriptions, and cannot modify Azure Entra ID. This is the principle of least privilege. - -**Step 6: Save Secrets to GitHub** - -```bash -gh secret set AZURE_CLIENT_ID --body "$APP_ID" -gh secret set AZURE_TENANT_ID --body "$TENANT_ID" -gh secret set AZURE_SUBSCRIPTION_ID --body "$SUBSCRIPTION_ID" -``` - -The three values needed by the `azure/login` action are written directly to the repository's GitHub Secrets using the GitHub CLI. No manual copy-pasting. These values are not sensitive in themselves — they are not passwords — but storing them as secrets keeps workflows clean and avoids hardcoding tenant-specific values. - -### Step 4: Add the SQL Password Secret Manually - -The script ends with a reminder: - -```bash -gh secret set SQL_ADMIN_PASSWORD --repo workcontrolgit/AngularNetTutorial -``` - -Run this command and type the SQL admin password when prompted. This is the one secret that must remain a secret — it is the password for the Azure SQL Server administrator account. It is intentionally not written to the parameters file (as shown in Article 5.4) and not passed on the command line in any log-visible way. - -### Step 5: Verify All Four Secrets - -Navigate to your repository on GitHub: - -``` -GitHub → Settings → Secrets and variables → Actions -``` - -You should see four repository secrets: - -* **`AZURE_CLIENT_ID`** — the App Registration's application ID -* **`AZURE_TENANT_ID`** — your Azure Entra ID tenant ID -* **`AZURE_SUBSCRIPTION_ID`** — your Azure subscription ID -* **`SQL_ADMIN_PASSWORD`** — the SQL Server administrator password - -None of these values are visible after they are saved — GitHub shows only the secret name, not the value. This is expected behavior. - -### Step 6: How Workflows Use These Secrets - -Every deployment workflow in Articles 5.6 and 5.7 starts with this block: - -```yaml -permissions: - id-token: write # Required: allows the job to request an OIDC token from GitHub - contents: read - -steps: - - name: Log in to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} -``` - -**`permissions: id-token: write`** — this permission is required. Without it, GitHub will not issue an OIDC token to the job. It is scoped to the job level — only jobs that declare this permission can use OIDC. - -**`azure/login@v2`** — the official Microsoft action that: -1. Requests an OIDC token from GitHub (short-lived JWT, expires when job ends) -2. Presents the token to Azure Entra ID along with the client ID and tenant ID -3. Azure validates the token against the Federated Identity Credential (checks issuer, subject, audience) -4. Azure returns a short-lived access token (valid for ~5 minutes) -5. The action configures the Azure CLI in subsequent steps with that access token - -After `azure/login`, any `az` command in subsequent steps runs with the Contributor permissions scoped to `rg-talent-dev` — exactly as configured by the setup script. - ---- - -## 💻 Try It Yourself - -After running the script, verify the App Registration exists in the Azure Portal: - -``` -portal.azure.com → Microsoft Entra ID → App registrations → All applications -``` - -Find `github-actions-talent-dev`. Click it and navigate to: - -``` -Certificates & secrets → Federated credentials -``` - -You should see `github-actions-branch-master` with issuer `token.actions.githubusercontent.com`. - -To verify the role assignment: - -```bash -az role assignment list \ - --resource-group rg-talent-dev \ - --output table -``` - -The output should include a row with `Contributor` assigned to `github-actions-talent-dev`. - ---- - -## 🔑 Key Design Decisions - -**OIDC over stored client secrets.** A client secret is a long-lived password — it must be rotated before it expires, stored securely, and distributed to every system that uses it. An OIDC federated credential is a trust configuration — it has no expiry, nothing to rotate, and nothing to distribute. The trade-off is setup complexity (one-time script vs. copy-pasting a secret), but the ongoing operational cost is zero. - -**Resource group scope, not subscription scope.** Granting `Contributor` at the subscription level would allow the deployment identity to create, modify, or delete any resource in the subscription — including the spending-limit configuration. Scoping to `rg-talent-dev` limits the blast radius: even a compromised token can only affect resources in that resource group. - -**Branch-specific federated credential.** The `subject` claim in the federated credential locks trust to `refs/heads/master`. A pull request branch, a feature branch, or a fork cannot obtain an Azure access token using this credential. This prevents a malicious pull request from running arbitrary Azure CLI commands against the subscription. - -**GitHub CLI (`gh`) for secret management.** Using `gh secret set` in the script eliminates manual copy-pasting. Manual steps introduce transcription errors and require the person running the script to have browser access to GitHub repository settings. The script runs end-to-end from the terminal with no manual steps except the SQL password. - -**`SQL_ADMIN_PASSWORD` added manually.** The SQL admin password is a genuine secret — a value that must remain confidential. The three OIDC values (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`) are not sensitive in the same way: knowing them without the federated credential is harmless. The SQL password is kept separate and typed interactively, never written to a file or echoed in a terminal. - -**GitHub Secrets vs Azure Key Vault — the clear split.** `SQL_ADMIN_PASSWORD` is the only runtime-sensitive value in GitHub Secrets for this project — and it is only used once, during the Bicep infrastructure deployment (Article 5.4). It is not used by the application itself. Database connection strings, JWT signing keys, and other **application runtime secrets** are stored in Azure Key Vault (`kv-talent-dev`), not in GitHub Secrets. The deploy workflows in Articles 5.6 and 5.7 inject `@Microsoft.KeyVault(SecretUri=...)` references into App Service settings rather than raw values. The Web App's managed identity resolves those references at runtime — the actual secret never passes through GitHub Actions at all. - ---- - -## 🌟 Why This Matters - -The OIDC pattern used here is not Azure-specific. The same approach works for AWS (OIDC with IAM), Google Cloud (Workload Identity Federation), and any other cloud provider that supports OpenID Connect. The underlying concept — replace long-lived credentials with short-lived, cryptographically verifiable tokens — is the direction the industry is moving for all non-human identities. - -For this tutorial, the immediate benefit is that Articles 5.6 and 5.7 can deploy to Azure using `azure/login` without managing any passwords. The longer-term benefit is that new developers joining the project do not need to be handed any credentials — the deployment identity is entirely self-contained in Azure and GitHub configuration. - -**Transferable skills:** - -* **Federated Identity Credentials** — the same Azure Entra ID concept applies to any workload that needs passwordless Azure access (GitHub Actions, Azure Pipelines, Kubernetes pods, on-premises servers) -* **`az role assignment create --scope`** — the pattern for least-privilege role grants applies to every Azure deployment, not just this one -* **`permissions: id-token: write`** — required for any GitHub Actions OIDC integration, regardless of cloud provider - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #azure #github #githubactions #oidc #cicd #devops #dotnet #angular #security #azurecli #federatedidentity #secretsmanagement #clouddeployment #webdevelopment #fullstack diff --git a/blogs/series-5-devops-data/5.6-azure-deploy-dotnet-apps.md b/blogs/series-5-devops-data/5.6-azure-deploy-dotnet-apps.md deleted file mode 100644 index a651071..0000000 --- a/blogs/series-5-devops-data/5.6-azure-deploy-dotnet-apps.md +++ /dev/null @@ -1,456 +0,0 @@ -# Deploy .NET API and IdentityServer to Azure App Service - -## Automated Build, Test, and Deploy on Every Push to Main - -A deployment that requires a developer to open a terminal, remember the right commands, and hope nothing changed since last time is a deployment waiting to fail. GitHub Actions replaces the manual process with a repeatable workflow: push to `main`, the pipeline runs, and the application lands in Azure. - -This article writes two GitHub Actions workflows — one for the .NET API, one for IdentityServer — that restore, build, test, publish, and deploy automatically. It also covers the App Service application settings that connect each app to the Azure SQL database and to each other. - -![Deploy .NET Apps to Azure](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-api-endpoints.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article deploys the backend. Article 5.7 deploys the Angular frontend. Article 5.5 set up the Azure authentication used here.** - ---- - -## 📚 What You'll Learn - -* **Workflow structure** — how `on: push: paths:` triggers only the relevant workflow -* **`submodules: recursive`** — why checkout must include submodules for a monorepo -* **`dotnet publish`** — what the publish output contains and why it differs from the build output -* **`azure/webapps-deploy`** — the action that uploads and hot-swaps the deployment -* **App Service settings** — how connection strings and URLs reach the running app without touching `appsettings.json` -* **Key Vault references** — why connection strings go into Key Vault, not GitHub Secrets, and how `@Microsoft.KeyVault(SecretUri=...)` wires them into App Service settings -* **GitHub Secrets vs Key Vault** — the split: CI/CD auth credentials in GitHub Secrets, runtime secrets in Key Vault -* **Deployment order** — why IdentityServer must be deployed and running before the API - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 5.4 complete** — Azure resources provisioned (`app-talent-api-dev`, `app-talent-ids-dev`) -* **Article 5.5 complete** — Four GitHub Secrets set: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`, `SQL_ADMIN_PASSWORD` -* **Additional secrets to add** — listed in Step 2 below - ---- - -## 🎯 The Problem - -The .NET API and IdentityServer each need several environment-specific values at runtime: the database connection string, the URL of IdentityServer (which the API uses to validate tokens), and the URL of the API itself (which IdentityServer uses to register the audience). These values differ between local development and Azure. Hardcoding them in `appsettings.json` would commit environment-specific secrets to source control and break the principle that the same build artifact runs in every environment. - -Even storing connection strings in GitHub Secrets is not production-grade — the raw value gets injected into App Service settings where it is visible in plain text in the Portal to anyone with Contributor access. The correct approach is to store secrets in **Azure Key Vault** (provisioned in Article 5.4) and reference them from App Service settings via `@Microsoft.KeyVault(SecretUri=...)`. The App Service resolves the reference at runtime using its managed identity — the actual connection string is never visible anywhere. - -Manual deployment — `dotnet publish`, zip the output, upload via the Portal — is error-prone and produces no audit trail. Running it inconsistently across developers produces inconsistent results. - ---- - -## 💡 The Solution - -GitHub Actions workflows are triggered by pushes to specific paths. When code changes in `ApiResources/TalentManagement-API/`, the API workflow runs. When code changes in `TokenService/Duende-IdentityServer/`, the IdentityServer workflow runs. Each workflow builds the app, runs tests, publishes the binary, logs into Azure using OIDC (no stored password), injects configuration as App Service settings, and deploys. - -App Service application settings are the Azure equivalent of environment variables. They override values in `appsettings.json` at runtime without touching the committed file. The same published binary runs locally (using `appsettings.json`) and in Azure (using App Service settings that override the file). - -For **non-sensitive settings** (URLs, feature flags, audience names), the workflow injects plain values directly. For **secrets** (connection strings, JWT signing key), the workflow injects a `@Microsoft.KeyVault(SecretUri=...)` reference instead of the raw value. App Service resolves these references at startup using the Web App's system-assigned managed identity — the actual secret is retrieved from Key Vault and injected into the app's environment without ever appearing in the Portal or in logs. See Article 5.9 for full detail on this pattern. - ---- - -## 🚀 How It Works - -### Step 1: Add the Remaining GitHub Secrets - -Article 5.5 set up four secrets (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`, `SQL_ADMIN_PASSWORD`). The deploy workflows need several more. - -**Navigate to:** GitHub → Repository → Settings → Secrets and variables → Actions → New repository secret - -**GitHub Secrets vs Azure Key Vault — the split:** - -* **CI/CD auth credentials (`AZURE_*`)** → GitHub Secrets — needed by the runner to log in to Azure -* **Infrastructure deploy password (`SQL_ADMIN_PASSWORD`)** → GitHub Secrets — one-time use for Bicep provisioning only -* **Runtime secrets (connection strings, JWT key)** → **Azure Key Vault** — never exposed as plain text; resolved by managed identity at runtime -* **Non-sensitive URLs and config** → GitHub Secrets — not sensitive, just convenient to store here - -**Secrets to add to GitHub:** - -* **`KEY_VAULT_URI`** — the URI of the Key Vault provisioned in Article 5.4 - -``` -https://kv-talent-dev.vault.azure.net/ -``` - -* **`IDENTITY_SERVER_URL`** — the HTTPS URL of the deployed IdentityServer App Service - -``` -https://app-talent-ids-dev.azurewebsites.net -``` - -* **`ANGULAR_APP_URL`** — the Azure Static Web App URL - -``` -https://mango-flower-0ced4011e.4.azurestaticapps.net -``` - -* **`IDENTITY_ADMIN_URL`** — the IdentityServer Admin UI URL - -``` -https://app-talent-admin-dev.azurewebsites.net -``` - -**Secrets to add to Key Vault** (not GitHub — these contain sensitive values): - -```bash -KV="kv-talent-dev" - -# API database connection string -az keyvault secret set --vault-name $KV \ - --name "ConnectionStrings--DefaultConnection" \ - --value "Server=tcp:sql-talent-dev.database.windows.net,1433;Initial Catalog=sqldb-talent-api-dev;Persist Security Info=False;User ID=sqladmin;Password=;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" - -# IdentityServer database connection string -az keyvault secret set --vault-name $KV \ - --name "ConnectionStrings--IdsDbConnection" \ - --value "Server=tcp:sql-talent-dev.database.windows.net,1433;Initial Catalog=sqldb-talent-ids-dev;Persist Security Info=False;User ID=sqladmin;Password=;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" - -# JWT signing key -az keyvault secret set --vault-name $KV \ - --name "JWTSettings--Key" \ - --value "" -``` - -**⚠️ One-time prerequisite:** Your local Azure CLI identity needs the **Key Vault Secrets Officer** role on `kv-talent-dev` to run `az keyvault secret set`. Grant it in the Portal: **Key Vault → kv-talent-dev → Access control (IAM) → Add role assignment → Key Vault Secrets Officer → your account**. - -### Step 2: Understand the Workflow File Structure - -Both workflows live in `.github/workflows/` in the parent repository. They follow the same pattern: - -``` -.github/workflows/ -├── deploy-api.yml ← triggers on ApiResources/ changes -└── deploy-identityserver.yml ← triggers on TokenService/ changes -``` - -### Step 3: The API Workflow - -The complete workflow is at `.github/workflows/deploy-api.yml`. Walk through its key sections: - -**Trigger:** - -```yaml -on: - push: - branches: - - main - paths: - - 'ApiResources/TalentManagement-API/**' - - '.github/workflows/deploy-api.yml' - workflow_dispatch: -``` - -**`paths:`** — the workflow only runs when a push to `main` changes files under `ApiResources/TalentManagement-API/` or the workflow file itself. Pushing only Angular changes does not trigger the API deployment. This is critical for a monorepo where multiple components share one repository. - -**`workflow_dispatch:`** — allows manually triggering the workflow from the GitHub UI or CLI. Useful for redeploying without making a code change. - -**OIDC permissions:** - -```yaml -permissions: - id-token: write # allows the job to request an OIDC token from GitHub - contents: read -``` - -Without `id-token: write`, GitHub will not issue an OIDC token and `azure/login` will fail. This permission is declared at the workflow level and applies to all jobs. - -**Checkout with submodules:** - -```yaml -- name: Checkout repository (with submodules) - uses: actions/checkout@v4 - with: - submodules: recursive -``` - -The API code lives in the `ApiResources/TalentManagement-API` submodule. Without `submodules: recursive`, the checkout produces an empty directory and `dotnet restore` fails immediately. The `recursive` flag initializes all nested submodules. - -**Build and test:** - -```yaml -- name: Restore dependencies - run: dotnet restore ${{ env.SOLUTION_PATH }} - -- name: Build - run: dotnet build ${{ env.SOLUTION_PATH }} --configuration Release --no-restore - -- name: Run unit tests - run: dotnet test ${{ env.SOLUTION_PATH }} --configuration Release --no-build --verbosity normal -``` - -Tests run against the Release build. `--no-build` and `--no-restore` reuse the artifacts from the previous steps — the build does not run twice. - -**Publish:** - -```yaml -- name: Publish - run: | - dotnet publish ${{ env.PROJECT_PATH }} \ - --configuration Release \ - --no-build \ - --output ${{ env.PUBLISH_DIR }} -``` - -`dotnet publish` produces a self-contained deployable directory: compiled DLLs, static files, `appsettings.json`, and the runtime host. This is what gets deployed to App Service — not the source code, not the intermediate build output. - -**OIDC login:** - -```yaml -- name: Log in to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} -``` - -This is the OIDC exchange described in Article 5.5. After this step, all subsequent `az` commands run with the Contributor role scoped to `rg-talent-dev`. - -**Configure App Service settings:** - -```yaml -- name: Configure App Service settings - env: - API_CONN: ${{ secrets.API_DB_CONNECTION_STRING }} - IDS_URL: ${{ secrets.IDENTITY_SERVER_URL }} - JWT_KEY: ${{ secrets.JWT_KEY }} - ANGULAR_URL: ${{ secrets.ANGULAR_APP_URL }} - run: | - az webapp config appsettings set \ - --resource-group ${{ env.RESOURCE_GROUP }} \ - --name ${{ env.APP_SERVICE_NAME }} \ - --settings \ - "ConnectionStrings__DefaultConnection=$API_CONN" \ - "Sts__ServerUrl=$IDS_URL" \ - "Sts__ValidIssuer=$IDS_URL" \ - "Sts__Audience=app.api.talentmanagement" \ - "JWTSettings__Key=$JWT_KEY" \ - "JWTSettings__Issuer=CoreIdentity" \ - "JWTSettings__Audience=CoreIdentityUser" \ - "JWTSettings__DurationInMinutes=60" \ - "FeatureManagement__AuthEnabled=true" \ - "Cors__AllowedOrigins__0=$ANGULAR_URL" \ - "Cors__AllowedOrigins__1=https://workcontrolgit.github.io" \ - "ASPNETCORE_ENVIRONMENT=Production" -``` - -**Why `env:` block instead of `${{ secrets.X }}` inline?** Bash strips characters after `$` followed by a digit — a password like `Abc$9xyz` becomes `Abcxyz` when interpolated inline in a shell script. Mapping secrets to environment variables in the `env:` block and referencing them as `$API_CONN` avoids this. Always use `env:` when passing secrets into shell commands. - -App Service uses double underscores (`__`) to represent the `:` separator in .NET configuration keys. `ConnectionStrings__DefaultConnection` maps to `ConnectionStrings:DefaultConnection` in the .NET configuration system, which in turn maps to `appsettings.json`'s `ConnectionStrings.DefaultConnection`. This is the standard pattern for hierarchical configuration in Azure App Service. - -`ASPNETCORE_ENVIRONMENT=Production` causes ASP.NET Core to merge `appsettings.Production.json` (if it exists) on top of `appsettings.json`, then apply App Service settings on top of that. App Service settings always win — they cannot be overridden by a file in the deployment package. - -### Complete App Service Settings Reference - -All settings injected by the GitHub Actions workflows, cross-referenced against the live Azure environment. - -**API App (`app-talent-api-dev`):** - -* **`ConnectionStrings__DefaultConnection`** — Azure SQL connection string for the API database — source: `API_DB_CONNECTION_STRING` GitHub Secret -* **`Sts__ServerUrl`** — IdentityServer URL used to fetch the OIDC discovery document — source: `IDENTITY_SERVER_URL` Secret -* **`Sts__ValidIssuer`** — Expected `iss` claim in incoming JWT tokens — source: `IDENTITY_SERVER_URL` Secret -* **`Sts__Audience`** — Expected `aud` claim — hardcoded: `app.api.talentmanagement` -* **`JWTSettings__Key`** — Symmetric signing key for the API's own JWT issuance — source: `JWT_KEY` Secret -* **`JWTSettings__Issuer`** — Issuer name in API-issued tokens — hardcoded: `CoreIdentity` -* **`JWTSettings__Audience`** — Audience in API-issued tokens — hardcoded: `CoreIdentityUser` -* **`JWTSettings__DurationInMinutes`** — Token lifetime — hardcoded: `60` -* **`FeatureManagement__AuthEnabled`** — Enables JWT authentication globally — hardcoded: `true` -* **`Cors__AllowedOrigins__0`** — Azure SWA URL — source: `ANGULAR_APP_URL` Secret -* **`Cors__AllowedOrigins__1`** — GitHub Pages URL — hardcoded: `https://workcontrolgit.github.io` -* **`ASPNETCORE_ENVIRONMENT`** — Runtime environment — hardcoded: `Production` - -**IdentityServer STS (`app-talent-ids-dev`):** - -* **`ConnectionStrings__ConfigurationDbConnection`** — Clients, scopes, resources — source: `IDS_DB_CONNECTION_STRING` Secret -* **`ConnectionStrings__PersistedGrantDbConnection`** — Tokens, sessions, grants — source: `IDS_DB_CONNECTION_STRING` Secret -* **`ConnectionStrings__IdentityDbConnection`** — User accounts (ASP.NET Identity) — source: `IDS_DB_CONNECTION_STRING` Secret -* **`ConnectionStrings__DataProtectionDbConnection`** — ASP.NET Data Protection keys — source: `IDS_DB_CONNECTION_STRING` Secret -* **`AdminConfiguration__IdentityServerBaseUrl`** — STS own public URL (used by Admin) — source: `IDENTITY_SERVER_URL` Secret -* **`AdminConfiguration__IdentityAdminBaseUrl`** — Admin UI URL (used for back-channel links) — source: `IDENTITY_ADMIN_URL` Secret -* **`CspTrustedDomains__0`** — hardcoded: `www.gravatar.com` (user avatars) -* **`CspTrustedDomains__1`** — hardcoded: `fonts.googleapis.com` -* **`CspTrustedDomains__2`** — hardcoded: `fonts.gstatic.com` -* **`CspTrustedDomains__3`** — hardcoded: `workcontrolgit.github.io` (GitHub Pages Angular) -* **`CspTrustedDomains__4`** — hardcoded: `mango-flower-0ced4011e.4.azurestaticapps.net` (Azure SWA Angular) -* **`ASPNETCORE_ENVIRONMENT`** — hardcoded: `Production` - -**IdentityServer Admin (`app-talent-admin-dev`):** - -* **`ConnectionStrings__ConfigurationDbConnection`** — source: `IDS_DB_CONNECTION_STRING` Secret -* **`ConnectionStrings__PersistedGrantDbConnection`** — source: `IDS_DB_CONNECTION_STRING` Secret -* **`ConnectionStrings__IdentityDbConnection`** — source: `IDS_DB_CONNECTION_STRING` Secret -* **`ConnectionStrings__AdminLogDbConnection`** — Admin action logs — source: `IDS_DB_CONNECTION_STRING` Secret -* **`ConnectionStrings__AdminAuditLogDbConnection`** — Audit trail — source: `IDS_DB_CONNECTION_STRING` Secret -* **`ConnectionStrings__DataProtectionDbConnection`** — source: `IDS_DB_CONNECTION_STRING` Secret -* **`AdminConfiguration__IdentityServerBaseUrl`** — STS URL for Admin to authenticate against — source: `IDENTITY_SERVER_URL` Secret -* **`AdminConfiguration__IdentityAdminRedirectUri`** — OIDC callback URL for the Admin UI — source: `IDENTITY_ADMIN_URL` Secret + `/signin-oidc` -* **`SeedConfiguration__ApplySeed`** — Prevents re-seeding on every deploy — hardcoded: `false` -* **`DatabaseMigrationsConfiguration__ApplyDatabaseMigrations`** — Prevents auto-migration on deploy — hardcoded: `false` -* **`ASPNETCORE_ENVIRONMENT`** — hardcoded: `Production` - -**⚠️ Stale settings to be aware of:** Running `az webapp config appsettings list` against the live apps may show additional settings not managed by the workflow — for example `CorsOrigins` (an older duplicate of `Cors__AllowedOrigins__0`) or `ASPNETCORE_DETAILEDERRORS` set manually during debugging. Settings set outside the workflow are not removed by subsequent workflow runs — only values for keys the workflow explicitly sets are updated. Remove stale settings manually via the Portal or `az webapp config appsettings delete`. - -**Deploy:** - -```yaml -- name: Deploy to Azure App Service - uses: azure/webapps-deploy@v3 - with: - app-name: ${{ env.APP_SERVICE_NAME }} - package: ${{ env.PUBLISH_DIR }} -``` - -`azure/webapps-deploy` compresses the publish directory, uploads it to App Service, and performs a hot-swap deployment. The app restarts with the new code. The action waits until the deployment completes and the health check passes before reporting success. - -### Step 4: The IdentityServer Workflow - -The IdentityServer workflow at `.github/workflows/deploy-identityserver.yml` follows the same pattern. Key differences: - -**Trigger paths:** - -```yaml -paths: - - 'TokenService/Duende-IdentityServer/**' - - '.github/workflows/deploy-identityserver.yml' -``` - -IdentityServer changes trigger this workflow; API changes do not. - -**App Service settings for IdentityServer STS (using `env:` block):** - -```yaml -- name: Configure STS App Service settings - env: - IDS_CONN: ${{ secrets.IDS_DB_CONNECTION_STRING }} - IDS_URL: ${{ secrets.IDENTITY_SERVER_URL }} - ADMIN_URL: ${{ secrets.IDENTITY_ADMIN_URL }} - run: | - az webapp config appsettings set \ - --settings \ - "ConnectionStrings__ConfigurationDbConnection=$IDS_CONN" \ - "ConnectionStrings__PersistedGrantDbConnection=$IDS_CONN" \ - "ConnectionStrings__IdentityDbConnection=$IDS_CONN" \ - "ConnectionStrings__DataProtectionDbConnection=$IDS_CONN" \ - "AdminConfiguration__IdentityServerBaseUrl=$IDS_URL" \ - "AdminConfiguration__IdentityAdminBaseUrl=$ADMIN_URL" -``` - -IdentityServer uses four different connection string keys — `ConfigurationDbConnection` (clients, scopes), `PersistedGrantDbConnection` (tokens, sessions), `IdentityDbConnection` (user accounts), and `DataProtectionDbConnection` (ASP.NET Data Protection keys). All four point to the same Azure SQL database (`sqldb-talent-ids-dev`) in this tutorial. In production, separating them onto dedicated databases is common. The same `$IDS_CONN` value is reused for all four — the `env:` block means the secret is only referenced once. - -### Step 5: Deployment Order - -**IdentityServer must be deployed and running before the API.** - -The API validates every incoming request bearer token against IdentityServer's discovery endpoint (`/.well-known/openid-configuration`). If IdentityServer is not reachable, the API fails to start or fails all authenticated requests. - -**Correct deployment order:** - -1. Deploy IdentityServer → wait for it to start → verify `https://app-talent-ids-dev.azurewebsites.net/.well-known/openid-configuration` returns JSON -2. Deploy the API → the `Sts__ServerUrl` setting points to the running IdentityServer - -In GitHub Actions, you can enforce this with workflow dependencies if both deploy in the same workflow run. For the path-filtered workflows in this article, the safest approach is to deploy IdentityServer first by pushing those changes first, then push API changes. - -### Step 6: Verify the Deployments - -After both workflows complete: - -**Verify IdentityServer:** - -```bash -curl https://app-talent-ids-dev.azurewebsites.net/.well-known/openid-configuration -``` - -Expected: a JSON object with `issuer`, `authorization_endpoint`, `token_endpoint`, and other discovery metadata. - -**Verify the API:** - -```bash -curl https://app-talent-api-dev.azurewebsites.net/api/v1/health -``` - -Or open the Swagger UI: - -``` -https://app-talent-api-dev.azurewebsites.net/swagger -``` - -The Swagger UI loads if the app started correctly. Click "Authorize", enter a Bearer token obtained from IdentityServer, and call a protected endpoint to verify the full auth flow. - -**Check deployment logs in GitHub:** - -Navigate to GitHub → Actions → the workflow run → click the job → expand each step to see output. Failed steps show the exact error — a misconfigured connection string, a missing secret, or a test failure. - ---- - -## 💻 Try It Yourself - -Make a trivial change to the API (add a comment to any file) and push to `main`: - -```bash -# In the ApiResources submodule -git checkout main -echo "# deployment test" >> TalentManagementAPI.WebApi/appsettings.json -git add . && git commit -m "Test API deployment workflow" -git push -``` - -Then go to GitHub → Actions and watch the `Deploy .NET API to Azure App Service` workflow run. The full pipeline — restore, build, test, publish, deploy — should complete in approximately 3–4 minutes. - ---- - -## 🔑 Key Design Decisions - -**Path-filtered triggers, not a single monorepo workflow.** A single workflow that deploys everything on every push would deploy IdentityServer when only the API changed, and vice versa. Path filters give each component its own deployment pipeline while keeping all workflows in one repository. This is the recommended pattern for monorepos with multiple independently deployable components. - -**App Service settings over `appsettings.Production.json`.** Committing a `appsettings.Production.json` with real connection strings would put secrets in source control. App Service settings inject configuration at runtime without touching the build artifact. The same published ZIP can be deployed to any environment — the environment-specific values come from outside the artifact. - -**`submodules: recursive` is non-negotiable.** Without it, `dotnet restore` fails on the first run because the source files are absent. The `actions/checkout@v4` action does not fetch submodules by default. This is a common gotcha for developers new to GitHub Actions with submodule-based monorepos. - -**Tests run in CI before deploy.** The `dotnet test` step runs the full test suite before any deployment artifact is produced. A failing test aborts the workflow before `dotnet publish` runs — the current production deployment is never replaced by a broken build. This is the guarantee that makes CI/CD trustworthy. - -**IdentityServer before API.** The API attempts to fetch IdentityServer's discovery document at startup to configure token validation. Deploying the API before IdentityServer is running causes the API to start in a degraded state where all authenticated endpoints return 401. Deploying IdentityServer first and verifying its health endpoint before deploying the API ensures a clean startup sequence. - ---- - -## 🌟 Why This Matters - -The two workflows in this article automate what would otherwise be a 20-step manual process prone to missed settings and inconsistent results. More importantly, they make the deployment process auditable: every GitHub Actions run shows who triggered it, which commit was deployed, whether tests passed, and exactly which `az` commands ran. - -For a team, this means any developer can deploy by merging a pull request. No special deployment credentials are needed. No deployment runbook to follow. No risk of deploying an untested build. - -**Transferable skills:** - -* **`paths:` triggers** — the correct approach for any GitHub Actions monorepo; prevents unnecessary deployments and keeps pipeline minutes within free tier limits -* **`__` separator for App Service settings** — applies to any .NET application on App Service, regardless of the complexity of the settings hierarchy -* **`azure/webapps-deploy@v3`** — the standard action for all .NET App Service deployments; the same action works for Node.js, Python, and Java apps with different build steps - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #azure #githubactions #dotnet #cicd #devops #appservice #azuresql #oidc #azurecli #continuousdeployment #identityserver #webdevelopment #fullstack #csharp #cleanarchitecture diff --git a/blogs/series-5-devops-data/5.7-azure-deploy-angular-swa.md b/blogs/series-5-devops-data/5.7-azure-deploy-angular-swa.md deleted file mode 100644 index fdf8317..0000000 --- a/blogs/series-5-devops-data/5.7-azure-deploy-angular-swa.md +++ /dev/null @@ -1,350 +0,0 @@ -# Deploy Angular to Azure Static Web Apps - -## Inject Environment Variables at Build Time and Deploy with One GitHub Actions Workflow - -Angular applications compiled for production are static files: HTML, JavaScript bundles, CSS, and images. They have no runtime server to read environment variables from — the API URL and IdentityServer URL must be baked into the JavaScript at build time. Getting the right URLs into the right build requires a step between `npm ci` and `ng build` that injects the production values. - -This article writes the GitHub Actions workflow that installs, injects environment values, builds, and deploys the Angular application to Azure Static Web Apps — and explains the `staticwebapp.config.json` file that makes client-side routing work correctly after deployment. - -![Deploy Angular to Azure Static Web Apps](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/angular/application-dashboard-anonymous.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article deploys the Angular frontend. Articles 5.4–5.6 covered infrastructure provisioning and backend deployment. Article 5.8 covers post-deployment configuration.** - ---- - -## 📚 What You'll Learn - -* **Why Angular needs build-time injection** — there is no runtime server to provide environment variables to a compiled SPA -* **`sed` for variable injection** — how to replace placeholder URLs in `environment.prod.ts` before the build runs -* **`npm ci` vs `npm install`** — why `ci` is correct for workflows -* **Static Web Apps deployment token** — how the workflow retrieves it from Azure using OIDC -* **`staticwebapp.config.json`** — the file that prevents 404 on page refresh for Angular routes -* **`skip_app_build: true`** — why the workflow builds Angular manually instead of letting the SWA action do it - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 5.4 complete** — `swa-talent-ui-dev` Static Web App provisioned -* **Article 5.5 complete** — OIDC secrets (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`) in GitHub -* **Article 5.6 complete** — `app-talent-api-dev` and `app-talent-ids-dev` deployed and running - -**Additional secrets to add:** - -Navigate to GitHub → Settings → Secrets and variables → Actions → New repository secret: - -* **`API_APP_URL`** — the base URL of the deployed API App Service (without `/api/v1`) - -``` -https://app-talent-api-dev.azurewebsites.net -``` - -* **`IDENTITY_SERVER_URL`** — already added in Article 5.6 - -``` -https://app-talent-ids-dev.azurewebsites.net -``` - ---- - -## 🎯 The Problem - -The Angular production environment file (`src/environments/environment.prod.ts`) ships with placeholder URLs: - -```typescript -apiUrl: 'https://your-production-api.com/api/v1', -identityServerUrl: 'https://localhost:44310', -``` - -These values are compiled into the JavaScript bundle by `ng build --configuration production`. After compilation, there is no way to change them at runtime — they are inside minified, hashed JavaScript files. If the wrong URL is baked in, the app cannot reach the API or authenticate. - -Two naive solutions both have problems: - -* **Committing production URLs directly** — ties the code to a specific deployment, breaks for staging environments, and may expose URLs in public repositories -* **Using runtime injection** (reading from `window.__env`) — requires a server-side render or a meta-tag injection step and adds significant complexity - ---- - -## 💡 The Solution - -Inject the production URLs as environment variables in the GitHub Actions workflow, use `sed` to replace the placeholder strings in `environment.prod.ts` before the build runs, then build normally. The build picks up the real URLs. The binary contains the correct values. The same source file ships to every environment — what changes is only the injection step in the workflow. - -Additionally, `staticwebapp.config.json` tells Azure Static Web Apps to serve `index.html` for any route that doesn't match a static file. Without this, navigating directly to `https://your-swa.azurestaticapps.net/employees` returns a 404 because there is no `employees` file on the server — only the Angular router knows what to render at that path. - ---- - -## 🚀 How It Works - -### Step 1: Add the `staticwebapp.config.json` File - -Create `public/staticwebapp.config.json` in the Angular application directory: - -```json -{ - "navigationFallback": { - "rewrite": "/index.html", - "exclude": ["/api/*", "/*.{css,js,png,jpg,svg,ico,woff,woff2,ttf,eot}"] - }, - "mimeTypes": { - ".json": "application/json" - }, - "globalHeaders": { - "X-Frame-Options": "SAMEORIGIN", - "X-Content-Type-Options": "nosniff", - "Referrer-Policy": "strict-origin-when-cross-origin" - } -} -``` - -This file lives in `public/` so Angular's build copies it verbatim to `dist/talent-management/browser/`. The Angular build configuration in `angular.json` already includes `"input": "public"` as an asset glob, so no configuration change is needed. - -**`navigationFallback`** — the core SPA fix. All requests that don't match a static file or the `/api/*` path are rewritten to serve `index.html`. Angular's router then handles the URL client-side. - -**`exclude`** — static assets (JavaScript bundles, images, fonts) must not be rewritten to `index.html`. The pattern matches known static file extensions so they are served directly. - -**`globalHeaders`** — basic security headers applied to every response. These are set at the CDN edge, before responses reach the browser. - -### Step 2: Understand the Workflow - -The complete workflow is at `.github/workflows/deploy-angular.yml`. Its key sections: - -**Trigger:** - -```yaml -on: - push: - branches: - - main - paths: - - 'Clients/TalentManagement-Angular-Material/**' - - '.github/workflows/deploy-angular.yml' - workflow_dispatch: -``` - -Only Angular changes (or changes to the workflow itself) trigger this deployment. Backend changes do not re-deploy the Angular app. - -**Checkout with submodules:** - -```yaml -- name: Checkout repository (with submodules) - uses: actions/checkout@v4 - with: - submodules: recursive -``` - -The Angular source lives in the `Clients/TalentManagement-Angular-Material` submodule. Without `submodules: recursive`, the `Clients/` directory is empty and `npm ci` fails. - -**Node.js setup with caching:** - -```yaml -- name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '22.x' - cache: 'npm' - cache-dependency-path: '${{ env.ANGULAR_APP_DIR }}/package-lock.json' -``` - -`cache: 'npm'` caches the npm dependency cache keyed on the `package-lock.json` hash. When `package-lock.json` has not changed, `npm ci` restores from the cache instead of downloading from the npm registry — cutting install time from 60 seconds to under 10 seconds on cache hits. - -**Install with `npm ci`:** - -```yaml -- name: Install dependencies - working-directory: ${{ env.ANGULAR_APP_DIR }} - run: npm ci -``` - -`npm ci` (clean install) is the correct command for CI/CD: - -* Installs exactly what is in `package-lock.json` — no version resolution, no surprises -* Deletes `node_modules` before installing — no leftover artifacts from previous runs -* Fails if `package-lock.json` is out of sync with `package.json` — catches a common developer mistake - -`npm install` is for local development where you want version resolution and lock file updates. Never use `npm install` in a workflow. - -**Inject environment variables:** - -```yaml -- name: Inject production environment variables - working-directory: ${{ env.ANGULAR_APP_DIR }} - env: - API_URL: ${{ secrets.API_APP_URL }} - IDENTITY_SERVER_URL: ${{ secrets.IDENTITY_SERVER_URL }} - run: | - sed -i "s|https://your-production-api.com/api/v1|${API_URL}/api/v1|g" \ - src/environments/environment.prod.ts - sed -i "s|https://localhost:44310|${IDENTITY_SERVER_URL}|g" \ - src/environments/environment.prod.ts -``` - -`sed -i` performs an in-place string replacement on the file. The placeholder strings in `environment.prod.ts` are replaced with the real Azure URLs before `ng build` runs. After the build, the modified `environment.prod.ts` is discarded — the runner's workspace is a fresh checkout every time. The committed source file always contains the safe placeholder values. - -**Why `sed` over a templating tool:** It requires no dependencies, works on any Linux runner, and is auditable — the exact replacement strings are visible in the workflow log (the URLs, not the secrets themselves, which are masked by GitHub Actions). - -**Build Angular:** - -```yaml -- name: Build Angular (production) - working-directory: ${{ env.ANGULAR_APP_DIR }} - run: npm run build -- --configuration production -``` - -`npm run build` calls `ng build` as defined in `package.json`. The `-- --configuration production` passes the flag through npm's argument separator. The production configuration uses `environment.prod.ts` (with the injected URLs) and enables full optimization: tree shaking, minification, and output hashing. - -The build output lands in `dist/talent-management/browser/` — the directory that the deploy action uploads. - -**OIDC login:** - -```yaml -- name: Log in to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} -``` - -The same OIDC pattern as the backend workflows. After this step, `az` commands run with Contributor access to `rg-talent-dev`. - -**Retrieve the Static Web App deployment token:** - -```yaml -- name: Get Static Web App deployment token - id: swa-token - run: | - SWA_TOKEN=$(az staticwebapp secrets list \ - --name ${{ env.STATIC_WEB_APP_NAME }} \ - --resource-group ${{ env.RESOURCE_GROUP }} \ - --query properties.apiKey \ - --output tsv) - echo "token=${SWA_TOKEN}" >> $GITHUB_OUTPUT -``` - -Azure Static Web Apps uses a separate deployment token — not an Azure RBAC credential — to authorize uploads. The token is retrieved from the resource itself using the OIDC-authenticated Azure CLI session. This avoids storing the SWA deployment token as a separate GitHub Secret. - -**Deploy:** - -```yaml -- name: Deploy to Azure Static Web Apps - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ steps.swa-token.outputs.token }} - action: upload - app_location: 'Clients/TalentManagement-Angular-Material/talent-management' - output_location: 'dist/talent-management/browser' - skip_app_build: true -``` - -**`skip_app_build: true`** — by default, `Azure/static-web-apps-deploy` tries to build the app itself using Oryx (Azure's build system). Oryx does not know about the `sed` injection step and cannot inject GitHub Secrets into the build environment the same way. Setting `skip_app_build: true` tells the action that the build is already done — just upload the output directory. - -**`output_location`** — points to the built browser bundle. This must match Angular's output directory. For the `@angular/build:application` builder used in Angular 17+, the output is `dist//browser`. - -### Step 3: Push the `staticwebapp.config.json` and Trigger a Deployment - -The `staticwebapp.config.json` was added to the Angular submodule. Commit and push it: - -```bash -# In the Clients submodule -cd Clients/TalentManagement-Angular-Material -git checkout develop -git add talent-management/public/staticwebapp.config.json -git commit -m "Add Static Web Apps routing config for SPA fallback" -git push - -# Back in the parent repo, update submodule reference and merge to main -cd ../.. -git add Clients/TalentManagement-Angular-Material -git commit -m "Update Angular client submodule with SWA routing config" -``` - -When this commit reaches `main`, the Angular workflow triggers. - -### Step 4: Verify the Deployment - -After the workflow completes, retrieve the Static Web App URL: - -```bash -az staticwebapp show \ - --name swa-talent-ui-dev \ - --resource-group rg-talent-dev \ - --query defaultHostname \ - --output tsv -``` - -Open that URL in a browser. The Angular application should load. Test the routing fix by: - -1. Navigating to any route (e.g., `/dashboard/employees`) -2. Pressing F5 to hard-refresh the page -3. The page should reload correctly — not show a 404 - ---- - -## 💻 Try It Yourself - -Trigger the Angular workflow manually from the GitHub UI: - -``` -GitHub → Actions → Deploy Angular to Azure Static Web Apps → Run workflow → Run workflow -``` - -Or push any change to the Angular submodule and observe the workflow trigger automatically. Watch the "Inject production environment variables" step and confirm the correct URLs are being injected (the URLs are visible in the log; the secrets themselves are masked). - ---- - -## 🔑 Key Design Decisions - -**`sed` injection before `ng build`, not runtime injection.** Angular SPAs have no runtime server. All configuration must be in the compiled bundle. The `sed` approach modifies the source file in the ephemeral workflow workspace — the committed file remains unchanged. This is simpler and more reliable than runtime meta-tag injection or environment-specific `window.__env` objects. - -**`skip_app_build: true` with a manual build step.** The Static Web Apps action's built-in Oryx builder cannot receive GitHub Secrets as environment variables in the same way a workflow step can. Running `ng build` as a standard workflow step (with secrets available as `env:` variables) gives full control over the build environment. The action is used only for the upload step. - -**`npm ci` not `npm install`.** The `ci` command is deterministic, fast on cache hits, and fails loudly on lock file mismatches. `npm install` can silently update `package-lock.json` during a workflow run and install different versions than what was tested locally. In CI/CD, reproducibility is more important than convenience. - -**`staticwebapp.config.json` in `public/`, not a post-build copy.** The Angular build configuration already copies everything in `public/` to the output directory. Placing the config file there avoids a manual copy step in the workflow and keeps the configuration alongside the application code where it is easy to find and review. - -**Deployment token retrieved via OIDC, not stored as a secret.** The Static Web Apps deployment token is long-lived (it does not expire unless regenerated). Storing it as a GitHub Secret means another secret to manage. Retrieving it from Azure using the OIDC session at deploy time means one fewer secret, and the token is only in memory for the duration of the deploy step. - ---- - -## 🌟 Why This Matters - -Angular deployment is deceptively simple — until a user reports that the login button does nothing, the dashboard shows the wrong data, or half the routes return 404 after a refresh. Each of those failures has a specific cause: wrong IdentityServer URL baked into the bundle, wrong API URL baked in, or missing SPA fallback routing. - -The workflow and configuration in this article solve all three systematically, using patterns that apply to any Angular application hosted on any static file host: - -* **Environment injection at build time** — works for any SPA (React, Vue, Angular) on any host -* **SPA fallback routing config** — required on any static file host that doesn't natively understand client-side routing (S3, GitHub Pages, Azure SWA, Netlify all have equivalent configurations) -* **Build in CI, not in the deploy action** — gives full control over the build environment and makes the build reproducible - -**Transferable skills:** - -* **`sed -i` for config injection** — applies to any build that reads from a source file (environment files, JSON configs, YAML manifests) -* **`staticwebapp.config.json` routing rules** — the exact syntax for Azure SWA; equivalent files exist for S3/CloudFront (`cloudfront-routing.json`) and Netlify (`_redirects`) -* **`npm ci` in CI/CD** — the correct npm command for any CI workflow, regardless of the host or framework - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #azure #angular #githubactions #staticwebapps #cicd #devops #typescript #oidc #clouddeployment #spa #angularmaterial #webdevelopment #fullstack #azurecli #frontend diff --git a/blogs/series-5-devops-data/5.8-azure-post-deployment-config.md b/blogs/series-5-devops-data/5.8-azure-post-deployment-config.md deleted file mode 100644 index 5e1ac94..0000000 --- a/blogs/series-5-devops-data/5.8-azure-post-deployment-config.md +++ /dev/null @@ -1,305 +0,0 @@ -# Connect the Stack: Post-Deployment Configuration and Validation - -## Add Production URLs to IdentityServer, Set API CORS, and Validate the Full Login Flow - -Three services are running in Azure. Each started successfully. The Swagger UI loads. The Angular app loads. But clicking Login redirects to IdentityServer and bounces back with an error — `invalid_redirect_uri`. The stack is deployed but not yet connected. - -This article covers the configuration changes that wire the three services together: adding the production Azure URLs to IdentityServer's client registration, setting the API CORS policy, and walking through a validation checklist that confirms the complete OAuth 2.0 login flow works end-to-end. - -![Post-Deployment Configuration](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-api-endpoints.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article is the final step in the Azure deployment sub-series. Articles 5.3–5.7 provisioned infrastructure and deployed all three services.** - ---- - -## 📚 What You'll Learn - -* **Why `invalid_redirect_uri` happens** — and exactly which config file to edit -* **`identityserverdata.json` structure** — the `RedirectUris`, `PostLogoutRedirectUris`, and `AllowedCorsOrigins` arrays for the Angular client -* **How CORS settings flow** — from `appsettings.json` to the API's middleware pipeline -* **Validation sequence** — the correct order to test each layer before testing the full flow -* **Common failure checklist** — 401, CORS, scope mismatch, and silent refresh errors - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 5.6 complete** — IdentityServer deployed at `https://app-talent-ids-dev.azurewebsites.net` -* **Article 5.7 complete** — Angular deployed at the Static Web App URL (e.g., `https://mango-flower-0ced4011e.4.azurestaticapps.net`) -* **The three Azure URLs** — retrieve them: - -```bash -# API URL -az webapp show \ - --resource-group rg-talent-dev \ - --name app-talent-api-dev \ - --query defaultHostName --output tsv - -# IdentityServer URL -az webapp show \ - --resource-group rg-talent-dev \ - --name app-talent-ids-dev \ - --query defaultHostName --output tsv - -# Static Web App URL -az staticwebapp show \ - --resource-group rg-talent-dev \ - --name swa-talent-ui-dev \ - --query defaultHostname --output tsv -``` - ---- - -## 🎯 The Problem - -OAuth 2.0 authorization codes and tokens can only flow to URLs that are explicitly registered in the authorization server (IdentityServer). If the Angular application running at `https://mango-flower-0ced4011e.4.azurestaticapps.net` sends an authorization request asking IdentityServer to redirect the browser back to that URL, IdentityServer checks its registered `RedirectUris` for the `TalentManagement` client. If the production URL isn't there, IdentityServer rejects the request immediately with `invalid_redirect_uri`. - -Similarly, the API's CORS policy controls which origins can call the API from a browser. If the Angular app's domain is not in the allowed origins list, the browser blocks the API response before Angular can read it — even though the API returned HTTP 200. - -These two configurations — IdentityServer redirect URIs and API CORS origins — are the connective tissue that makes the three-tier stack function as a unit. - ---- - -## 💡 The Solution - -**Note on connection strings:** Database connection strings were stored in Azure Key Vault in Article 5.4 and wired into App Service settings as `@Microsoft.KeyVault(...)` references by the deploy workflows in Article 5.6. No `appsettings.json` changes are needed for connection strings — this article focuses only on IdentityServer redirect URIs and API CORS origins, which are URLs (not secrets) and are set by the deploy workflows as plain values. - -Add the production Static Web App URL to three lists in `identityserverdata.json` (the file that seeds IdentityServer's database): - -* `RedirectUris` — where IdentityServer may send authorization codes after login -* `PostLogoutRedirectUris` — where IdentityServer may redirect after logout -* `AllowedCorsOrigins` — origins that IdentityServer's token endpoint accepts cross-origin requests from - -Then update the API's allowed CORS origins so the Angular app can call protected endpoints. - -Both changes go into source control, IdentityServer's database is re-seeded, and the API is redeployed with updated settings. - ---- - -## 🚀 How It Works - -### Step 1: Find Your Static Web App URL - -```bash -az staticwebapp show \ - --resource-group rg-talent-dev \ - --name swa-talent-ui-dev \ - --query defaultHostname \ - --output tsv -``` - -The URL follows the pattern `https://.azurestaticapps.net`. Record this value — it is needed in several places. - -If you configured a custom domain, use that instead. - -### Step 2: Update `identityserverdata.json` - -Open `TokenService/Duende-IdentityServer/shared/identityserverdata.json`. Find the `TalentManagement` client section: - -```json -{ - "ClientId": "TalentManagement", - "ClientName": "Talent Management Demo Angular App", - "RedirectUris": [ - "http://localhost:4200", - "https://localhost:4200", - "http://localhost:4200/silent-refresh.html", - "https://localhost:4200/silent-refresh.html", - "http://localhost:4200/callback", - "https://localhost:4200/callback" - ], - "PostLogoutRedirectUris": [ - "http://localhost:4200", - "https://localhost:4200" - ], - "AllowedCorsOrigins": [ - "http://localhost:4200", - "https://localhost:4200" - ] -} -``` - -Add the production Azure URLs to each list. Replace `https://mango-flower-0ced4011e.4.azurestaticapps.net` with your actual Static Web App URL: - -```json -{ - "ClientId": "TalentManagement", - "ClientName": "Talent Management Demo Angular App", - "RedirectUris": [ - "http://localhost:4200", - "https://localhost:4200", - "http://localhost:4200/silent-refresh.html", - "https://localhost:4200/silent-refresh.html", - "http://localhost:4200/callback", - "https://localhost:4200/callback", - "https://mango-flower-0ced4011e.4.azurestaticapps.net", - "https://mango-flower-0ced4011e.4.azurestaticapps.net/silent-refresh.html", - "https://mango-flower-0ced4011e.4.azurestaticapps.net/callback", - "https://workcontrolgit.github.io/AngularNetTutorial", - "https://workcontrolgit.github.io/AngularNetTutorial/silent-refresh.html", - "https://workcontrolgit.github.io/AngularNetTutorial/callback" - ], - "PostLogoutRedirectUris": [ - "http://localhost:4200", - "https://localhost:4200", - "https://mango-flower-0ced4011e.4.azurestaticapps.net", - "https://workcontrolgit.github.io/AngularNetTutorial" - ], - "AllowedCorsOrigins": [ - "http://localhost:4200", - "https://localhost:4200", - "https://mango-flower-0ced4011e.4.azurestaticapps.net", - "https://workcontrolgit.github.io" - ] -} -``` - -**Three redirect URIs for the production URL:** -* The root URL — for the implicit return to the app after login -* `/silent-refresh.html` — for the silent token refresh iframe (background token renewal without a visible login page) -* `/callback` — the explicit OAuth callback route that the angular-auth-oidc-client library uses to process the authorization code - -### Step 3: Re-seed IdentityServer's Database - -`identityserverdata.json` is a seed file. On first run (or when the IdentityServer Admin seeds on startup), its contents are written to the IdentityServer database tables. After editing the file, IdentityServer's database must be updated. - -**Option A: Redeploy IdentityServer** — push the change to `main`, the IdentityServer workflow runs, IdentityServer restarts and re-seeds its database from the updated file on startup (if seed-on-startup is configured). - -**Option B: Use the Admin UI** — navigate to `https://app-talent-ids-dev.azurewebsites.net/admin` (if the Admin project is deployed), log in with admin credentials, and update the client's redirect URIs and CORS origins directly through the UI. Changes take effect immediately without a deployment. - -For this tutorial, Option A (commit and push) is preferred — the source file remains the source of truth. - -### Step 4: Update API CORS Settings - -The API allows CORS origins configured via App Service settings using the `Cors__AllowedOrigins__` array pattern. Two origins are needed — the Azure Static Web App and GitHub Pages: - -```bash -az webapp config appsettings set \ - --resource-group rg-talent-dev \ - --name app-talent-api-dev \ - --settings \ - "Cors__AllowedOrigins__0=https://mango-flower-0ced4011e.4.azurestaticapps.net" \ - "Cors__AllowedOrigins__1=https://workcontrolgit.github.io" -``` - -**Note:** Replace `https://mango-flower-0ced4011e.4.azurestaticapps.net` with your actual Static Web App URL. The GitHub Pages origin is the host only — no path suffix. This is handled automatically by the `deploy-api.yml` workflow on every deployment, so this manual step is only needed if you are configuring the API outside of a workflow run. - -The API's CORS middleware reads `Cors:AllowedOrigins` from `IConfiguration` at startup. The `__` double-underscore in the setting name maps to the `:` separator, and the `__0` / `__1` suffixes create an array in .NET configuration. - -### Step 5: Validate Each Layer in Order - -Test from the bottom up. Do not attempt end-to-end validation until each individual layer passes. - -**Layer 1: IdentityServer discovery endpoint** - -```bash -curl https://app-talent-ids-dev.azurewebsites.net/.well-known/openid-configuration -``` - -Expected: a JSON object containing `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`. If this fails, IdentityServer is not running or the App Service is not healthy — check Application Insights or the App Service log stream. - -**Layer 2: API health endpoint** - -```bash -curl https://app-talent-api-dev.azurewebsites.net/api/v1/health -``` - -Or open `https://app-talent-api-dev.azurewebsites.net/swagger`. If Swagger loads, the API started correctly. If it returns 500, check the App Service settings — a missing or malformed connection string is the most common cause. - -**Layer 3: Angular app loads** - -Open the Static Web App URL in a browser. The Angular application should load — the nav bar, the login button, and the default page should be visible without any console errors. If the page is blank or shows a JavaScript error, the `environment.prod.ts` injection likely used the wrong URL. - -Check the browser console for errors like `cannot GET /dashboard` (SPA routing fallback missing) or `Failed to fetch` (wrong API URL). - -**Layer 4: Login redirect** - -Click Login. The browser should redirect to `https://app-talent-ids-dev.azurewebsites.net/Account/Login`. The IdentityServer login page should appear. If the browser instead shows `invalid_redirect_uri`, Step 2 is incomplete — the production callback URL is not in `RedirectUris`. - -**Layer 5: Login with test credentials** - -Enter test credentials (`ashtyn1` / `Pa$$word123`). IdentityServer authenticates the user and redirects back to the Angular application with an authorization code in the query string. Angular's OIDC library exchanges the code for tokens. - -After successful login: -* The Angular route should update to the dashboard -* The browser developer tools → Application → Local Storage should show the OIDC tokens -* Network tab should show API requests with an `Authorization: Bearer ...` header - -**Layer 6: API call with Bearer token** - -Navigate to the Employees list in the Angular app. The network tab should show `GET /api/v1/employees` returning HTTP 200. If it returns 401, the `Sts__ServerUrl` App Service setting on the API points to a wrong or unreachable IdentityServer URL. If it returns a CORS error, the CORS setting in Step 4 is missing or incorrect. - ---- - -## 💻 Try It Yourself - -After completing Steps 1–4, run through the Layer 1–6 validation in order. Each layer takes under 60 seconds to validate. A failure at any layer gives a clear signal of what to fix — you don't need to guess why the login flow isn't working if you know exactly which layer failed. - -**Quick CORS verification from the browser console:** - -```javascript -// From Azure SWA -fetch('https://app-talent-api-dev.azurewebsites.net/api/v1/health', { - headers: { 'Origin': 'https://mango-flower-0ced4011e.4.azurestaticapps.net' } -}).then(r => console.log('Status:', r.status, 'CORS:', r.headers.get('access-control-allow-origin'))) - -// From GitHub Pages -fetch('https://app-talent-api-dev.azurewebsites.net/api/v1/health', { - headers: { 'Origin': 'https://workcontrolgit.github.io' } -}).then(r => console.log('Status:', r.status, 'CORS:', r.headers.get('access-control-allow-origin'))) -``` - -Expected for both: `Status: 200 CORS: ` - ---- - -## 🔑 Key Design Decisions - -**Add production URLs alongside localhost, not instead.** The `localhost` entries in `identityserverdata.json` keep local development working without any configuration change. A developer cloning the repo and running locally does not need to edit a config file to log in. The production and local URLs coexist in the same list. - -**Three redirect URIs per environment, not one.** OAuth 2.0 redirect URI matching is exact — a trailing slash, a missing path segment, or an `http` vs `https` mismatch causes a rejection. The three patterns (root, `/callback`, `/silent-refresh.html`) cover the full OIDC library's behavior: the initial login callback, the explicit callback route, and the silent token renewal iframe. - -**Validate layer by layer, not end-to-end first.** Attempting to log in before verifying the discovery endpoint conflates multiple possible failures. If login fails, you don't know whether IdentityServer is unreachable, the redirect URI is wrong, or the Angular env vars are incorrect. Bottom-up validation isolates each failure to a single cause. - -**App Service settings for CORS origins, not a code change.** The API CORS policy reads from configuration. Updating allowed origins as an App Service setting (rather than editing `appsettings.json` and redeploying) takes effect on the next app restart without a new deployment. This is the correct pattern for any configuration that may change between environments — keep the code the same, change only the config. - ---- - -## 🌟 Why This Matters - -Post-deployment configuration is the step most tutorials skip. They provision infrastructure, deploy the code, and leave it to the reader to figure out why the login button doesn't work. The `invalid_redirect_uri` error, CORS rejections, and 401 responses from the API are not deployment failures — they are configuration gaps that require connecting the services together after deployment. - -Understanding the configuration layer means understanding the security model. IdentityServer's explicit allowlist of redirect URIs is a security control, not a convenience setting. It prevents an attacker from crafting a login URL that redirects tokens to their own server. CORS origins on the API prevent browsers from leaking token-authenticated responses to unauthorized origins. These are not obstacles to get past — they are the reason the auth flow is trustworthy. - -**Transferable skills:** - -* **IdentityServer client configuration** — `RedirectUris`, `PostLogoutRedirectUris`, and `AllowedCorsOrigins` are standard OpenID Connect client properties; the same patterns apply to any OpenID Connect provider (Auth0, Azure Entra ID, Okta) -* **Layer-by-layer validation** — the discovery endpoint → health endpoint → app load → redirect → login → API call sequence applies to any three-tier app with OAuth 2.0 -* **App Service settings for CORS** — the `az webapp config appsettings set` pattern for runtime config applies to any .NET setting, not just CORS - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #azure #identityserver #oauth2 #oidc #angular #dotnet #cors #appservice #staticwebapps #cicd #devops #authentication #security #webdevelopment #fullstack diff --git a/blogs/series-5-devops-data/5.9-azure-key-vault-secrets.md b/blogs/series-5-devops-data/5.9-azure-key-vault-secrets.md deleted file mode 100644 index 4574e07..0000000 --- a/blogs/series-5-devops-data/5.9-azure-key-vault-secrets.md +++ /dev/null @@ -1,496 +0,0 @@ -# Zero Secrets in Config: Azure Key Vault for App Service and .NET Code - -## Store Connection Strings in Key Vault, Reference Them Without Hardcoding Anything - -Every deployment tutorial eventually gets to the awkward part: where do you put the database password? Environment variables work but are visible in plain text in the Azure Portal. GitHub Secrets keep them out of source control but they still get injected as raw values into App Service settings — visible to anyone with portal access. - -Azure Key Vault solves this end to end. Secrets live in one vault, access is controlled by managed identity (no passwords to rotate), and App Service resolves references at runtime. The .NET app reads `ConnectionStrings:DefaultConnection` exactly as it always has — it never knows Key Vault is involved. - -This article covers two use cases: **App Service environment settings** (no code changes required) and **reading secrets directly in .NET code** using `DefaultAzureCredential`. - -![Azure Key Vault](https://raw.githubusercontent.com/workcontrolgit/AngularNetTutorial/master/docs/images/webapi/swagger-api-endpoints.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series covering Angular 20, .NET 10, and OAuth 2.0 with Duende IdentityServer. **Article 5.4 provisioned the Key Vault and managed identities. This article puts them to use.** - ---- - -## 📚 What You'll Learn - -* **Key Vault secret naming** — why hyphens, not double-underscores -* **App Service Key Vault references** — the `@Microsoft.KeyVault(SecretUri=...)` syntax and how to verify it works -* **`DefaultAzureCredential`** — how one credential works for both local development and Azure production with no code changes -* **`AddAzureKeyVault()` in .NET** — how to wire Key Vault into `IConfiguration` so existing code requires zero changes -* **`SecretClient`** — reading a specific secret directly in code when you need it on demand -* **Visual Studio Manage User Secrets** — the local development equivalent of Key Vault, stored in AppData and never committed to git -* **Three secret sources, one IConfiguration key** — how user secrets, GitHub Secrets, and Key Vault work together across environments - ---- - -## 🎯 The Problem - -After deploying the Talent Management stack in Articles 5.6–5.8, the GitHub Actions workflows set App Service configuration like this: - -```bash -az webapp config appsettings set \ - --settings \ - "ConnectionStrings__DefaultConnection=$API_CONN" \ - "Sts__ServerUrl=$IDS_URL" -``` - -This works — but the raw connection string value (including username and password) is now visible in the Azure Portal under **App Service → Configuration → Application settings**. Anyone with Contributor access to the resource group can read it. - -**The better approach:** - -* Store secrets in Key Vault once -* Reference them from App Service settings using a Key Vault URI -* The Portal shows a Key Vault icon — never the raw value -* .NET code reads `IConfiguration` exactly as before — no changes needed - ---- - -## 💡 How It Works - -### The Three-Part Setup - -**Part 1 — Infrastructure (already done in Article 5.4):** -* Key Vault `kv-talent-dev` was provisioned -* Each Web App was given a system-assigned managed identity -* The `Key Vault Secrets User` role was granted to each identity - -**Part 2 — Secrets (covered in this article):** -* Store actual secret values in Key Vault using `az keyvault secret set` -* Update App Service settings to reference Key Vault URIs instead of raw values - -**Part 3 — .NET code (covered in this article):** -* Add `Azure.Extensions.AspNetCore.Configuration.Secrets` NuGet package -* Wire `AddAzureKeyVault()` into `Program.cs` -* Optionally use `SecretClient` for direct on-demand secret access - ---- - -## 🔑 Part 1: App Service Key Vault References - -This approach requires **zero .NET code changes**. App Service resolves Key Vault references before injecting values into the app's environment. - -### Step 1: Understand Secret Naming - -Key Vault secret names use **hyphens** (`-`) only — no colons, no double-underscores. App Service automatically maps: - -* `ConnectionStrings--DefaultConnection` → `ConnectionStrings:DefaultConnection` -* `Sts--ServerUrl` → `Sts:ServerUrl` -* `JWTSettings--Key` → `JWTSettings:Key` - -The double-hyphen (`--`) maps to the section separator (`:`) used in .NET configuration. This is a fixed Azure convention. - -### Step 2: Store Secrets in Key Vault - -```bash -# API database connection string -az keyvault secret set \ - --vault-name kv-talent-dev \ - --name "ConnectionStrings--DefaultConnection" \ - --value "Server=sql-talent-dev.database.windows.net;Database=sqldb-talent-api-dev;User Id=sqladmin;Password=YourPassword;" - -# IdentityServer database connection string -az keyvault secret set \ - --vault-name kv-talent-dev \ - --name "ConnectionStrings--ConfigurationDbConnection" \ - --value "Server=sql-talent-dev.database.windows.net;Database=sqldb-talent-ids-dev;User Id=sqladmin;Password=YourPassword;" - -# JWT signing key -az keyvault secret set \ - --vault-name kv-talent-dev \ - --name "JWTSettings--Key" \ - --value "YourJwtSigningKey" - -# IdentityServer URL (used by both API and STS) -az keyvault secret set \ - --vault-name kv-talent-dev \ - --name "Sts--ServerUrl" \ - --value "https://app-talent-ids-dev.azurewebsites.net" -``` - -Verify the secrets were stored: - -```bash -az keyvault secret list \ - --vault-name kv-talent-dev \ - --query "[].name" \ - --output table -``` - -### Step 3: Build Key Vault Reference URIs - -App Service uses the secret's URI (without version) so it always resolves the latest value: - -```bash -# Get the vault URI -KV_URI=$(az keyvault show \ - --name kv-talent-dev \ - --query properties.vaultUri \ - --output tsv) - -# URI format: {vaultUri}secrets/{secretName} -# Example: https://kv-talent-dev.vault.azure.net/secrets/ConnectionStrings--DefaultConnection -``` - -### Step 4: Update App Service Settings to Use Key Vault References - -Replace raw values with Key Vault references using the `@Microsoft.KeyVault(SecretUri=...)` syntax: - -```bash -KV_URI=$(az keyvault show --name kv-talent-dev --query properties.vaultUri --output tsv) - -az webapp config appsettings set \ - --resource-group rg-talent-dev \ - --name app-talent-api-dev \ - --settings \ - "ConnectionStrings__DefaultConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--DefaultConnection)" \ - "JWTSettings__Key=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/JWTSettings--Key)" \ - "Sts__ServerUrl=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/Sts--ServerUrl)" -``` - -**Note:** App Service setting names still use double-underscore (`__`) — this is how .NET maps flat environment variable names to nested configuration keys. Only Key Vault secret names use double-hyphen (`--`). - -### Step 5: Verify the References Resolve - -In the Azure Portal: - -1. Go to **App Service → app-talent-api-dev → Configuration → Application settings** -2. Each Key Vault reference setting shows a **Key Vault icon** (🔑) -3. The status column shows ✅ **green check** = resolved successfully, or ❌ **red X** = access problem - -If you see a red X, check: - -* The managed identity has the `Key Vault Secrets User` role on the vault (Article 5.4) -* The secret name in the URI exactly matches the secret stored in Key Vault (case-sensitive) -* The vault URI ends with a `/` before `secrets/` - -The .NET app reads `ConnectionStrings:DefaultConnection` via `IConfiguration` as usual — it receives the resolved value, never the `@Microsoft.KeyVault(...)` string. - -### Step 6: Update GitHub Actions to Use Key Vault References - -Update the deploy workflow so future deployments inject Key Vault references instead of raw secret values. In `.github/workflows/deploy-api.yml`, replace the raw `$API_CONN` injection: - -```yaml -- name: Configure App Service settings - env: - IDS_URL: ${{ secrets.IDENTITY_SERVER_URL }} - ANGULAR_URL: ${{ secrets.ANGULAR_APP_URL }} - KV_URI: ${{ secrets.KEY_VAULT_URI }} # e.g. https://kv-talent-dev.vault.azure.net/ - run: | - az webapp config appsettings set \ - --resource-group ${{ env.RESOURCE_GROUP }} \ - --name ${{ env.APP_SERVICE_NAME }} \ - --settings \ - "ConnectionStrings__DefaultConnection=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/ConnectionStrings--DefaultConnection)" \ - "JWTSettings__Key=@Microsoft.KeyVault(SecretUri=${KV_URI}secrets/JWTSettings--Key)" \ - "Sts__ServerUrl=$IDS_URL" \ - "Sts__ValidIssuer=$IDS_URL" \ - "Cors__AllowedOrigins__0=$ANGULAR_URL" \ - "Cors__AllowedOrigins__1=https://workcontrolgit.github.io" \ - "ASPNETCORE_ENVIRONMENT=Production" -``` - -Add `KEY_VAULT_URI` as a GitHub Secret (value: `https://kv-talent-dev.vault.azure.net/`). Note that `Sts__ServerUrl` can remain a plain value since it is not sensitive — Key Vault is for secrets only, not every configuration value. - ---- - -## 🖥️ Part 2: Reading Secrets Directly in .NET Code - -Sometimes you need a secret on demand in code — not just at startup via configuration. Use `SecretClient` with `DefaultAzureCredential` for this. - -### Step 1: Add NuGet Packages - -In the `TalentManagementAPI.WebApi` project: - -```bash -dotnet add package Azure.Identity -dotnet add package Azure.Security.KeyVault.Secrets -dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets -``` - -* **`Azure.Identity`** — provides `DefaultAzureCredential`, which tries multiple authentication methods in order -* **`Azure.Security.KeyVault.Secrets`** — the `SecretClient` for on-demand secret access -* **`Azure.Extensions.AspNetCore.Configuration.Secrets`** — wires Key Vault into `IConfiguration` via `AddAzureKeyVault()` - -### Step 2: Wire Key Vault into IConfiguration - -Add `AddAzureKeyVault()` in `Program.cs` before `builder.Build()`. When added as a configuration source, **all Key Vault secrets become available through `IConfiguration`** — including secrets not referenced in App Service settings. - -```csharp -// Program.cs - -using Azure.Identity; - -var builder = WebApplication.CreateBuilder(args); - -// Add Key Vault as a configuration source (production only) -// In development: uses your logged-in Azure CLI identity (az login) -// In production: uses the App Service's system-assigned managed identity -if (!builder.Environment.IsDevelopment()) -{ - var keyVaultUri = new Uri(builder.Configuration["KeyVault:Uri"] - ?? throw new InvalidOperationException("KeyVault:Uri not configured")); - - builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential()); -} - -// ... rest of service registration -``` - -Add the Key Vault URI to `appsettings.json` (not a secret — just the vault address): - -```json -{ - "KeyVault": { - "Uri": "https://kv-talent-dev.vault.azure.net/" - } -} -``` - -**After this change, all Key Vault secrets are available via `IConfiguration` using their secret name.** Secret `ConnectionStrings--DefaultConnection` in Key Vault maps to `ConnectionStrings:DefaultConnection` in `IConfiguration` — the same key EF Core already reads. - -**`DefaultAzureCredential` authentication chain:** - -* **Local development** — uses your `az login` session (Visual Studio, Azure CLI, or VS Code Azure extension) -* **Azure App Service** — uses the system-assigned managed identity automatically -* **No code change needed** between environments — the same line works everywhere - -### Step 3: Local Development with User Secrets - -Do not use Key Vault during local development — it requires network access and an Azure login. Instead, keep using `dotnet user-secrets` for local secrets: - -```bash -# Store the connection string locally (never committed to git) -dotnet user-secrets set "ConnectionStrings:DefaultConnection" \ - "Server=localhost;Database=TalentManagementApiDb;Trusted_Connection=True;" \ - --project ApiResources/TalentManagement-API/TalentManagementAPI.WebApi -``` - -User secrets take precedence over `appsettings.json` in development. Key Vault takes precedence over `appsettings.json` in production (when added as a configuration source via `AddAzureKeyVault()`). - -**Configuration precedence (highest to lowest):** - -* Environment variables (App Service settings) -* User secrets (local development only) -* `AddAzureKeyVault()` (production) -* `appsettings.{Environment}.json` -* `appsettings.json` - -### Step 4: Read a Secret On Demand with SecretClient - -For cases where you need a secret at a specific point in code rather than at startup — for example, fetching a third-party API key only when a specific controller action runs: - -```csharp -using Azure.Identity; -using Azure.Security.KeyVault.Secrets; - -public class ExternalIntegrationService -{ - private readonly SecretClient _secretClient; - - public ExternalIntegrationService(IConfiguration configuration) - { - var vaultUri = new Uri(configuration["KeyVault:Uri"]!); - _secretClient = new SecretClient(vaultUri, new DefaultAzureCredential()); - } - - public async Task GetApiKeyAsync() - { - KeyVaultSecret secret = await _secretClient.GetSecretAsync("ExternalApi--ApiKey"); - return secret.Value.Value; - } -} -``` - -Register it in `Program.cs`: - -```csharp -builder.Services.AddScoped(); -``` - -`DefaultAzureCredential` is safe to instantiate multiple times — it is lightweight and stateless. The actual token is cached by the Azure SDK. - -### Step 5: Register SecretClient as a Singleton (Optional) - -If multiple services need on-demand secret access, register `SecretClient` once in DI rather than constructing it in each service: - -```csharp -// Program.cs -builder.Services.AddSingleton(sp => -{ - var vaultUri = new Uri(builder.Configuration["KeyVault:Uri"]!); - return new SecretClient(vaultUri, new DefaultAzureCredential()); -}); -``` - -Inject it directly: - -```csharp -public class MyService(SecretClient secretClient) -{ - public async Task DoSomethingAsync() - { - var secret = await secretClient.GetSecretAsync("MySecret--Name"); - // use secret.Value.Value - } -} -``` - ---- - -## 🖥️ Local Development: Visual Studio Manage User Secrets - -During local development you don't need Key Vault — it requires Azure network access and adds friction for every team member. Visual Studio has a built-in equivalent called **Manage User Secrets** that stores secrets outside the project folder so they're never committed to git. - -### How to Open It - -Right-click the `TalentManagementAPI.WebApi` project in Solution Explorer → **Manage User Secrets** - -This opens a `secrets.json` file stored at: - -``` -C:\Users\{you}\AppData\Roaming\Microsoft\UserSecrets\d7dba4fb-c08e-453d-9e13-d7d0f8ba8ff0\secrets.json -``` - -The GUID is the `` value already in `TalentManagementAPI.WebApi.csproj`. Each developer has their own copy — the file never appears in git. - -### What to Put in secrets.json - -```json -{ - "ConnectionStrings": { - "DefaultConnection": "Server=localhost;Database=TalentManagementApiDb;Trusted_Connection=True;MultipleActiveResultSets=true" - }, - "JWTSettings": { - "Key": "YourLocalJwtSigningKeyMinimum32Characters" - }, - "Sts": { - "ServerUrl": "https://localhost:44310", - "ValidIssuer": "https://localhost:44310" - } -} -``` - -These values override `appsettings.json` automatically in the `Development` environment — no code change required. - -### The Full Picture: Three Secret Sources, One IConfiguration Key - -The same `ConnectionStrings:DefaultConnection` key is read by the .NET app in every environment. What changes is where the value comes from: - -**Local development — Visual Studio Manage User Secrets:** -* Stored in `AppData` on your machine -* Never in git, never in source control -* Overrides `appsettings.json` automatically - -**GitHub Actions deploy — GitHub Secrets:** -* Used to log in to Azure and run Bicep -* Writes `@Microsoft.KeyVault(...)` references to App Service settings -* The actual database password is never in GitHub Secrets at all - -**Azure App Service runtime — Key Vault:** -* App Service resolves `@Microsoft.KeyVault(...)` references via managed identity -* The running app receives the real connection string value -* Never visible in plain text in the Portal - -``` -Local dev → VS Manage User Secrets → AppData on your machine -CI/CD deploy → GitHub Secrets → Azure login + KV reference URIs -Azure runtime → Azure Key Vault → managed identity, resolved at startup -``` - -**The application code never changes across environments.** `builder.Configuration["ConnectionStrings:DefaultConnection"]` works the same way whether the value came from `secrets.json`, an App Service setting, or a Key Vault reference. - ---- - -## 🔄 When to Use Each Approach - -**App Service Key Vault references** (`@Microsoft.KeyVault(...)` in settings): -* Connection strings read by EF Core at startup -* JWT keys read by `AddJwtBearer()` at startup -* Any secret injected via `IConfiguration` that is read once at startup -* ✅ Zero code changes required - -**`AddAzureKeyVault()` in Program.cs:** -* You want all Key Vault secrets available anywhere via `IConfiguration` -* You have secrets that are read after startup (not just at startup) -* You want Key Vault as the single source of truth, removing App Service settings entirely -* ✅ One-time code change, then all secrets work the same way - -**`SecretClient` in code:** -* You need a secret only in specific scenarios (not at startup) -* You want to fetch the latest value dynamically without restarting the app -* You need to list, create, or rotate secrets programmatically -* ✅ Full control over when secrets are fetched - ---- - -## 💻 Try It Yourself - -After storing secrets and updating App Service settings, verify end-to-end: - -```bash -# Check that Key Vault references resolved successfully -az webapp config appsettings list \ - --resource-group rg-talent-dev \ - --name app-talent-api-dev \ - --query "[?contains(value, '@Microsoft.KeyVault')].[name,value]" \ - --output table - -# Test the API health endpoint (confirms the app started successfully) -curl https://app-talent-api-dev.azurewebsites.net/health -``` - -If the app starts and the health endpoint returns `Healthy`, the connection string was resolved from Key Vault and EF Core connected to the database successfully. - ---- - -## 🔑 Key Design Decisions - -**App Service references over `AddAzureKeyVault()` for connection strings.** The App Service reference approach (`@Microsoft.KeyVault(...)`) requires zero code changes and works for any app regardless of framework or language. The .NET `AddAzureKeyVault()` approach is more powerful — all secrets flow through `IConfiguration` — but requires adding packages and modifying `Program.cs`. For a tutorial project, both are demonstrated so you can choose based on your needs. - -**`DefaultAzureCredential` over explicit `ManagedIdentityCredential`.** `DefaultAzureCredential` tries a chain of credential types automatically. In local development it uses the Azure CLI session. In App Service it uses the managed identity. Using `ManagedIdentityCredential` directly would fail locally; using `DefaultAzureCredential` works everywhere with no environment-specific code. - -**User secrets for local development, Key Vault for production.** Never connect local development to a shared Key Vault — it creates a dependency on network access and complicates onboarding. Each developer uses their own local database with `dotnet user-secrets`. Key Vault is a production concern. - -**Versionless secret URIs for App Service references.** Omitting the version from the Key Vault reference URI (`secrets/MySecret` not `secrets/MySecret/abc123`) means the App Service always resolves the latest version. When you rotate a secret in Key Vault, the App Service picks it up on next restart with no configuration change. - ---- - -## 🌟 Why This Matters - -The pattern in this article — managed identity + Key Vault + App Service references — is the production-grade zero-credential approach recommended by Microsoft's Well-Architected Framework. No password in source control, no password in a GitHub Secret that gets injected as plain text, no password visible in the Portal. - -The same `IConfiguration` API that reads from `appsettings.json` in development reads from Key Vault in production. Application code never changes based on where a secret comes from. This separation of configuration source from configuration consumption is the goal. - -**Transferable skills:** - -* **`DefaultAzureCredential`** — applies to any Azure SDK call: Storage, Service Bus, Cosmos DB, all follow the same pattern -* **App Service Key Vault references** — works for any language/framework deployed to App Service (Node.js, Python, Java) — not just .NET -* **`IConfiguration` + `AddAzureKeyVault()`** — the standard .NET pattern for centralized secret management across multiple environments - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -📖 **Series:** [AngularNetTutorial Series Navigation](../SERIES-NAVIGATION-TOC.md) - ---- - -**📌 Tags:** #azure #keyvault #dotnet #managedidentity #security #secretsmanagement #appservice #azureidentity #csharp #devops #zerotrust #cloudnative #configuration #githubactions #fullstack diff --git a/blogs/series-6-ai-app-features/6.1-dotnet-ai-foundation.md b/blogs/series-6-ai-app-features/6.1-dotnet-ai-foundation.md deleted file mode 100644 index 112cf5d..0000000 --- a/blogs/series-6-ai-app-features/6.1-dotnet-ai-foundation.md +++ /dev/null @@ -1,448 +0,0 @@ -# Run a Local LLM in Your .NET 10 API with Ollama - -## How Microsoft.Extensions.AI, OllamaSharp, and a Custom Service Interface Make Your .NET 10 API AI-Ready - -Every developer wants AI in their app. The problem is getting started: API keys, cloud costs, rate limits, and the fear of betting your architecture on one vendor. What if you could add a working AI endpoint to your .NET 10 API in under an hour — for free, running entirely on your laptop? - -This article shows you exactly how, using [Ollama](https://ollama.com) for a local LLM and [OllamaSharp](https://github.com/awaescher/OllamaSharp) as the .NET client library. - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article kicks off Series 6 by adding AI capabilities to the existing TalentManagement API — without breaking any existing functionality for developers who don't have Ollama installed.** - ---- - -## 🎓 What You'll Learn - -* **Microsoft.Extensions.AI (MEA)** — The GA abstraction layer shipping with .NET 10 that gives you a single `IChatClient` interface across all AI providers -* **OllamaSharp + MEA** — How OllamaSharp 5.x natively implements `IChatClient`, making Ollama a drop-in MEA provider with no extra package -* **Ollama integration** — Pull a free local model and connect it to your .NET API in minutes -* **Feature flag gating** — Why per-method `IsEnabledAsync` checks return `503` instead of the misleading `404` from `[FeatureGate]` -* **Clean Architecture placement** — Where AI interfaces, implementations, and controllers belong in the layer structure -* **Custom `IAiChatService` interface** — How defining your own service interface in the Application layer hides MEA/OllamaSharp from callers and makes the implementation swappable - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **TalentManagement stack running** — Complete [Series 0–5](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) or clone the tutorial repo -* **.NET 10 SDK** — `dotnet --version` should show `10.x` -* **Ollama installed** — Download from [ollama.com](https://ollama.com/download) (free, no account required) -* **llama3.2 model pulled** — `ollama pull llama3.2` (~2 GB download) -* **Basic C# and Clean Architecture familiarity** — Understanding of interfaces, DI, and MediatR helps - -**Not set up yet?** Follow the [AngularNetTutorial setup guide](https://github.com/workcontrolgit/AngularNetTutorial) first. - ---- - -## 🎯 The Problem - -Adding AI to a production .NET API sounds daunting. Most tutorials show you how to call OpenAI with an API key — which is fine until you hit a rate limit, get an unexpected bill, or need to demo the app offline. Developers following a tutorial shouldn't need a credit card. - -Beyond getting started, there's an architectural risk: if your AI code reaches directly into the OpenAI SDK, switching providers later means touching every file that calls it. You've created a tight dependency on one vendor. - -**Common pain points:** - -* **Vendor lock-in** — Switching from OpenAI to Azure OpenAI (or Ollama) requires rewriting service code -* **Cost barrier** — Cloud LLMs require API keys, rate limits, and billing setup before you can write a single test -* **Feature flag complexity** — Without proper gating, enabling AI affects every user — even those on machines without Ollama installed - ---- - -## 💡 The Solution - -**[Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai)** (MEA) is the standard AI abstraction layer that ships GA with .NET 10. It defines `IChatClient` — a single interface for chat completions that works across OpenAI, Azure OpenAI, Ollama, and any other provider. You code against `IChatClient`; switching providers is a one-line DI change. - -**[OllamaSharp](https://github.com/awaescher/OllamaSharp) 5.x** natively implements `IChatClient` from MEA. The former `Microsoft.Extensions.AI.Ollama` provider package has been deprecated — OllamaSharp is now the recommended path. That means you need just two NuGet packages in `Infrastructure.Shared`: `Microsoft.Extensions.AI.Abstractions` for the interface and `OllamaSharp` for the implementation. - -[Ollama](https://ollama.com) runs open-weight models like `llama3.2` locally. No API key. No cloud. Works offline. Perfect for tutorials and development. - -Provider independence has two layers: MEA's `IChatClient` (standard .NET 10 interface) and our own `IAiChatService` (defined in the Application layer). `OllamaAiService` receives `IChatClient` from DI and wraps it in the Application-layer contract. To swap from Ollama to Azure OpenAI, you register a different `IChatClient` implementation — `OllamaAiService` itself is untouched, and the Application layer, handlers, and controller never change. - -We gate AI per-method with `IFeatureManagerSnapshot.IsEnabledAsync("AiEnabled")` rather than a class-level `[FeatureGate]` attribute. The attribute returns `404 Not Found` when disabled — confusing for a known endpoint. The per-method check returns `503 Service Unavailable` with a `detail` message that tells the developer exactly what to enable and where. - -**Key benefits:** - -* ✅ **Zero cost** — Ollama is free; no API key, no credit card, no rate limits -* ✅ **Standard .NET 10 AI abstraction** — MEA's `IChatClient` is GA and built into the platform; no preview dependencies -* ✅ **Provider swap in one line** — Register a different `IChatClient` to move from Ollama to Azure OpenAI; no service code changes -* ✅ **Safe coexistence** — Feature flag default `false` means original tutorial (Series 0–5) works unchanged -* ✅ **Clean Architecture** — Interface in Application, implementation in Infrastructure.Shared, controller in WebApi - ---- - -## 🚀 How It Works - -### Step 1: Install Ollama and Pull a Model - -Download Ollama from [ollama.com/download](https://ollama.com/download) for your OS. After installation: - -```bash -# Pull the llama3.2 model (~2 GB — fast, capable, great for tutorials) -ollama pull llama3.2 - -# Start the Ollama server (runs at http://localhost:11434) -ollama serve - -# Verify it's running -curl http://localhost:11434/api/tags -``` - -**What this does:** Ollama downloads model weights and runs a local HTTP server that accepts chat requests. Our .NET API will call this endpoint internally — no external network traffic. - -### Step 2: Add NuGet Packages - -Add two packages to the Infrastructure.Shared project — the MEA abstraction and the OllamaSharp implementation: - -**`TalentManagementAPI.Infrastructure.Shared.csproj`**: - -```xml - - -``` - -**Why both packages?** - -`Microsoft.Extensions.AI.Abstractions` ships GA with .NET 10. It defines `IChatClient` — the standard interface for chat completions across all AI providers. Coding against `IChatClient` means your service code never changes when you switch from Ollama to Azure OpenAI or any other provider. - -`OllamaSharp` 5.x natively implements `IChatClient`. The former `Microsoft.Extensions.AI.Ollama` provider package has been **deprecated** — OllamaSharp is the recommended path for Ollama integration. This means you do not need a separate adapter package: `OllamaApiClient` from OllamaSharp implements both `IChatClient` (for chat) and `IOllamaApiClient` (for embeddings), and you register it once as a singleton. - -**No provider boilerplate in Program.cs.** All AI registration stays inside `AddSharedInfrastructure` — the WebApi project adds zero AI-specific setup. - -### Step 3: Add Feature Flag and Ollama Config - -In `TalentManagementAPI.WebApi/appsettings.json`, add `AiEnabled` to the existing `FeatureManagement` section and a new `Ollama` section: - -```json -"FeatureManagement": { - "AuthEnabled": true, - "CacheEnabled": true, - "AiEnabled": false -}, -"Ollama": { - "BaseUrl": "http://localhost:11434", - "Model": "llama3.2", - "EmbeddingModel": "nomic-embed-text", - "CacheTtlMinutes": 60 -} -``` - -**What each field does:** - -* **`BaseUrl`** — where Ollama is listening (`ollama serve` defaults to port 11434) -* **`Model`** — the chat model to use; `llama3.2` is pulled in Step 1 -* **`EmbeddingModel`** — used in later articles (6.5+) for semantic search; `nomic-embed-text` is a compact, high-quality embedding model -* **`CacheTtlMinutes`** — how long AI responses are cached in-memory; identical questions within this window return instantly without hitting Ollama again (introduced in the `CachingAiChatService` below) - -**Key point:** `"AiEnabled": false` is the default. Developers who haven't installed Ollama can still clone and run the full stack — the AI endpoint simply returns 404. To activate AI features, change this to `true` and ensure Ollama is running. - -### Step 4: Define the Application Interface - -Create `TalentManagementAPI.Application/Interfaces/IAiChatService.cs`: - -```csharp -namespace TalentManagementAPI.Application.Interfaces -{ - public interface IAiChatService - { - Task ChatAsync(string message, string? systemPrompt = null, - CancellationToken cancellationToken = default); - } -} -``` - -**Why an interface?** The Application layer defines *what* the service does — not *how*. This follows the Dependency Inversion Principle: high-level modules (Application) don't depend on low-level details (Ollama SDK). Tests can inject a mock `IAiChatService` without needing Ollama running. - -### Step 5: Implement in Infrastructure.Shared - -Create `TalentManagementAPI.Infrastructure.Shared/Services/OllamaAiService.cs`: - -```csharp -#nullable enable -using Microsoft.Extensions.AI; -using TalentManagementAPI.Application.Interfaces; - -namespace TalentManagementAPI.Infrastructure.Shared.Services -{ - public class OllamaAiService : IAiChatService - { - private readonly IChatClient _chatClient; - - public OllamaAiService(IChatClient chatClient) - { - _chatClient = chatClient; - } - - public async Task ChatAsync(string message, string? systemPrompt = null, - CancellationToken cancellationToken = default) - { - var messages = new List(); - - if (!string.IsNullOrWhiteSpace(systemPrompt)) - messages.Add(new ChatMessage(Microsoft.Extensions.AI.ChatRole.System, systemPrompt)); - - messages.Add(new ChatMessage(Microsoft.Extensions.AI.ChatRole.User, message)); - - var response = await _chatClient.GetResponseAsync(messages, cancellationToken: cancellationToken); - return response.Text ?? string.Empty; - } - } -} -``` - -**What this does:** `OllamaAiService` takes `IChatClient` (the MEA standard interface) from DI — registered in Step 6 as `OllamaApiClient`. It builds a message list, optionally prepending a system prompt that lets callers control the AI's persona or constraints. `GetResponseAsync` is the MEA 10.x API for a single-turn chat completion. `response.Text` returns the assistant's reply as a plain string. - -**Why `Microsoft.Extensions.AI.ChatRole.System` instead of just `ChatRole.System`?** Both OllamaSharp and MEA define a `ChatRole` type. Fully qualifying the namespace resolves the ambiguity cleanly without needing a using alias. - -### Step 6: Register Services - -In `Infrastructure.Shared/ServiceRegistration.cs`, register `OllamaApiClient` as a concrete singleton and expose it as both `IChatClient` and `IOllamaApiClient` — so both interfaces resolve to the same instance: - -```csharp -using Microsoft.Extensions.AI; -using TalentManagementAPI.Application.Interfaces; -using TalentManagementAPI.Infrastructure.Shared.Services; - -public static void AddSharedInfrastructure(this IServiceCollection services, IConfiguration config) -{ - services.Configure(config.GetSection("MailSettings")); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - // OllamaApiClient implements both IChatClient (Microsoft.Extensions.AI) and IOllamaApiClient. - // Register as a singleton so both interfaces resolve to the same instance. - services.AddSingleton(_ => - { - var baseUrl = config["Ollama:BaseUrl"] ?? "http://localhost:11434"; - var model = config["Ollama:Model"] ?? "llama3.2"; - return new OllamaApiClient(new Uri(baseUrl), model); - }); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => sp.GetRequiredService()); - - // Metadata scoped per-request so the controller can read cache hit/miss - services.AddScoped(); - - // Wrap OllamaAiService with a caching decorator — identical questions within - // CacheTtlMinutes return instantly without hitting Ollama again - var ttlMinutes = config.GetValue("Ollama:CacheTtlMinutes", 60); - services.AddTransient(); - services.AddTransient(sp => new CachingAiChatService( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - TimeSpan.FromMinutes(ttlMinutes))); -} -``` - -In `WebApi/Program.cs`, the only AI-related line is the call to `AddSharedInfrastructure` — no extra registration needed: - -```csharp -builder.Services.AddApplicationLayer(); -builder.Services.AddPersistenceInfrastructure(builder.Configuration); -builder.Services.AddSharedInfrastructure(builder.Configuration); // ← registers IChatClient + IAiChatService -``` - -**Why three registrations for one object?** `OllamaApiClient` implements two interfaces from different libraries: - -* `IChatClient` (from `Microsoft.Extensions.AI`) — used by `OllamaAiService` for chat completions -* `IOllamaApiClient` (from OllamaSharp) — used by `OllamaEmbeddingService` for embedding generation (introduced in Series 6.7) - -Registering the concrete type first as a singleton, then aliasing both interfaces to it, ensures both resolve to the same underlying instance — one HTTP connection, one model selection, shared across the app. - -**To swap Ollama for Azure OpenAI in production:** Replace the three `AddSingleton` calls with your Azure OpenAI `IChatClient` registration. `OllamaAiService`, the Application layer, MediatR handlers, and the controller require zero changes. - -**What the caching decorator does:** `CachingAiChatService` wraps `OllamaAiService`. On the first call for a given `(message, systemPrompt)` pair, it calls Ollama and stores the reply. On subsequent identical calls within the TTL window, it returns the cached reply — skipping the 1–4 second Ollama inference. The `IAiResponseMetadata` flag tells the controller whether the response was a cache hit, which is surfaced as the `X-AI-Cache: HIT/MISS` response header. - -### Step 7: Create the AI Controller - -Create `TalentManagementAPI.WebApi/Controllers/v1/AiController.cs`: - -```csharp -using Asp.Versioning; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using TalentManagementAPI.Application.Interfaces; - -namespace TalentManagementAPI.WebApi.Controllers.v1 -{ - [ApiVersion("1.0")] - [AllowAnonymous] - [Route("api/v{version:apiVersion}/ai")] - public sealed class AiController : BaseApiController - { - private readonly IAiChatService _aiChatService; - private readonly IFeatureManagerSnapshot _featureManager; - private readonly IAiResponseMetadata _aiMetadata; - - public AiController( - IAiChatService aiChatService, - IFeatureManagerSnapshot featureManager, - IAiResponseMetadata aiMetadata) - { - _aiChatService = aiChatService; - _featureManager = featureManager; - _aiMetadata = aiMetadata; - } - - private void SetAiCacheHeader() - => Response.Headers["X-AI-Cache"] = _aiMetadata.WasCacheHit ? "HIT" : "MISS"; - - /// - /// Send a message to the AI assistant and receive a reply. - /// - [HttpPost("chat")] - public async Task Chat([FromBody] AiChatRequest request, - CancellationToken cancellationToken) - { - if (!await _featureManager.IsEnabledAsync("AiEnabled")) - { - return Problem( - detail: "AI chat is disabled. Enable FeatureManagement:AiEnabled to use this endpoint.", - title: "AI chat is disabled", - statusCode: StatusCodes.Status503ServiceUnavailable); - } - - var reply = await _aiChatService.ChatAsync( - request.Message, request.SystemPrompt, cancellationToken); - SetAiCacheHeader(); - return Ok(new AiChatResponse(reply)); - } - } - - public record AiChatRequest(string Message, string? SystemPrompt = null); - public record AiChatResponse(string Reply); -} -``` - -**Why per-method checks instead of `[FeatureGate]` on the class?** - -The `[FeatureGate("AiEnabled")]` attribute returns `404 Not Found` when the feature is disabled — a misleading status for a known endpoint. The per-method check returns `503 Service Unavailable` with a clear `detail` message explaining exactly what to enable and where. This is far more helpful to developers hitting the endpoint for the first time. - -**`IFeatureManagerSnapshot`** — the snapshot variant reads the feature flags once per request and caches the result for the request lifetime. This avoids multiple config reads per action. - -**`IAiResponseMetadata`** — a scoped flag (set by `CachingAiChatService` in Step 6) that records whether the response came from the cache. `SetAiCacheHeader()` surfaces this as `X-AI-Cache: HIT` or `MISS` in every response — visible in the browser Network tab and Swagger, making it easy to see when caching is working. - -When `AiEnabled` is `true`, the endpoint is fully active. No other code changes needed. - ---- - -## 💻 Try It Yourself - -**Enable AI features** by setting `"AiEnabled": true` in `appsettings.json` and starting Ollama: - -```bash -# Terminal 1: Start Ollama (if not already running) -ollama serve - -# Terminal 2: Start the .NET API (from the ApiResources submodule) -cd ApiResources/TalentManagement-API -dotnet run -``` - -Open Swagger at `https://localhost:44378/swagger` and confirm the API is running — you'll see all controller groups listed. - -![NSwag Swagger UI for the TalentManagement .NET 10 Web API — all versioned controller groups collapsed](../../docs/screenshots/series-6-ai-app-features/swagger-ui-overview.png) - -Scroll down to find the **Ai** section and expand it. - -![Swagger UI AI controller expanded — POST /api/v1/ai/chat, POST /api/v1/ai/hr-insight, POST /api/v1/ai/nl-employee-search](../../docs/screenshots/series-6-ai-app-features/swagger-ai-endpoints.png) - -Click **POST /api/v1/ai/chat**, then **Try it out**, and send: - -![Swagger UI POST /api/v1/ai/chat endpoint — request body schema with message and systemPrompt fields](../../docs/screenshots/series-6-ai-app-features/swagger-ai-chat-endpoint.png) - -Expand **POST /api/v1/ai/chat**, click **Try it out**, and send: - -```json -{ - "message": "What is the difference between OAuth 2.0 and OIDC?", - "systemPrompt": "You are a helpful assistant specializing in identity and security." -} -``` - -You'll see Ollama's reply in the response body within a few seconds. - -**To test without Swagger** — use curl: - -```bash -curl -X POST https://localhost:44378/api/v1/ai/chat \ - -H "Content-Type: application/json" \ - -k \ - -d '{"message": "Explain JWT tokens in one paragraph."}' -``` - -**To verify the feature flag** — set `"AiEnabled": false`, restart the API, and try the same curl. You'll get a `503 Service Unavailable` with a `detail` message explaining exactly what to enable. - ---- - -## 📊 Real-World Impact - -**Before this approach:** - -* ❌ AI code is tightly coupled to OpenAI SDK — migrating to another provider requires rewriting service code -* ❌ Tutorial readers need an API key and billing account just to run the demo -* ❌ Enabling AI in `develop` branch breaks builds for developers without Ollama - -**After this approach:** - -* ✅ Swap Ollama for Azure OpenAI in production by writing a new `IAiChatService` implementation and updating one DI registration — Application layer and controller unchanged -* ✅ Zero-cost, zero-signup AI during development — every tutorial reader can follow along -* ✅ Feature flag default `false` means the full Series 0–5 stack runs unchanged — AI is opt-in - ---- - -## 🌟 Why This Matters - -**Microsoft.Extensions.AI ships GA with .NET 10** — it is not a preview library. `IChatClient` is the platform-standard interface for chat completions, with first-party support from Microsoft and implementations across OpenAI, Azure OpenAI, Ollama (via OllamaSharp), and more. Building against `IChatClient` means your service layer is future-proof: new providers ship as NuGet packages, and switching is a one-line DI change. - -**OllamaSharp 5.x natively implements `IChatClient`.** The former `Microsoft.Extensions.AI.Ollama` adapter package has been deprecated in favor of OllamaSharp directly. That means one fewer dependency and no abstraction-over-an-abstraction: `OllamaApiClient` *is* the `IChatClient` — no adapter wrapper needed. - -The custom `IAiChatService` interface adds a second layer of provider independence specific to this application's contract (`ChatAsync` with an optional system prompt). Application-layer code (handlers, queries) and the controller depend only on `IAiChatService` — they are completely unaware of MEA or OllamaSharp. When you are ready to move to a cloud provider, you register a different `IChatClient` (one line in ServiceRegistration.cs) and the rest of the codebase is untouched. - -For tutorial purposes, Ollama removes the biggest barrier to learning: access. Every developer on every OS can pull `llama3.2`, type `ollama serve`, and have a working LLM in their local environment. No billing, no configuration, no waiting for API access. - -The feature flag pattern ensures this is safe to ship: the codebase always builds, always runs, and the original Series 0–5 experience is completely unchanged. AI features activate on demand. - -**Transferable skills:** - -* **Microsoft.Extensions.AI** — `IChatClient` is the standard .NET 10 AI interface; the same pattern applies to OpenAI, Azure OpenAI, and any future MEA provider -* **Custom service interface for AI** — The `IAiChatService` pattern applies to any AI provider; define the contract in Application, implement in Infrastructure -* **Per-method feature flag checks** — `IsEnabledAsync` returning `503 Service Unavailable` is more developer-friendly than the `404` from `[FeatureGate]` on a class -* **Clean Architecture for external services** — Interface in Application, implementation in Infrastructure, DI registration in WebApi - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - -**Found this helpful?** Share it with your team and follow for more full-stack development content! - ---- - -## 📖 Series Navigation - -**AngularNetTutorial Blog Series:** - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial -* [Stop Juggling Multiple Repos: Manage Your Full-Stack App Like a Workspace](../series-0-architecture/0.1-git-submodule-workspace.md) — Git Submodules -* [End-to-End Testing Made Simple: How Playwright Transforms Testing](../series-0-architecture/0.2-playwright-testing.md) — Playwright Overview -* [Speed Up Your Dashboard: Easy Response Caching in .NET 10 With EasyCaching](../series-2-dotnet-api/2.5-dotnet-easycaching.md) — Response Caching (Series 2.5) -* **This Article** — Run a Local LLM in Your .NET 10 API with Ollama (Series 6.1) -* [Build an HR AI Assistant That Knows Your Data](6.2-dotnet-ai-hr-assistant.md) — HR AI Assistant (Series 6.2) -* [Add an AI Chat Widget to Angular with Streaming](6.3-angular-ai-chat-widget.md) — Angular Chat Widget (Series 6.3) - ---- - -**📌 Tags:** #dotnet #csharp #ai #ollama #llm #microsoftextensionsai #cleanarchitecture #aspnetcore #webapi #featureflags #fullstack #angular #oauth2 #locallm #generativeai diff --git a/blogs/series-6-ai-app-features/6.2-dotnet-ai-hr-assistant.md b/blogs/series-6-ai-app-features/6.2-dotnet-ai-hr-assistant.md deleted file mode 100644 index 1da7b57..0000000 --- a/blogs/series-6-ai-app-features/6.2-dotnet-ai-hr-assistant.md +++ /dev/null @@ -1,415 +0,0 @@ -# Build an HR AI Assistant That Knows Your Data - -## How to Ground an LLM in Real Workforce Metrics Using MediatR and Clean Architecture - -Generic chatbots answer generic questions. What makes an AI assistant useful in a business application is *context* — the ability to answer questions like "Which department has the most headcount?" or "How many new hires joined this month?" by looking at real, live data instead of guessing. - -This article shows you how to build a data-aware HR assistant that pulls live workforce metrics from the database, injects them into the LLM prompt, and returns answers grounded in your actual data — not hallucinated statistics. - -![HR Insight page showing a data-grounded Ollama answer — references live department headcounts, execution time shown below reply](../../docs/screenshots/series-6-ai-app-features/ai-hr-insight-with-answer.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article builds on [Article 6.1](6.1-dotnet-ai-foundation.md), which established the `IAiChatService` interface, Ollama integration, and feature flag setup. Complete that article before continuing.** - ---- - -## 🎓 What You'll Learn - -* **Retrieval-Augmented Generation (RAG) without a vector store** — How to inject structured data directly into a system prompt instead of using embeddings -* **MediatR query for AI** — Creating `GetHrInsightQuery` that fetches live data and builds a context-aware prompt -* **Prompt engineering with real data** — How to format workforce metrics so the LLM answers accurately -* **Clean Architecture placement** — Where the AI query handler belongs in the layer structure -* **`IDashboardMetricsReader` reuse** — How to leverage existing infrastructure without duplicating data access code - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 6.1 complete** — `IAiChatService`, `OllamaAiService`, `AiController`, and `[FeatureGate("AiEnabled")]` all in place -* **Ollama running** — `ollama serve` at `http://localhost:11434` with `llama3.2` pulled -* **`AiEnabled: true`** in `appsettings.json` for local testing -* **TalentManagement data seeded** — Employees, departments, and positions in the database (see Series 5.1) - ---- - -## 🎯 The Problem - -The `POST /api/v1/ai/chat` endpoint from Article 6.1 accepts any message and returns a general-purpose LLM reply. Ask it "How many employees are in the Engineering department?" and it will confidently make up a number — because it has no idea what your data looks like. - -This is the classic **hallucination problem**: LLMs generate plausible-sounding text based on training data, not your database. For a business application, invented statistics are worse than no answer. - -**Two common approaches to grounding an LLM in real data:** - -* **Vector embeddings + semantic search (RAG)** — Embed your data into a vector store, retrieve relevant chunks at query time, inject them into the prompt. Powerful, but complex to set up and requires an embedding model. -* **Direct context injection** — Fetch the data you need with a regular database query, format it as text, and include it in the system prompt. Simpler, works for structured/aggregate data. - -For HR dashboard metrics — totals, distributions, recent hires — direct context injection is the right choice. The data is small (a few dozen lines of text), always current, and exactly what the LLM needs. - -![Dashboard showing KPI metric cards — total employees, departments, new hires, and Chart.js charts — the same data injected into the LLM system prompt](../../docs/screenshots/series-6-ai-app-features/dashboard-metrics-charts.png) - ---- - -## 💡 The Solution - -`GetHrInsightQuery` is a MediatR query handler that: - -1. Calls `IDashboardMetricsReader.GetDashboardMetricsAsync()` to fetch live workforce data -2. Formats that data into a structured system prompt (the "context window") -3. Calls `IAiChatService.ChatAsync()` with the user's question and the context prompt -4. Returns a `Result` with the answer, original question, and execution time - -The LLM receives a system prompt like this: - -``` -You are an HR data analyst assistant. Answer questions using only the workforce data provided below. - -=== CURRENT WORKFORCE DATA === -Total Employees: 47 -Total Departments: 6 -Total Positions: 12 -New Hires This Month: 3 -Average Salary: $72,400.00 -Gender Distribution: 29 male, 18 female - -Employees by Department: - - Engineering: 14 - - Marketing: 9 - - Operations: 8 - ... -``` - -When the user asks "Which department has the most employees?", the LLM reads the injected data and answers correctly: "Engineering, with 14 employees." No hallucination — the answer is in the context. - ---- - -## 🚀 How It Works - -### Step 1: Create the DTO - -Create `TalentManagementAPI.Application/Features/AI/Queries/GetHrInsight/HrInsightDto.cs`: - -```csharp -#nullable enable -namespace TalentManagementAPI.Application.Features.AI.Queries.GetHrInsight -{ - public sealed class HrInsightDto - { - public string Question { get; init; } = string.Empty; - public string Answer { get; init; } = string.Empty; - public long ExecutionTimeMs { get; init; } - } -} -``` - -**Why include `Question` in the response?** The caller gets back the original question alongside the answer — useful for displaying both in the UI (Article 6.4) and for logging/auditing AI interactions. - -### Step 2: Create the Query Handler - -Create `TalentManagementAPI.Application/Features/AI/Queries/GetHrInsight/GetHrInsightQuery.cs`: - -```csharp -#nullable enable -using System.Diagnostics; -using System.Text; -using TalentManagementAPI.Application.Features.Dashboard.Queries.GetDashboardMetrics; -using TalentManagementAPI.Application.Interfaces; - -namespace TalentManagementAPI.Application.Features.AI.Queries.GetHrInsight -{ - public sealed class GetHrInsightQuery : IRequest> - { - public string Question { get; init; } = string.Empty; - - public sealed class GetHrInsightQueryHandler - : IRequestHandler> - { - private readonly IDashboardMetricsReader _metricsReader; - private readonly IAiChatService _aiChatService; - - public GetHrInsightQueryHandler( - IDashboardMetricsReader metricsReader, - IAiChatService aiChatService) - { - _metricsReader = metricsReader; - _aiChatService = aiChatService; - } - - public async Task> Handle( - GetHrInsightQuery request, - CancellationToken cancellationToken) - { - var start = Stopwatch.GetTimestamp(); - - var metrics = await _metricsReader - .GetDashboardMetricsAsync(cancellationToken) - .ConfigureAwait(false); - - var systemPrompt = BuildSystemPrompt(metrics); - - var answer = await _aiChatService - .ChatAsync(request.Question, systemPrompt, cancellationToken) - .ConfigureAwait(false); - - var elapsed = (long)Stopwatch.GetElapsedTime(start).TotalMilliseconds; - - return Result.Success(new HrInsightDto - { - Question = request.Question, - Answer = answer, - ExecutionTimeMs = elapsed - }); - } - - private static string BuildSystemPrompt(DashboardMetricsDto metrics) - { - var sb = new StringBuilder(); - sb.AppendLine("You are an HR data analyst assistant. Answer questions using only the workforce data provided below. Be concise and factual."); - sb.AppendLine(); - sb.AppendLine("=== CURRENT WORKFORCE DATA ==="); - sb.AppendLine($"Total Employees: {metrics.TotalEmployees}"); - sb.AppendLine($"Total Departments: {metrics.TotalDepartments}"); - sb.AppendLine($"Total Positions: {metrics.TotalPositions}"); - sb.AppendLine($"Total Salary Ranges: {metrics.TotalSalaryRanges}"); - sb.AppendLine($"New Hires This Month: {metrics.NewHiresThisMonth}"); - sb.AppendLine($"Average Salary: {metrics.AverageSalary:C}"); - sb.AppendLine($"Gender Distribution: {metrics.GenderDistribution.Male} male, {metrics.GenderDistribution.Female} female"); - sb.AppendLine(); - - if (metrics.EmployeesByDepartment.Count > 0) - { - sb.AppendLine("Employees by Department:"); - foreach (var d in metrics.EmployeesByDepartment) - sb.AppendLine($" - {d.DepartmentName}: {d.EmployeeCount}"); - sb.AppendLine(); - } - - if (metrics.EmployeesByPosition.Count > 0) - { - sb.AppendLine("Employees by Position:"); - foreach (var p in metrics.EmployeesByPosition) - sb.AppendLine($" - {p.PositionTitle}: {p.EmployeeCount}"); - sb.AppendLine(); - } - - if (metrics.RecentEmployees.Count > 0) - { - sb.AppendLine("Recent Hires (last 5):"); - foreach (var e in metrics.RecentEmployees) - sb.AppendLine($" - {e.FullName} ({e.PositionTitle}, {e.DepartmentName}) — hired {e.CreatedAt:yyyy-MM-dd}"); - } - - return sb.ToString(); - } - } - } -} -``` - -**Key design decisions:** - -* **`IDashboardMetricsReader` — not `IMediator`** — The handler calls the reader directly instead of dispatching another `GetDashboardMetricsQuery`. This avoids circular MediatR dispatch and keeps the data fetch simple and fast. -* **`StringBuilder` for the prompt** — String concatenation for many lines is inefficient. `StringBuilder` allocates once and appends, which matters when this method runs on every request. -* **`ConfigureAwait(false)`** — Both async calls use `ConfigureAwait(false)` to avoid deadlocks in ASP.NET Core's synchronization context. -* **`Stopwatch.GetTimestamp()` / `GetElapsedTime()`** — High-resolution timer, not `DateTime.UtcNow`. Used to measure total wall-clock time including both the database fetch and the LLM call. - -### Step 3: Add the Controller Endpoint - -In `TalentManagementAPI.WebApi/Controllers/v1/AiController.cs`, add the `hr-insight` action. This builds on the controller created in Article 6.1 — `_featureManager` (`IFeatureManagerSnapshot`) and `_aiMetadata` (`IAiResponseMetadata`) are already injected in the constructor alongside `IAiChatService`. The `hr-insight` action follows the exact same per-method feature flag pattern as `chat`. - -```csharp -using TalentManagementAPI.Application.Features.AI.Queries.GetHrInsight; - -// Inside AiController (add after the Chat action): - -/// -/// Ask the HR AI assistant a question about your current workforce data. -/// The assistant fetches live dashboard metrics and injects them into the prompt context. -/// -[HttpPost("hr-insight")] -public async Task HrInsight( - [FromBody] HrInsightRequest request, - CancellationToken cancellationToken) -{ - if (!await _featureManager.IsEnabledAsync("AiEnabled")) - { - return Problem( - detail: "AI features are disabled. Enable FeatureManagement:AiEnabled to use this endpoint.", - title: "AI is disabled", - statusCode: StatusCodes.Status503ServiceUnavailable); - } - - var result = await Mediator.Send( - new GetHrInsightQuery { Question = request.Question }, - cancellationToken); - - SetAiCacheHeader(); - return Ok(result); -} - -// At the bottom of the file, alongside AiChatRequest: -public record HrInsightRequest(string Question); -``` - -**Why use `Mediator.Send()` here but not in the handler?** The controller dispatches to MediatR (the pipeline entry point), which handles validation behaviors, logging, and other cross-cutting concerns. The handler calls the reader directly because it's already inside the pipeline — dispatching again would add unnecessary overhead. - -**No new DI registration needed.** MediatR scans the Application assembly and registers all `IRequestHandler<,>` implementations automatically (see `ServiceExtensions.cs`). `GetHrInsightQueryHandler` is picked up without any extra configuration. - ---- - -## 💻 Try It Yourself - -**Enable AI features** and start your stack: - -```bash -# Terminal 1: Ollama -ollama serve - -# Terminal 2: .NET API (with AiEnabled: true in appsettings.json) -cd ApiResources/TalentManagement-API/TalentManagementAPI.WebApi -dotnet run -``` - -Open Swagger at `https://localhost:44378/swagger` and find **POST /api/v1/ai/hr-insight**. - -![Swagger UI AI controller expanded — POST /ai/chat, POST /ai/hr-insight, POST /ai/nl-employee-search](../../docs/screenshots/series-6-ai-app-features/swagger-ai-endpoints.png) - -Try these questions: - -```json -{ "question": "Which department has the most employees?" } -``` - -```json -{ "question": "How many new hires joined this month?" } -``` - -```json -{ "question": "What is the gender distribution across the company?" } -``` - -```json -{ "question": "Who are the most recent hires and what positions do they hold?" } -``` - -**Expected response shape:** - -```json -{ - "succeeded": true, - "data": { - "question": "Which department has the most employees?", - "answer": "Engineering has the most employees with 14 staff members.", - "executionTimeMs": 2341 - } -} -``` - -![HR Insight page showing a data-grounded Ollama answer — references live department headcounts, execution time shown below reply](../../docs/screenshots/series-6-ai-app-features/ai-hr-insight-with-answer.png) - -**To test the feature flag** — set `"AiEnabled": false` in `appsettings.json` and restart the API. Both `/ai/chat` and `/ai/hr-insight` return `503 Service Unavailable`. No other endpoints are affected. - ---- - -## 📊 What Changes Between Requests - -Every call to `POST /api/v1/ai/hr-insight`: - -1. **Fetches fresh metrics** from the database via `IDashboardMetricsReader` -2. **Builds a new system prompt** with the current data -3. **Sends to Ollama** — the LLM sees your live workforce numbers - -If you add 10 new employees and ask "How many employees do we have?", the answer reflects the current count — not a cached snapshot from startup. - -**Performance note:** Each request makes two I/O calls — one database query and one Ollama inference. The database query is fast (milliseconds). Ollama inference with `llama3.2` typically takes 1–4 seconds on a modern laptop. Article 6.8 will add caching for repeated identical questions. - ---- - -## 📐 Where This Fits in Clean Architecture - -``` -Application/ -└── Features/ - └── AI/ - └── Queries/ - └── GetHrInsight/ - ├── GetHrInsightQuery.cs ← Handler lives here - └── HrInsightDto.cs ← Response shape - -WebApi/ -└── Controllers/v1/ - └── AiController.cs ← New hr-insight endpoint -``` - -**Why `Features/AI/` and not `Features/Dashboard/`?** The query belongs to the AI feature, not the dashboard feature. It *uses* dashboard data, but its responsibility is answering HR questions — a distinct concern. Mixing it into the dashboard feature would blur the boundary and make the code harder to navigate as AI features grow. - -**Dependencies flow inward:** - -* `AiController` → `GetHrInsightQuery` (Application) -* `GetHrInsightQueryHandler` → `IDashboardMetricsReader` (Application interface) -* `GetHrInsightQueryHandler` → `IAiChatService` (Application interface) -* `DashboardMetricsReader` implements `IDashboardMetricsReader` (Infrastructure.Persistence) -* `OllamaAiService` implements `IAiChatService` (Infrastructure.Shared) - -The handler never references `OllamaAiService` or `DashboardMetricsReader` directly — only the interfaces. This is the Dependency Inversion Principle in action: swap the database or the LLM provider without touching the query handler. - ---- - -## 📊 Real-World Impact - -**Before this approach:** - -* ❌ LLM answers HR questions with invented statistics — no connection to real data -* ❌ Analysts must export data, format it manually, and paste into ChatGPT -* ❌ Every AI question requires a separate data-fetch step outside the application - -**After this approach:** - -* ✅ LLM answers grounded in live workforce data — no hallucination for covered metrics -* ✅ Single endpoint: ask a question, get a data-backed answer -* ✅ No new infrastructure — reuses `IDashboardMetricsReader` already in production - ---- - -## 🌟 Why This Matters - -This pattern — "fetch data, inject into prompt, ask LLM" — is the foundation of practical AI in business applications. Vector databases and embeddings get more attention, but for structured aggregate data (counts, averages, distributions), direct context injection is simpler, faster to implement, and easier to debug. - -The LLM becomes a natural language interface to your existing data. HR managers who can't write SQL can ask plain-English questions and get accurate answers from a system that knows your actual workforce. - -**Transferable skills:** - -* **Prompt construction patterns** — Format any structured data into LLM context using the same `StringBuilder` technique -* **MediatR + AI** — The `GetHrInsightQuery` pattern extends to any domain: sales metrics, inventory, customer data -* **Grounding vs. hallucination** — Understanding *when* to inject context vs. when to let the LLM use general knowledge - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - ---- - -## 📖 Series Navigation - -**AngularNetTutorial Blog Series:** - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial -* [Run a Local LLM in Your .NET 10 API with Ollama](6.1-dotnet-ai-foundation.md) — AI Foundation (Series 6.1) -* **This Article** — Build an HR AI Assistant That Knows Your Data (Series 6.2) -* [Add an AI Chat Widget to Angular with Streaming](6.3-angular-ai-chat-widget.md) — Angular Chat Widget (Series 6.3) -* [AI-Generated Dashboard Insights in Angular Material](6.4-angular-ai-dashboard-insights.md) — Dashboard Insights (Series 6.4) - ---- - -**📌 Tags:** #dotnet #csharp #ai #ollama #llm #rag #promptengineering #cleanarchitecture #mediatr #aspnetcore #webapi #hrtech #fullstack #angular #generativeai #locallm diff --git a/blogs/series-6-ai-app-features/6.3-angular-ai-chat-widget.md b/blogs/series-6-ai-app-features/6.3-angular-ai-chat-widget.md deleted file mode 100644 index 3c3a0be..0000000 --- a/blogs/series-6-ai-app-features/6.3-angular-ai-chat-widget.md +++ /dev/null @@ -1,501 +0,0 @@ -# Build a Dedicated AI Section in Angular with Submenu Navigation - -## How to Build Four Focused Standalone Pages with Sidebar Sub-Menu Navigation - -AI features deserve their own section. This article adds a proper **AI submenu** to the TalentManagement Angular app — four dedicated pages, each with a single responsibility, registered as children of an `ai` route group in the sidebar. Each page is a standalone Angular component with its own route, template, and styles. - -![Dashboard with the AI submenu expanded in the sidebar — four child items: AI Assistant, HR Insight, NL Search, Vector Search](../../docs/screenshots/series-6-ai-app-features/ai-submenu-sidebar.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article builds on Article 6.1 (AI Foundation) and Article 6.2 (HR AI Assistant). Both API endpoints must be in place before continuing.** - ---- - -## 🎓 What You'll Learn - -* **Angular nested child routes** — How to register a `{ path: 'ai', children: [...] }` block so four pages share the same `/ai/*` URL prefix -* **Sidebar sub-menu in ng-matero** — Adding a `"type": "sub"` entry with children to `menu.json` and translation keys in `en-US.json` -* **One component, one responsibility** — Why extracting each AI feature into its own standalone component is better than growing a single tabbed component -* **`AiService`** — A lightweight, focused service that calls the AI endpoints without inheriting the base CRUD service - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 6.1 complete** — `POST /api/v1/ai/chat` endpoint working -* **Article 6.2 complete** — `POST /api/v1/ai/hr-insight` endpoint working -* **Ollama running** — `ollama serve` at `http://localhost:11434` with `llama3.2` pulled -* **`AiEnabled: true`** in the API's `appsettings.json` for local testing -* **Angular app running** — `npm start` in `Clients/TalentManagement-Angular-Material/talent-management/` - ---- - -## 🎯 What We're Building - -A new **AI** submenu in the sidebar, replacing a single flat `AI Assistant` link, with four child pages: - -* **AI Assistant** (`/ai/assistant`) — General-purpose chat with the LLM -* **HR Insight** (`/ai/hr-insight`) — Data-aware chat grounded in live workforce metrics -* **NL Search** (`/ai/nl-search`) — Natural language employee search powered by LLM query parsing -* **Vector Search** (`/ai/vector-search`) — Semantic position search using vector embeddings - -Each page is its own Angular standalone component with its own template and styles. No tabs, no shared state — clean separation. - ---- - -## 🚀 Implementation - -### Step 1: Add `aiEnabled` to the Environment - -Add the feature flag to both environment files. - -**`src/environments/environment.ts`:** - -```typescript -export const environment = { - production: false, - apiUrl: 'https://localhost:44378/api/v1', - identityServerUrl: 'https://localhost:44310', - clientId: 'TalentManagement', - scope: 'openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write', - - // Feature Flags - allowAnonymousAccess: true, - aiEnabled: false, // ← set to true to activate AI features -}; -``` - -**`src/environments/environment.prod.ts`:** - -Add the same `aiEnabled: false` line. Production defaults to off. - -> **Note for repo cloners:** `environment.ts` in the repo may already have `aiEnabled: true` (set during development). To see the disabled banner first, flip it to `false`, then back to `true` when testing the chat UI. - ---- - -### Step 2: Create the AI Service - -The AI endpoints don't fit the CRUD pattern in `BaseApiService`. Create a focused service. - -**`src/app/services/api/ai.service.ts`:** - -```typescript -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; - -interface ApiResult { - isSuccess: boolean; - value: T; - errors: string[]; -} - -export interface AiChatResponse { - reply: string; -} - -export interface HrInsightResponse { - question: string; - answer: string; - executionTimeMs: number; -} - -export interface NlEmployeeFilter { - originalQuery: string; - firstName: string; - lastName: string; - email: string; - employeeNumber: string; - positionTitle: string; - parsedExpression: string; - executionTimeMs: number; -} - -export interface SemanticPositionResult { - id: string; - positionNumber: string; - positionTitle: string; - positionDescription: string; - departmentName: string; - salaryRangeName: string; - score: number; - executionTimeMs: number; -} - -@Injectable({ providedIn: 'root' }) -export class AiService { - private http = inject(HttpClient); - private apiUrl = environment.apiUrl; - - chat(message: string, systemPrompt?: string): Observable { - return this.http.post(`${this.apiUrl}/ai/chat`, { message, systemPrompt }); - } - - hrInsight(question: string): Observable { - return this.http.post>(`${this.apiUrl}/ai/hr-insight`, { - question, - }).pipe(map(r => r.value)); - } - - nlEmployeeSearch(query: string): Observable { - return this.http.post(`${this.apiUrl}/ai/nl-employee-search`, { query }); - } - - semanticPositionSearch(queryText: string, topK = 10): Observable { - return this.http.post(`${this.apiUrl}/positions/semantic-search`, { - queryText, topK, - }); - } -} -``` - -**Key design decisions:** - -* **Does not extend `BaseApiService`** — The AI endpoints are not CRUD endpoints. A focused service with four methods is self-documenting. -* **Four methods, four endpoints** — `chat`, `hrInsight`, `nlEmployeeSearch`, `semanticPositionSearch`. Each article in this series introduces one method; the full service is shown here for reference. -* **`hrInsight` unwraps `ApiResult`** — The `POST /ai/hr-insight` endpoint returns the standard `Result` envelope (`{ isSuccess, value, errors }`) used by all MediatR-dispatched endpoints. The private `ApiResult` interface models this shape, and `.pipe(map(r => r.value))` unwraps it so components receive a plain `HrInsightResponse`. The `chat` and `nlEmployeeSearch` endpoints return their payloads directly (no envelope), so no unwrapping is needed for those. -* **`inject()` not constructor injection** — Consistent with every other service in this codebase. - -**Add to the barrel export** in `src/app/services/api/index.ts`: - -```typescript -export * from './ai.service'; -``` - ---- - -### Step 3: Create the Four AI Components - -Create a folder `src/app/routes/ai/` with four subfolders — one per page. Each folder contains three files: `.ts`, `.html`, `.scss`. - -**Folder structure:** - -``` -src/app/routes/ai/ -├── ai-assistant/ -│ ├── ai-assistant.component.ts -│ ├── ai-assistant.component.html -│ └── ai-assistant.component.scss -├── ai-hr-insight/ -│ ├── ai-hr-insight.component.ts -│ ├── ai-hr-insight.component.html -│ └── ai-hr-insight.component.scss -├── ai-nl-search/ -│ ├── ai-nl-search.component.ts -│ ├── ai-nl-search.component.html -│ └── ai-nl-search.component.scss -└── ai-vector-search/ - ├── ai-vector-search.component.ts - ├── ai-vector-search.component.html - └── ai-vector-search.component.scss -``` - ---- - -#### AI Assistant Component - -This page provides general-purpose chat with the LLM at a dedicated route. - -**`ai-assistant.component.ts`:** - -```typescript -import { Component, OnDestroy, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { MatButtonModule } from '@angular/material/button'; -import { MatInputModule } from '@angular/material/input'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatDividerModule } from '@angular/material/divider'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; -import { PageHeader } from '@shared'; -import { AiService } from '../../../services/api/ai.service'; -import { environment } from '../../../../environments/environment'; - -export interface ChatMessage { - role: 'user' | 'assistant'; - content: string; -} - -@Component({ - selector: 'app-ai-assistant', - standalone: true, - templateUrl: './ai-assistant.component.html', - styleUrl: './ai-assistant.component.scss', - imports: [ - CommonModule, FormsModule, MatCardModule, MatIconModule, - MatButtonModule, MatInputModule, MatFormFieldModule, - MatProgressSpinnerModule, MatDividerModule, PageHeader, - ], -}) -export class AiAssistantComponent implements OnDestroy { - private aiService = inject(AiService); - private destroy$ = new Subject(); - - aiEnabled = environment.aiEnabled; - messages: ChatMessage[] = []; - input = ''; - loading = false; - error = ''; - - send(): void { - const message = this.input.trim(); - if (!message || this.loading) return; - - this.messages.push({ role: 'user', content: message }); - this.input = ''; - this.loading = true; - this.error = ''; - - this.aiService.chat(message) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: response => { - this.messages.push({ role: 'assistant', content: response.reply }); - this.loading = false; - }, - error: err => { - this.error = err?.error?.detail ?? 'Failed to get a response. Is the API running with AiEnabled: true?'; - this.loading = false; - }, - }); - } - - onKeydown(event: KeyboardEvent): void { - if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.send(); } - } - - clear(): void { this.messages = []; this.error = ''; } - - ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } -} -``` - -**Key design decisions:** - -* **Single responsibility** — This component handles only the general chat flow. HR Insight, NL Search, and Vector Search each live in their own component. -* **`private destroy$ = new Subject()`** — The `takeUntil` pattern ensures in-flight Ollama requests are cancelled cleanly when the user navigates away. -* **State is scoped to this component** — Navigating to HR Insight and back does not clear the chat history here. - ---- - -#### HR Insight Component - -Provides data-aware chat grounded in live workforce metrics. Full implementation follows the same pattern as AI Assistant — replace `chat()` with `hrInsight()` and add the suggestion chips. - -The `ai-hr-insight.component.ts` structure is identical to AI Assistant, differing only in: -* Calls `aiService.hrInsight(question)` instead of `aiService.chat(message)` -* Stores `executionTimeMs` on each assistant message -* Shows suggestion chip buttons when the conversation is empty - -See the repository for the full file: `src/app/routes/ai/ai-hr-insight/ai-hr-insight.component.ts` - ---- - -#### NL Search and Vector Search - -**NL Search** (`ai-nl-search`) and **Vector Search** (`ai-vector-search`) are covered in Articles 6.4 and 6.5 respectively. Their component scaffolding is created here, but the backend endpoints they call are documented in those articles. - -See the repository for the full files in: -* `src/app/routes/ai/ai-nl-search/` -* `src/app/routes/ai/ai-vector-search/` - ---- - -### Step 4: Register the Routes - -The key change from the old approach is **nesting** the four AI routes under a parent `ai` path. - -**`src/app/app.routes.ts`** — add the nested `ai` route group: - -```typescript -import { AiAssistantComponent } from './routes/ai/ai-assistant/ai-assistant.component'; -import { AiHrInsightComponent } from './routes/ai/ai-hr-insight/ai-hr-insight.component'; -import { AiNlSearchComponent } from './routes/ai/ai-nl-search/ai-nl-search.component'; -import { AiVectorSearchComponent } from './routes/ai/ai-vector-search/ai-vector-search.component'; - -// Inside the AdminLayout children array: -{ - path: 'ai', - children: [ - { path: 'assistant', component: AiAssistantComponent }, - { path: 'hr-insight', component: AiHrInsightComponent }, - { path: 'nl-search', component: AiNlSearchComponent }, - { path: 'vector-search', component: AiVectorSearchComponent }, - { path: '', redirectTo: 'assistant', pathMatch: 'full' }, - ], -}, -``` - -**Why a `children` block instead of flat routes?** - -Flat routes like `{ path: 'ai/assistant', component: ... }` would work for navigation but the URL structure would not be understood by the ng-matero sidebar. The sidebar resolves `route: "assistant"` relative to the parent `route: "ai"` — the routes file must mirror this nesting for the active-route highlighting to work correctly. - ---- - -### Step 5: Add the Sidebar Sub-Menu - -**`public/data/menu.json`** — add the AI sub-menu group: - -```json -{ - "route": "ai", - "name": "ai", - "type": "sub", - "icon": "smart_toy", - "children": [ - { "route": "assistant", "name": "ai.aiAssistant", "type": "link" }, - { "route": "hr-insight", "name": "ai.aiHrInsight", "type": "link" }, - { "route": "nl-search", "name": "ai.aiNlSearch", "type": "link" }, - { "route": "vector-search", "name": "ai.aiVectorSearch","type": "link" } - ] -} -``` - -**`public/i18n/en-US.json`** — replace the old `"aiChat"` key: - -```json -"ai": "AI", -"ai.aiAssistant": "AI Assistant", -"ai.aiHrInsight": "HR Insight", -"ai.aiNlSearch": "NL Search", -"ai.aiVectorSearch":"Vector Search" -``` - -**Key design decisions:** - -* **`"type": "sub"` with a children array** — This is the ng-matero convention for a collapsible group. The parent entry (`"ai"`) shows the icon and label in the sidebar; the children appear when expanded. -* **Translation keys use dot notation** — `"ai.aiAssistant"` follows the convention already used by `"employees.employeeList"`, `"departments.departmentList"`, etc. No library changes needed — the ng-matero `TranslateService` resolves the dot as a nested key automatically. -* **No `"permissions"` on the parent** — All users can see the AI submenu. Individual pages check `environment.aiEnabled` themselves and show a banner if AI is disabled. - ---- - -## 💻 Try It Yourself - -**Enable AI features and start the stack:** - -```bash -# Terminal 1: Ollama -ollama serve - -# Terminal 2: .NET API (with AiEnabled: true in appsettings.json) -cd ApiResources/TalentManagement-API/TalentManagementAPI.WebApi -dotnet run - -# Terminal 3: Angular -cd Clients/TalentManagement-Angular-Material/talent-management -npm start -``` - -In `src/environments/environment.ts`, set `aiEnabled: true`. - -Open `http://localhost:4200` → log in → expand **AI** in the sidebar. - -**Test AI Assistant (`/ai/assistant`):** - -![Full AI Assistant page at /ai/assistant — chat card with message input when aiEnabled is true](../../docs/screenshots/series-6-ai-app-features/ai-assistant-page-full.png) - -![AI Assistant page — empty state before any messages, showing the "Start a conversation" prompt](../../docs/screenshots/series-6-ai-app-features/ai-assistant-empty-state.png) - -``` -What are the best practices for structuring a .NET Clean Architecture project? -``` - -**Test HR Insight (`/ai/hr-insight`):** - -![HR Insight page at /ai/hr-insight — empty state showing four suggestion buttons and the question input](../../docs/screenshots/series-6-ai-app-features/ai-hr-insight-empty.png) - -Click a suggestion chip — *"Which department has the most employees?"* — and wait for the live data answer. - -![HR Insight page showing a data-grounded Ollama answer — references live department headcounts, execution time shown below reply](../../docs/screenshots/series-6-ai-app-features/ai-hr-insight-with-answer.png) - -**Test the disabled state:** -Set `aiEnabled: false` → hot-reload → all four pages show the info banner. - ---- - -## 📐 Where This Fits in the Architecture - -``` -src/app/ -├── routes/ai/ -│ ├── ai-assistant/ ← general chat -│ ├── ai-hr-insight/ ← HR data-aware chat -│ ├── ai-nl-search/ ← NL employee search (Article 6.4) -│ └── ai-vector-search/ ← semantic position search (Article 6.6) -├── services/api/ -│ └── ai.service.ts ← AiService (4 methods) -└── app.routes.ts ← nested ai children - -public/ -├── data/menu.json ← ai sub-menu entry with 4 children -└── i18n/en-US.json ← ai.* translation keys -``` - ---- - -## 📊 Real-World Impact - -**Without this structure:** - -* ❌ A flat sidebar link gives AI one entry point — no room to grow -* ❌ Mixing AI features in a single component creates tight coupling — a change to HR Insight risks breaking General Chat -* ❌ No URL structure for bookmarking or deep linking into specific AI tools - -**With this structure:** - -* ✅ Four focused pages — each component is ~80 lines, easy to read and test independently -* ✅ Clean URL structure — `/ai/assistant`, `/ai/hr-insight`, `/ai/nl-search`, `/ai/vector-search` -* ✅ Sidebar group — users discover all four AI features naturally via the collapsible sub-menu - - ---- - -## 🌟 Why This Matters - -The refactoring from tabs to child routes mirrors how any growing feature section should evolve in Angular. Tabs are a UI pattern — they belong in the template when you need to show multiple views of *the same data* simultaneously (like a settings panel). They are not the right pattern when each tab is a distinct feature with its own state, API calls, and navigation depth. - -**Transferable skills:** - -* **Nested routes** — The `children` array pattern works for any section that needs multiple pages under a shared URL prefix (e.g., `/employees`, `/reports/*`, `/admin/*`) -* **Sub-menu in ng-matero** — `"type": "sub"` with `"children"` is the standard pattern for any sidebar group in the ng-matero admin template -* **Backward-compat redirects** — A one-line `redirectTo` protects users, bookmarks, and automated tests when routes are renamed - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - ---- - -## 📖 Series Navigation - -**AngularNetTutorial Blog Series:** - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial -* [Run a Local LLM in Your .NET 10 API with Ollama](6.1-dotnet-ai-foundation.md) — AI Foundation (Series 6.1) -* [Build an HR AI Assistant That Knows Your Data](6.2-dotnet-ai-hr-assistant.md) — HR AI Assistant (Series 6.2) -* **This Article** — Build a Dedicated AI Section in Angular with Submenu Navigation (Series 6.3) -* [Natural Language Employee Search in Angular Material](6.4-angular-ai-nl-search.md) — NL Search Angular (Series 6.4) -* [Natural Language Employee Search with LLM Query Parsing](6.5-dotnet-natural-language-search.md) — NL Search .NET backend (Series 6.5) -* [Semantic Position Search with Vector Embeddings](6.6-angular-ai-vector-search.md) — Vector Search (Series 6.6) - ---- - -**📌 Tags:** #angular #angularmaterial #typescript #ai #ollama #llm #standalone #rxjs #cleanarchitecture #aspnetcore #dotnet #hrtech #fullstack #generativeai #locallm #routing #submenu diff --git a/blogs/series-6-ai-app-features/6.4-angular-ai-nl-search.md b/blogs/series-6-ai-app-features/6.4-angular-ai-nl-search.md deleted file mode 100644 index f5e8c6c..0000000 --- a/blogs/series-6-ai-app-features/6.4-angular-ai-nl-search.md +++ /dev/null @@ -1,446 +0,0 @@ -# Natural Language Employee Search in Angular Material - -## How to Let Users Find Employees by Describing Them in Plain English - -HR managers don't think in filter fields. They think in sentences: *"Show me all software engineers hired in the last 6 months"* or *"Find employees in IT with a salary above $80,000."* This article adds a dedicated **Natural Language Search** page to the TalentManagement Angular app — a text input that accepts a plain-English query, sends it to the LLM for parsing, then fires a structured API call using the extracted filters. - -![Natural Language Search page at /ai/nl-search — empty state with search input and prompt to type a query](../../docs/screenshots/series-6-ai-app-features/ai-nl-search-empty.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article builds on Article 6.3 (AI Submenu). The `AiService` and the `ai/nl-search` route scaffold created there are used here.** - ---- - -## 🎓 What You'll Learn - -* **Two-step AI search pattern** — LLM parses the natural language query → structured filter drives the real API call -* **RxJS `debounceTime` + `switchMap`** — How to debounce a search input and cancel in-flight requests when the user keeps typing -* **Display the parsed expression** — Show users what the LLM understood from their query, building trust in the AI output -* **Error handling at both steps** — Handle LLM parse failures and downstream API failures independently -* **`NlEmployeeFilter` interface** — The typed contract between the Angular component and the LLM parse endpoint - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 6.1 complete** — `POST /api/v1/ai/chat` endpoint working -* **Article 6.2 complete** — `POST /api/v1/ai/hr-insight` endpoint working -* **Article 6.3 complete** — AI submenu in place, `ai-nl-search` component scaffolded, `AiService` with `nlEmployeeSearch()` method -* **Article 6.5 (.NET) complete** — `POST /api/v1/ai/nl-employee-search` endpoint working (the backend for this feature) -* **`AiEnabled: true`** in the API's `appsettings.json` for local testing - -> **Note:** The .NET endpoint for natural language employee search is documented in Article 6.5. If you are following this article first (to understand the Angular side), you can still build and run the component — it will show an error until the API endpoint exists. - ---- - -## 🎯 What We're Building - -![Dashboard with the AI submenu expanded — AI Assistant, HR Insight, NL Search, Vector Search child items visible](../../docs/screenshots/series-6-ai-app-features/ai-submenu-sidebar.png) - -A dedicated **NL Search** page at `/ai/nl-search`: - -* A single text input — type a plain-English description of the employees you want -* **600ms debounce** — the API call fires 600ms after the user stops typing, not on every keystroke -* The query goes to `POST /api/v1/ai/nl-employee-search` — the LLM extracts `firstName`, `lastName`, `positionTitle`, etc. -* A **"Parsed filter"** hint shows the structured expression the LLM extracted (e.g., `positionTitle=Engineer`) -* The extracted filters are passed to the existing employee API (`GET /api/v1/employees`) as query params -* Results appear in a Material table with a link to view each employee - -**Why two steps instead of one?** - -The LLM doesn't query the database directly. It parses intent into structured parameters, then the existing, secured, paginated employee API runs the actual query. This keeps the AI layer stateless and the data access layer unchanged — the LLM is a query translator, not a data store. - ---- - -## 🚀 Implementation - -### Step 1: Add `nlEmployeeSearch` to `AiService` - -If you followed Article 6.3, `ai.service.ts` already includes this method and the `NlEmployeeFilter` interface. For reference: - -```typescript -export interface NlEmployeeFilter { - originalQuery: string; - firstName: string; - lastName: string; - email: string; - employeeNumber: string; - positionTitle: string; - parsedExpression: string; - executionTimeMs: number; -} - -// In the AiService class: -nlEmployeeSearch(query: string): Observable { - return this.http.post( - `${this.apiUrl}/ai/nl-employee-search`, - { query } - ); -} -``` - -**Why `parsedExpression`?** The backend returns a human-readable string like `"positionTitle=Engineer AND departmentName=IT"` alongside the structured fields. Displaying this in the UI lets users verify that the LLM understood their query correctly before they act on the results. - ---- - -### Step 2: Build `AiNlSearchComponent` - -**`src/app/routes/ai/ai-nl-search/ai-nl-search.component.ts`:** - -```typescript -import { Component, OnDestroy, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { Router } from '@angular/router'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { MatButtonModule } from '@angular/material/button'; -import { MatInputModule } from '@angular/material/input'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatTableModule } from '@angular/material/table'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { Subject, of } from 'rxjs'; -import { debounceTime, switchMap, catchError, takeUntil } from 'rxjs/operators'; -import { PageHeader } from '@shared'; -import { AiService, NlEmployeeFilter } from '../../../services/api/ai.service'; -import { EmployeeService } from '../../../services/api'; -import { Employee } from '../../../models'; -import { environment } from '../../../../environments/environment'; - -@Component({ - selector: 'app-ai-nl-search', - standalone: true, - templateUrl: './ai-nl-search.component.html', - styleUrl: './ai-nl-search.component.scss', - imports: [ - CommonModule, FormsModule, MatCardModule, MatIconModule, MatButtonModule, - MatInputModule, MatFormFieldModule, MatProgressSpinnerModule, - MatTableModule, MatTooltipModule, PageHeader, - ], -}) -export class AiNlSearchComponent implements OnDestroy { - private aiService = inject(AiService); - private employeeService = inject(EmployeeService); - private router = inject(Router); - private destroy$ = new Subject(); - private searchSubject = new Subject(); - - aiEnabled = environment.aiEnabled; - query = ''; - loading = false; - error = ''; - parsedExpression = ''; - results: Employee[] = []; - displayedColumns = ['employeeNumber', 'fullName', 'positionTitle', 'departmentName', 'actions']; - - constructor() { - this.searchSubject - .pipe( - debounceTime(600), - switchMap(q => { - if (!q.trim()) { - this.results = []; - this.parsedExpression = ''; - return of(null); - } - this.loading = true; - this.error = ''; - return this.aiService.nlEmployeeSearch(q).pipe( - catchError(err => { - this.error = err?.error?.detail - ?? 'Failed to parse query. Is the API running with AiEnabled: true?'; - this.loading = false; - return of(null); - }) - ); - }), - takeUntil(this.destroy$) - ) - .subscribe(filter => { - if (!filter) { this.loading = false; return; } - this.parsedExpression = filter.parsedExpression; - this.applyFilter(filter); - }); - } - - onQueryChange(): void { - this.searchSubject.next(this.query); - } - - private applyFilter(filter: NlEmployeeFilter): void { - const params: Record = {}; - if (filter.firstName) params['FirstName'] = filter.firstName; - if (filter.lastName) params['LastName'] = filter.lastName; - if (filter.email) params['Email'] = filter.email; - if (filter.employeeNumber) params['EmployeeNumber'] = filter.employeeNumber; - if (filter.positionTitle) params['PositionTitle'] = filter.positionTitle; - - this.employeeService.getAllPaged({ pageNumber: 1, pageSize: 50, ...params }).subscribe({ - next: response => { this.results = response.value; this.loading = false; }, - error: err => { - console.error('Error loading employees:', err); - this.error = 'Failed to load employee results.'; - this.loading = false; - }, - }); - } - - clear(): void { this.query = ''; this.results = []; this.parsedExpression = ''; this.error = ''; } - - viewEmployee(id: string): void { this.router.navigate(['/employees', id]); } - - ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } -} -``` - -**Key design decisions:** - -* **`debounceTime(600)` + `switchMap`** — The debounce prevents API calls on every keystroke. `switchMap` automatically cancels the in-flight request if the user types again before the current request completes — no stale results, no race conditions. - -* **`catchError` inside the `switchMap`** — Errors from `nlEmployeeSearch()` are caught at the inner observable level. Without this, a single error would complete the outer `searchSubject` stream and the search box would stop responding permanently. - -* **Two separate HTTP calls** — First to the AI endpoint (parse intent), then to the employee API (fetch data). The second call only fires if the first succeeds. This keeps each call's error handling independent. - -* **`private applyFilter`** — The method that calls `employeeService.getAllPaged()` is private because it is an implementation detail of the search pipeline, not something the template should call directly. - -* **`pageSize: 50`** — A natural language search is exploratory. 50 results is enough to give a useful answer without overwhelming the user. Standard pagination is available by navigating to the Employees list. - ---- - -### Step 3: Template — `ai-nl-search.component.html` - -```html - - - -
- - -
- info -
- AI features are disabled. -

- Set aiEnabled: true in environment.ts and - "AiEnabled": true in the API's appsettings.json. -

-
-
-
-
-
- - -
- - - manage_search - Natural Language Employee Search - Describe the employee you're looking for in plain English - - - - -
- - Search employees - - search - - -
- - -
- info_outline - Parsed filter: {{ parsedExpression }} -
- - -
- - Parsing query and searching… -
-
- error_outline - {{ error }} -
- - -
-
-
- info -

No employees found

-
-
- - - - - - - - - - - - - - - - - - - - - - -
Employee #{{ emp.employeeNumber }}Name{{ emp.firstName }} {{ emp.lastName }}Position{{ emp.positionTitle }}Department{{ emp.departmentName }}Actions - -
-

{{ results.length }} result(s) found

- - - -
- person_search -

No employees matched your query

-
-
- manage_search -

Type a natural language query to search employees

-
- - - -``` - ---- - -## 💻 Try It Yourself - -**Start the stack with AI enabled** (see Article 6.3 for setup). - -Navigate to `http://localhost:4200/ai/nl-search`. - -![Natural Language Search page at /ai/nl-search — empty state with search input and prompt to type a query](../../docs/screenshots/series-6-ai-app-features/ai-nl-search-empty.png) - -**Try these queries:** - -``` -software engineers in IT department -``` - -``` -employees with manager in their title -``` - -``` -recently hired in HR -``` - -**What to observe:** - -* After you stop typing for 600ms, the loading spinner appears -* The "Parsed filter" line shows what the LLM extracted from your query (e.g., `positionTitle=Software Engineer AND departmentName=IT`) -* The results table shows matching employees from the live database -* Click the eye icon to navigate to the employee's detail page - -**To test the disabled state:** -Set `aiEnabled: false` → the info banner appears instead of the search UI. - ---- - -## 📐 Where This Fits in the Architecture - -``` -src/app/ -├── routes/ai/ -│ └── ai-nl-search/ -│ ├── ai-nl-search.component.ts ← search pipeline (debounce → parse → fetch) -│ ├── ai-nl-search.component.html ← search input, parsed expression, results table -│ └── ai-nl-search.component.scss ← styles -├── services/api/ -│ ├── ai.service.ts ← nlEmployeeSearch() — calls LLM parse endpoint -│ └── employee.service.ts ← getAllPaged() — calls the existing employee API -└── app.routes.ts ← { path: 'nl-search', component: AiNlSearchComponent } - registered under the 'ai' children block -``` - -**No changes to:** -* The employee list component — NL search is a separate page, not embedded -* The employee API — the same `getAllPaged` endpoint is reused with the LLM-extracted filters -* Authentication — the route inherits `authGuard` from the parent `AdminLayout` block - ---- - -## 📊 Real-World Impact - -**Before this article:** - -* ❌ HR managers must use multiple filter fields to find employees -* ❌ Expressing complex criteria (title + department + hire date) requires multiple steps -* ❌ Non-technical users struggle with structured search forms - -**After this article:** - -* ✅ Plain-English search: *"Show me senior managers hired this year"* -* ✅ AI parses the intent — the user sees what was understood and can refine if needed -* ✅ Results use the existing, secured employee API — no new data access layer -* ✅ The employee list component is unchanged — NL search is opt-in via the AI submenu - ---- - -## 🌟 Why the Two-Step Pattern Scales - -The LLM-parse → structured-API pattern applies to any entity in the application: - -* **Department search** — *"Find departments with more than 20 employees"* → parse → `GET /departments?minEmployeeCount=20` -* **Position search** — *"Show me open senior positions in Engineering"* → parse → `GET /positions?level=Senior&department=Engineering` -* **Salary range search** — *"Positions with salary between $70k and $90k"* → parse → `GET /positions?minSalary=70000&maxSalary=90000` - -The LLM stays out of the data layer. It only translates human intent into the parameter shapes the existing API already understands. - -**Transferable skills:** - -* **`debounceTime` + `switchMap`** — The canonical RxJS pattern for autocomplete and search inputs. Used throughout Angular Material data tables. -* **`catchError` inside `switchMap`** — Keeping stream errors contained at the inner observable level so the outer stream stays alive. -* **Two-phase search** — Parse first, then fetch. The AI response becomes a typed data object, not a magic string. - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - ---- - -## 📖 Series Navigation - -**AngularNetTutorial Blog Series:** - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial -* [Run a Local LLM in Your .NET 10 API with Ollama](6.1-dotnet-ai-foundation.md) — AI Foundation (Series 6.1) -* [Build an HR AI Assistant That Knows Your Data](6.2-dotnet-ai-hr-assistant.md) — HR AI Assistant (Series 6.2) -* [Build a Dedicated AI Section in Angular with Submenu Navigation](6.3-angular-ai-chat-widget.md) — AI Submenu (Series 6.3) -* **This Article** — Natural Language Employee Search in Angular Material (Series 6.4) -* [Semantic Position Search with Vector Embeddings](6.6-angular-ai-vector-search.md) — Vector Search (Series 6.6) - ---- - -**📌 Tags:** #angular #angularmaterial #typescript #ai #ollama #llm #rxjs #switchmap #debounce #naturallanguage #search #nlp #hrtech #standalone #cleanarchitecture #aspnetcore #dotnet #fullstack diff --git a/blogs/series-6-ai-app-features/6.5-dotnet-natural-language-search.md b/blogs/series-6-ai-app-features/6.5-dotnet-natural-language-search.md deleted file mode 100644 index 28c2499..0000000 --- a/blogs/series-6-ai-app-features/6.5-dotnet-natural-language-search.md +++ /dev/null @@ -1,759 +0,0 @@ -# Natural Language Employee Search with LLM Query Parsing - -## How to Build a "Search in Plain English" Feature Using an LLM as a Query Translator - -The employee search form in TalentManagement has five filter fields: First Name, Last Name, Email, Employee Number, and Position Title. Each one requires the user to know the field, spell it correctly, and fill it in separately. That's five mental steps just to find someone. - -What if users could type *"find all engineers"* or *"show me employees named Smith"* and get the right results instantly? - -This article shows you how to use an LLM as a **query translator** — converting plain English into the structured filter parameters your existing API already understands. The LLM never touches your database. It simply bridges the gap between human language and machine query syntax. - -![Natural Language Search page at /ai/nl-search — single text input, type a plain-English description of the employees you are looking for](../../docs/screenshots/series-6-ai-app-features/ai-nl-search-empty.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article builds on Article 6.1 (AI Foundation) and Article 6.3 (Angular AI Service). The `IAiChatService` and `AiService` created in those articles are reused here without modification.** - ---- - -## 🎓 What You'll Learn - -* **LLM as a query translator** — Using a language model to convert natural language into structured API filter parameters, not just to generate text -* **Structured JSON output** — How to prompt an LLM to respond only with valid JSON, and how to parse and validate that output in .NET -* **Client-side orchestration** — How Angular calls the AI parse endpoint, receives filter parameters, and applies them to an existing paginated search — no new backend query logic required -* **Progressive enhancement** — The original five-field search form remains intact; NL search is an opt-in layer on top, hidden when `aiEnabled` is `false` -* **Graceful fallback** — What to do when the LLM returns malformed JSON or cannot parse the query - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 6.1 complete** — `IAiChatService`, `OllamaAiService`, `AiController`, and `[FeatureGate("AiEnabled")]` all in place -* **Article 6.3 complete** — `AiService` and `aiEnabled` feature flag in the Angular app -* **Ollama running** — `ollama serve` at `http://localhost:11434` with `llama3.2` pulled -* **`AiEnabled: true`** in `appsettings.json` for local testing - ---- - -## 🎯 What We're Building - -A new **natural language search bar** above the existing employee filter form: - -* User types a plain English query — *"find all software engineers"*, *"employees named Johnson"*, *"show HR department staff"* -* Angular sends the query to a new `POST /api/v1/ai/nl-employee-search` endpoint -* The .NET handler forwards the query to Ollama with a structured JSON prompt -* Ollama returns a filter object: `{ "positionTitle": "software engineer", "lastName": "", ... }` -* Angular reads the parsed filter and calls the existing `GET /api/v1/employees` endpoint with those parameters -* The employee table refreshes with matching results - -**Why two separate calls instead of one?** - -This design deliberately separates two responsibilities: - -* **AI endpoint** — translates language to structure (fast, no DB access) -* **Employee endpoint** — executes the query (same endpoint the existing search form uses) - -The existing pagination, sorting, and field-level filtering work exactly as before. The LLM only populates the filter parameters — it never changes how the query runs. This makes the feature easy to audit: you can log exactly what filter the LLM produced and compare it to what the user typed. - -**Hidden when `aiEnabled` is `false`:** - -The NL search bar uses `*ngIf="aiEnabled"`. Readers without Ollama see the standard five-field search form unchanged. No broken UI, no failed requests in the console. - ---- - -## 🚀 Implementation - -### Step 1: Create the Filter DTO - -Create `TalentManagementAPI.Application/Features/AI/Queries/NlSearch/NlEmployeeFilterDto.cs`: - -```csharp -#nullable enable -namespace TalentManagementAPI.Application.Features.AI.Queries.NlSearch -{ - /// - /// The structured employee filter parsed from a natural language query. - /// Each string field maps directly to a parameter accepted by GET /api/v1/employees. - /// - public sealed class NlEmployeeFilterDto - { - public string OriginalQuery { get; init; } = string.Empty; - public string FirstName { get; init; } = string.Empty; - public string LastName { get; init; } = string.Empty; - public string Email { get; init; } = string.Empty; - public string EmployeeNumber { get; init; } = string.Empty; - public string PositionTitle { get; init; } = string.Empty; - public string ParsedExpression { get; init; } = string.Empty; // human-readable summary of what was parsed - public long ExecutionTimeMs { get; init; } - } -} -``` - ---- - -### Step 2: Create the MediatR Query and Handler - -Create `TalentManagementAPI.Application/Features/AI/Queries/NlSearch/NlSearchQuery.cs`: - -```csharp -using Ardalis.Result; -using MediatR; - -namespace TalentManagementAPI.Application.Features.AI.Queries.NlSearch -{ - public record NlSearchQuery : IRequest> - { - public string Query { get; init; } = string.Empty; - } -} -``` - -Create `TalentManagementAPI.Application/Features/AI/Queries/NlSearch/NlSearchQueryHandler.cs`: - -```csharp -#nullable enable -using System.Diagnostics; -using System.Text.Json; -using Ardalis.Result; -using MediatR; -using TalentManagementAPI.Application.Interfaces; - -namespace TalentManagementAPI.Application.Features.AI.Queries.NlSearch -{ - public class NlSearchQueryHandler : IRequestHandler> - { - private readonly IAiChatService _aiChatService; - - public NlSearchQueryHandler(IAiChatService aiChatService) - { - _aiChatService = aiChatService; - } - - // System prompt: instructs the LLM to output structured JSON only. - // The fields map 1:1 to GET /api/v1/employees query parameters. - private const string SystemPrompt = """ - You are an employee search assistant. Convert the user's natural language query - into a JSON object with exactly these fields: - - { - "firstName": "", - "lastName": "", - "email": "", - "employeeNumber": "", - "positionTitle": "", - "parsedExpression": "" - } - - Rules: - - Populate only fields mentioned or implied in the query; leave others as "" - - All values are partial-match strings (e.g., "eng" matches "Engineering") - - parsedExpression: a brief human-readable summary of what you extracted (e.g., "positionTitle contains 'engineer'") - - Output ONLY the JSON object — no explanation, no markdown, no code fences - """; - - public async Task> Handle( - NlSearchQuery request, - CancellationToken cancellationToken) - { - var sw = Stopwatch.StartNew(); - - string rawJson; - try - { - rawJson = await _aiChatService.ChatAsync( - request.Query, - SystemPrompt, - cancellationToken); - } - catch (Exception ex) - { - return Result.Error($"AI service unavailable: {ex.Message}"); - } - - // Strip markdown code fences if the LLM wrapped the JSON (common with some models) - var cleaned = StripCodeFences(rawJson); - - LlmFilterResponse? parsed; - try - { - parsed = JsonSerializer.Deserialize( - cleaned, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - } - catch (JsonException) - { - // Return the raw string so callers can log what the LLM produced - return Result.Error( - $"AI returned non-JSON output. Raw response: {rawJson}"); - } - - if (parsed is null) - return Result.Error("AI returned an empty response."); - - sw.Stop(); - - return Result.Success(new NlEmployeeFilterDto - { - OriginalQuery = request.Query, - FirstName = parsed.FirstName, - LastName = parsed.LastName, - Email = parsed.Email, - EmployeeNumber = parsed.EmployeeNumber, - PositionTitle = parsed.PositionTitle, - ParsedExpression = parsed.ParsedExpression, - ExecutionTimeMs = sw.ElapsedMilliseconds, - }); - } - - private static string StripCodeFences(string text) - { - var t = text.Trim(); - if (t.StartsWith("```")) - { - var firstNewline = t.IndexOf('\n'); - if (firstNewline >= 0) t = t[(firstNewline + 1)..]; - } - if (t.EndsWith("```")) t = t[..^3]; - return t.Trim(); - } - - // Internal deserialization target — not exposed in the API response - private sealed class LlmFilterResponse - { - public string FirstName { get; set; } = string.Empty; - public string LastName { get; set; } = string.Empty; - public string Email { get; set; } = string.Empty; - public string EmployeeNumber { get; set; } = string.Empty; - public string PositionTitle { get; set; } = string.Empty; - public string ParsedExpression { get; set; } = string.Empty; - } - } -} -``` - -**Key design decisions:** - -* **`StripCodeFences`** — Despite the instruction to output raw JSON, some LLM models (including older llama versions) wrap output in ` ```json ``` ` blocks. Stripping fences defensively prevents deserialization failures on those models. - -* **`PropertyNameCaseInsensitive = true`** — The LLM may return `"FirstName"`, `"firstname"`, or `"first_name"`. Case-insensitive deserialization handles all of them without requiring a strict system prompt. - -* **`Result.Error(...)` on JSON failure** — The handler returns a structured error rather than throwing. The controller maps this to a `400 Bad Request` with the raw LLM output included, so developers can see exactly what the model returned when debugging a parse failure. - -* **`IAiChatService` reuse** — No new AI infrastructure. The same interface and Ollama service from Article 6.1 handle the request. - ---- - -### Step 3: Add the Endpoint to AiController - -Open `TalentManagementAPI.WebApi/Controllers/v1/AiController.cs` and add: - -```csharp -/// -/// Parses a natural language employee search query into structured filter parameters. -/// The returned filter fields map directly to GET /api/v1/employees query parameters. -/// -/// Natural language query (e.g., "find all engineers") -[HttpPost("nl-employee-search")] -[ProducesResponseType(typeof(NlEmployeeFilterDto), StatusCodes.Status200OK)] -[ProducesResponseType(StatusCodes.Status400BadRequest)] -public async Task NlEmployeeSearch( - [FromBody] NlEmployeeSearchRequest request, - CancellationToken cancellationToken) -{ - var query = new NlSearchQuery { Query = request.Query }; - var result = await _mediator.Send(query, cancellationToken); - - return result.IsSuccess - ? Ok(result.Value) - : BadRequest(new { detail = result.Errors.FirstOrDefault() }); -} -``` - -Add the request model (inline in the controller file or a separate `Requests/` file): - -```csharp -public record NlEmployeeSearchRequest -{ - [Required] - [MinLength(3)] - public string Query { get; init; } = string.Empty; -} -``` - -The full `AiController` after this addition has three endpoints: - -* `POST /api/v1/ai/chat` — General purpose chat (Article 6.1) -* `POST /api/v1/ai/hr-insight` — Data-aware HR assistant (Article 6.2) -* `POST /api/v1/ai/nl-employee-search` — NL query parser (this article) - -All three are protected by `[FeatureGate("AiEnabled")]` at the controller level. - ---- - -### Step 4: Update the Angular AI Service - -Open `src/app/services/api/ai.service.ts` and add the NL search types and method: - -```typescript -// --- Add these interfaces --- - -export interface NlEmployeeFilter { - originalQuery: string; - firstName: string; - lastName: string; - email: string; - employeeNumber: string; - positionTitle: string; - parsedExpression: string; - executionTimeMs: number; -} - -// --- Add this method to AiService --- - -nlEmployeeSearch(query: string): Observable { - return this.http.post( - `${this.baseUrl}/ai/nl-employee-search`, - { query } - ); -} -``` - -No changes to the service constructor, base URL, or imports — the new method follows the same pattern as `chat()` and `hrInsight()`. - ---- - -### Step 5: Add the NL Search Bar to the Employee List Component - -Open `src/app/routes/employees/employee-list.component.ts`. - -**Add the new imports and inject `AiService`:** - -```typescript -import { debounceTime, distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; -import { AiService, NlEmployeeFilter } from '../../services/api/ai.service'; -import { environment } from '../../../environments/environment'; -``` - -**Add state properties and the NL search subject:** - -```typescript -// --- Add to the component class (alongside existing properties) --- - -// AI Natural Language Search -aiEnabled = environment.aiEnabled; -nlQuery = ''; -nlLoading = false; -nlError = ''; -nlParsedExpression = ''; -private nlSearch$ = new Subject(); -private destroy$ = new Subject(); -``` - -**Inject `AiService` and set up the NL search stream in `ngOnInit`:** - -```typescript -private aiService = inject(AiService); // add to existing injections - -ngOnInit(): void { - this.loadEmployees(); // existing - - // NL search: debounce the input, cancel in-flight requests on new input - this.nlSearch$ - .pipe( - debounceTime(600), - distinctUntilChanged(), - switchMap(query => { - if (!query || query.length < 3) { - this.nlParsedExpression = ''; - this.nlError = ''; - return []; - } - this.nlLoading = true; - this.nlError = ''; - return this.aiService.nlEmployeeSearch(query); - }), - takeUntil(this.destroy$) - ) - .subscribe({ - next: (filter: NlEmployeeFilter) => { - this.nlLoading = false; - this.nlParsedExpression = filter.parsedExpression; - this.applyNlFilter(filter); - }, - error: err => { - this.nlLoading = false; - this.nlError = err?.error?.detail ?? 'Could not parse query. Try rephrasing.'; - } - }); -} - -ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); -} - -// Called when the user types in the NL search input -onNlQueryChange(value: string): void { - this.nlQuery = value; - this.nlSearch$.next(value); -} - -// Clears the NL search and resets the form to its initial state -clearNlSearch(): void { - this.nlQuery = ''; - this.nlParsedExpression = ''; - this.nlError = ''; - this.searchForm.reset(); // reset the five-field form - this.loadEmployees(); // reload full list -} - -// Applies the LLM-parsed filter to the existing search form, then triggers a search -private applyNlFilter(filter: NlEmployeeFilter): void { - this.searchForm.patchValue({ - FirstName: filter.firstName, - LastName: filter.lastName, - Email: filter.email, - EmployeeNumber: filter.employeeNumber, - PositionTitle: filter.positionTitle, - }); - // The existing search form subscription picks up the patchValue and executes the search -} -``` - -**Key design decisions:** - -* **`switchMap`** — Cancels any in-flight HTTP request to `nl-employee-search` if the user keeps typing. Without `switchMap`, fast typing would queue multiple AI calls that arrive out of order. - -* **`debounceTime(600)`** — 600ms rather than the 500ms on the regular form. NL search is slower (Ollama needs inference time), so a slightly longer debounce reduces unnecessary calls without feeling sluggish. - -* **`applyNlFilter` calls `patchValue`** — The NL search populates the existing reactive form fields. The form's `valueChanges` subscription (already in the component) picks up these values and executes the standard employee search. No new HTTP call for employees is needed here — the existing search flow handles it. - -* **`clearNlSearch`** — Gives users an explicit way to return to the full list and clear all filters. This matters because after NL search, the five-field form shows the parsed values (e.g., `PositionTitle: "engineer"`). Users need a clear way to reset. - ---- - -### Step 6: Update the Employee List Template - -Open `src/app/routes/employees/employee-list.component.html`. - -Add the NL search bar **above the existing `
` filter section**: - -```html - - - - - - - smart_toy - Search in plain English - - - search - - - - - -
- auto_awesome - AI interpreted: {{ nlParsedExpression }} -
- - -
- warning - {{ nlError }} -
- -
-
- - - - -
-``` - ---- - -### Step 7: Add Styles - -Add to `employee-list.component.scss`: - -```scss -.nl-search-card { - margin-bottom: 16px; - - .nl-search-field { - width: 100%; - } - - .nl-parsed-hint { - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; - color: rgba(0, 0, 0, 0.6); - margin-top: 4px; - - mat-icon { - font-size: 16px; - height: 16px; - width: 16px; - color: #1976d2; - } - } - - .nl-error { - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; - color: #b00020; - margin-top: 4px; - - mat-icon { - font-size: 16px; - height: 16px; - width: 16px; - } - } -} -``` - ---- - -## 💡 Prompt Engineering: Getting Reliable JSON from an LLM - -The hardest part of this feature is not the Angular or .NET code — it's writing a system prompt that reliably produces valid, parseable JSON. - -**What can go wrong:** - -* LLM wraps JSON in markdown fences (` ```json ``` `) -* LLM adds explanation before or after the JSON (`"Here is the JSON: {...}"`) -* LLM uses different field names than specified (`"first_name"` instead of `"firstName"`) -* LLM includes extra fields not in the schema (`"department": "Engineering"`) -* LLM returns natural language when it does not understand the query - -**How the system prompt in this article addresses each:** - -* **Markdown fences** → `StripCodeFences()` in the handler removes them defensively -* **Extra explanation** → *"Output ONLY the JSON object — no explanation, no markdown, no code fences"* -* **Wrong field names** → `PropertyNameCaseInsensitive = true` in `JsonSerializer.Deserialize` -* **Extra fields** → Ignored by deserialization (only mapped fields are read) -* **Natural language fallback** → `JsonException` is caught; error response includes raw LLM output - -**One-shot example in the prompt:** - -The system prompt in Step 2 does not include an example. For `llama3.2`, the field listing and the "Output ONLY the JSON" instruction is sufficient. If you switch to a smaller or less instruction-tuned model and see formatting issues, add a one-shot example to the system prompt: - -``` -Example input: "show me all software engineers" -Example output: {"firstName":"","lastName":"","email":"","employeeNumber":"","positionTitle":"software engineer","parsedExpression":"positionTitle contains 'software engineer'"} -``` - -One-shot examples dramatically improve JSON consistency across model families. - ---- - -## 💻 Try It Yourself - -**Enable AI features:** - -```bash -# In appsettings.json (ApiResources/TalentManagement-API/TalentManagementAPI.WebApi): -"FeatureManagement": { - "AiEnabled": true -} - -# In environment.ts (Clients/.../src/environments/): -aiEnabled: true -``` - -**Start the stack:** - -```bash -# Terminal 1: Ollama -ollama serve - -# Terminal 2: .NET API -cd ApiResources/TalentManagement-API/TalentManagementAPI.WebApi -dotnet run - -# Terminal 3: Angular -cd Clients/TalentManagement-Angular-Material/talent-management -npm start -``` - -**Test queries:** - -Open `http://localhost:4200` → log in → navigate to Employees. - -* **"find all engineers"** — populates `PositionTitle: "engineer"`, searches all employees with engineer in their job title -* **"show employees named Johnson"** — populates `LastName: "johnson"`, returns all Johnsons -* **"find john smith"** — populates `FirstName: "john"` and `LastName: "smith"` -* **"employee number EMP001"** — populates `EmployeeNumber: "EMP001"` -* **"hr manager emails"** — populates `PositionTitle: "hr manager"` (and potentially `Email` if the LLM interprets it that way) - -**Verify the existing search still works:** - -After testing NL search, click the **×** clear button to reset. The five-field form becomes active again with empty values and the full employee list reloads. NL search is additive — it never breaks the baseline. - -**Test with `aiEnabled: false`:** - -Set `aiEnabled: false` in `environment.ts` → hot-reload → navigate to Employees. The NL search bar is gone. The existing five-field form is exactly as it was before Series 6. - -**Verify in Swagger:** - -Navigate to `https://localhost:44378/swagger` → open `POST /api/v1/ai/nl-employee-search`. - -![Swagger UI AI controller expanded — POST /ai/chat, POST /ai/hr-insight, POST /ai/nl-employee-search](../../docs/screenshots/series-6-ai-app-features/swagger-ai-endpoints.png) - -```json -{ "query": "find all software engineers" } -``` - -Expected response: - -```json -{ - "originalQuery": "find all software engineers", - "firstName": "", - "lastName": "", - "email": "", - "employeeNumber": "", - "positionTitle": "software engineer", - "parsedExpression": "positionTitle contains 'software engineer'", - "executionTimeMs": 3241 -} -``` - ---- - -## 📐 What Changed in the Architecture - -**API (TalentManagement-API):** - -``` -Application/Features/AI/Queries/NlSearch/ -├── NlSearchQuery.cs ← MediatR query record -├── NlSearchQueryHandler.cs ← LLM call + JSON parse -└── NlEmployeeFilterDto.cs ← API response shape - -WebApi/Controllers/v1/ -└── AiController.cs ← +POST nl-employee-search endpoint -``` - -**No changes to:** - -* `IAiChatService` — reused as-is from Article 6.1 -* `OllamaAiService` — no modification -* `EmployeesController` — the existing GET endpoint handles filtered queries unchanged -* Any Domain or Infrastructure.Persistence code - -**Angular (TalentManagement-Angular-Material):** - -``` -src/app/services/api/ -└── ai.service.ts ← +NlEmployeeFilter interface, +nlEmployeeSearch() - -src/app/routes/employees/ -├── employee-list.component.ts ← inject AiService, add NL search state + stream -├── employee-list.component.html ← +mat-card NL search bar above existing form -└── employee-list.component.scss ← +nl-search-card styles -``` - -**No changes to:** - -* `app.routes.ts` — no new route -* `menu.json` — no new menu item -* `environment.ts` — `aiEnabled` flag already added in Article 6.3 - ---- - -## 📊 Real-World Impact - -**Before this article:** - -* ❌ Users must know the exact field to filter on (position? name? email?) -* ❌ Finding "all engineers named John" requires filling in two separate fields -* ❌ Non-technical HR staff must learn the filter form structure - -**After this article:** - -* ✅ Users type natural language — the AI fills in the filter fields automatically -* ✅ The parsed interpretation is shown ("AI interpreted: positionTitle contains 'engineer'") — users can verify what the AI understood -* ✅ Original filter form remains fully functional — power users can still use it directly -* ✅ Zero impact on readers without Ollama — the NL bar is invisible when `aiEnabled: false` -* ✅ One AI call per search (not per keystroke) — `debounceTime` and `switchMap` keep API usage minimal - ---- - -## 🌟 Why This Pattern Scales - -Using an LLM as a **query translator** — input: natural language, output: structured parameters — applies to any search or filter interface in your application. - -**Other scenarios where this pattern works:** - -* **Time-based queries** — *"show new hires from last quarter"* → LLM outputs `hireDate.gte: "2024-10-01"` and `hireDate.lte: "2024-12-31"` -* **Salary range queries** — *"employees earning over 80k"* → LLM outputs `salaryMin: 80000` -* **Multi-entity queries** — *"find developers in the Austin office"* → LLM outputs `positionTitle: "developer"` + `location: "Austin"` -* **Project management** — *"show me open tasks assigned to Alice"* → LLM outputs `status: "open"` + `assignee: "alice"` - -**The implementation pattern is always the same:** - -1. Write a system prompt that maps natural language to your API's filter schema -2. Parse the LLM's JSON output, handling code fence stripping and case normalization -3. Apply the parsed parameters to your existing query endpoint -4. Show users what the AI interpreted so they can verify or refine - -**Transferable skills:** - -* **Structured output prompting** — instructing an LLM to produce machine-readable JSON rather than conversational text -* **Defensive JSON parsing** — handling model-specific formatting quirks without breaking the feature -* **`switchMap` for async search** — replacing previous results with the latest, preventing race conditions in reactive search flows -* **Layered AI integration** — adding AI as a translation layer over existing infrastructure rather than replacing it - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - ---- - -## 📖 Series Navigation - -**AngularNetTutorial Blog Series:** - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial -* [Run a Local LLM in Your .NET 10 API with Ollama](6.1-dotnet-ai-foundation.md) — AI Foundation (Series 6.1) -* [Build an HR AI Assistant That Knows Your Data](6.2-dotnet-ai-hr-assistant.md) — HR AI Assistant (Series 6.2) -* [Build a Dedicated AI Section in Angular with Submenu Navigation](6.3-angular-ai-chat-widget.md) — AI Submenu (Series 6.3) -* [Natural Language Employee Search in Angular Material](6.4-angular-ai-nl-search.md) — NL Search Angular (Series 6.4) -* **This Article** — Natural Language Employee Search with LLM Query Parsing (Series 6.5) -* [Cache Your AI Responses: Save Time and API Costs](6.8-dotnet-ai-response-caching.md) — AI Response Caching (Series 6.8) - ---- - -**📌 Tags:** #angular #dotnet #ai #ollama #llm #naturallanguage #search #promptengineering #csharp #typescript #cleanarchitecture #mediatr #angularmaterial #rxjs #fullstack #generativeai #locallm #hrtech #structuredoutput diff --git a/blogs/series-6-ai-app-features/6.6-angular-ai-vector-search.md b/blogs/series-6-ai-app-features/6.6-angular-ai-vector-search.md deleted file mode 100644 index eb21ebf..0000000 --- a/blogs/series-6-ai-app-features/6.6-angular-ai-vector-search.md +++ /dev/null @@ -1,391 +0,0 @@ -# Semantic Position Search with Vector Embeddings in Angular Material - -## How to Find Positions by Meaning, Not Just Keywords - -Keyword search fails for positions. A user who types *"cloud infrastructure role"* will miss positions titled *"DevOps Engineer"* or *"Platform SRE"* — even if those roles are exactly what they're looking for. This article adds a dedicated **Vector Search** page to the TalentManagement app — a semantic search that finds positions by *meaning*, not exact text match. - -![Vector Search page at /ai/vector-search — empty state with search input and prompt to describe a position](../../docs/screenshots/series-6-ai-app-features/ai-vector-search-empty.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. The full-stack tutorial — covering Angular 20, .NET 10 Web API, and OAuth 2.0 with Duende IdentityServer — has been published at [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56). **This article builds on Article 6.3 (AI Submenu). The `AiService` and the `ai/vector-search` route scaffold created there are used here.** - ---- - -> **⚠️ Skeleton Article:** The .NET backend for semantic search (vector embeddings, `POST /api/v1/positions/semantic-search`) is not yet documented. This article covers the Angular component fully. The backend article will be published separately. - ---- - -## 🎓 What You'll Learn - -* **Semantic search vs. keyword search** — Why vector similarity finds conceptually related results that exact-match search misses -* **Match score display** — How to show a percentage similarity badge (`score` from 0.0 to 1.0) in an Angular Material table -* **`debounceTime` + `switchMap`** — The same RxJS pattern from Article 6.4, applied to vector search -* **`SemanticPositionResult` interface** — The typed contract between the Angular component and the vector search endpoint -* **Navigation from search to detail** — How to navigate to an existing position detail page from the search results - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 6.1 complete** — AI foundation in place -* **Article 6.3 complete** — AI submenu, `AiService` with `semanticPositionSearch()` method, `ai-vector-search` component scaffolded -* **`VectorSearchEnabled: true`** in the API's `appsettings.json` (separate from `AiEnabled`) -* **Vector embeddings seeded** — The API must have position descriptions embedded in the vector store - -> **Note:** Vector search requires additional backend setup (embedding model, vector store). See the .NET vector search article when published. - ---- - -## 🎯 What We're Building - -![Dashboard with the AI submenu expanded — AI Assistant, HR Insight, NL Search, Vector Search child items visible](../../docs/screenshots/series-6-ai-app-features/ai-submenu-sidebar.png) - -A dedicated **Vector Search** page at `/ai/vector-search`: - -* A single text input — describe the position you're looking for in natural language -* **600ms debounce** — identical pattern to NL Search (Article 6.4) -* The query goes to `POST /api/v1/positions/semantic-search` — returns up to 10 positions ranked by semantic similarity -* A **match score badge** shows each result's similarity percentage (green badge, e.g. `87%`) -* Results display in a Material table with position number, title, department, salary range, and a view link - -**How vector search works:** - -1. The user's query text is embedded into a vector (a list of numbers representing meaning) -2. The API compares that vector against pre-computed embeddings for all position descriptions -3. Positions with similar meanings (not just matching words) are returned, ranked by cosine similarity -4. The `score` field (0.0–1.0) represents how semantically close each position is to the query - ---- - -## 🚀 Implementation - -### Step 1: `semanticPositionSearch` in `AiService` - -If you followed Article 6.3, `ai.service.ts` already includes this method and the `SemanticPositionResult` interface. For reference: - -```typescript -export interface SemanticPositionResult { - id: string; - positionNumber: string; - positionTitle: string; - positionDescription: string; - departmentName: string; - salaryRangeName: string; - score: number; - executionTimeMs: number; -} - -// In the AiService class: -semanticPositionSearch(queryText: string, topK = 10): Observable { - return this.http.post( - `${this.apiUrl}/positions/semantic-search`, - { queryText, topK } - ); -} -``` - -**Why `topK = 10`?** The default returns the 10 most similar positions. Semantic search does not have a concept of "no results" — it always returns the closest matches. Limiting to 10 shows meaningful results without overwhelming the user with low-confidence matches. - ---- - -### Step 2: Build `AiVectorSearchComponent` - -**`src/app/routes/ai/ai-vector-search/ai-vector-search.component.ts`:** - -```typescript -import { Component, OnDestroy, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { Router } from '@angular/router'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { MatButtonModule } from '@angular/material/button'; -import { MatInputModule } from '@angular/material/input'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatTableModule } from '@angular/material/table'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatChipsModule } from '@angular/material/chips'; -import { Subject, of } from 'rxjs'; -import { debounceTime, switchMap, catchError, takeUntil } from 'rxjs/operators'; -import { PageHeader } from '@shared'; -import { AiService, SemanticPositionResult } from '../../../services/api/ai.service'; -import { environment } from '../../../../environments/environment'; - -@Component({ - selector: 'app-ai-vector-search', - standalone: true, - templateUrl: './ai-vector-search.component.html', - styleUrl: './ai-vector-search.component.scss', - imports: [ - CommonModule, FormsModule, MatCardModule, MatIconModule, MatButtonModule, - MatInputModule, MatFormFieldModule, MatProgressSpinnerModule, - MatTableModule, MatTooltipModule, MatChipsModule, PageHeader, - ], -}) -export class AiVectorSearchComponent implements OnDestroy { - private aiService = inject(AiService); - private router = inject(Router); - private destroy$ = new Subject(); - private searchSubject = new Subject(); - - aiEnabled = environment.aiEnabled; - query = ''; - loading = false; - error = ''; - results: SemanticPositionResult[] = []; - displayedColumns = ['score', 'positionNumber', 'positionTitle', 'departmentName', 'salaryRangeName', 'actions']; - - constructor() { - this.searchSubject - .pipe( - debounceTime(600), - switchMap(q => { - if (!q.trim()) { this.results = []; return of(null); } - this.loading = true; - this.error = ''; - return this.aiService.semanticPositionSearch(q).pipe( - catchError(err => { - this.error = err?.error?.detail - ?? 'Failed to search. Is the API running with VectorSearchEnabled: true?'; - this.loading = false; - return of(null); - }) - ); - }), - takeUntil(this.destroy$) - ) - .subscribe(results => { - if (results === null) { this.loading = false; return; } - this.results = results; - this.loading = false; - }); - } - - onQueryChange(): void { this.searchSubject.next(this.query); } - - clear(): void { this.query = ''; this.results = []; this.error = ''; } - - viewPosition(id: string): void { this.router.navigate(['/positions', id]); } - - scorePercent(score: number): string { return `${Math.round(score * 100)}%`; } - - ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } -} -``` - -**Key design decisions:** - -* **`of(null)` on empty query** — Clears the results and returns an immediately-completing observable when the input is empty. `switchMap` needs an observable — returning `null` directly would throw. - -* **`scorePercent()` helper** — Converts the raw float (e.g. `0.87`) to a display string (`87%`). Keeping this logic in the component (not the template) makes it testable. - -* **Single-step search** — Unlike NL Search (Article 6.4), vector search is a single API call. There is no second structured API call — the backend returns the already-resolved position records alongside the scores. - -* **`MatChipsModule`** in imports — Included for potential score badge styling via chip-like appearance. Used in the SCSS `.score-badge` class. - ---- - -### Step 3: Template — `ai-vector-search.component.html` - -```html - - - -
- - -
- info -
- AI features are disabled. -

- Set aiEnabled: true in environment.ts and - "VectorSearchEnabled": true in the API's appsettings.json. -

-
-
-
-
-
- -
- - - travel_explore - Vector Search — Positions - Find positions using semantic similarity - - - -
- - Describe the position - - travel_explore - - -
- -
- - Searching with vector similarity… -
-
- error_outline - {{ error }} -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Match - {{ scorePercent(pos.score) }} - Position #{{ pos.positionNumber }}Title{{ pos.positionTitle }}Department{{ pos.departmentName }}Salary Range{{ pos.salaryRangeName }}Actions - -
-

{{ results.length }} result(s) found

-
- -
- work_off -

No positions matched your query

-
-
- travel_explore -

Describe a position to find semantic matches

-
- -
-
-
-``` - ---- - -## 💻 Try It Yourself - -**Prerequisites:** Vector embeddings must be seeded in the API. See the .NET vector search article. - -Navigate to `http://localhost:4200/ai/vector-search`. - -![Vector Search page at /ai/vector-search — empty state with search input and prompt to describe a position](../../docs/screenshots/series-6-ai-app-features/ai-vector-search-empty.png) - -**Try these queries:** - -``` -senior cloud infrastructure engineer -``` - -``` -HR specialist with recruiting experience -``` - -``` -data analyst with SQL and Python skills -``` - -**What to observe:** - -* Results are ranked by the `Match` badge (highest similarity first) -* Positions with different titles but similar meanings appear (e.g., querying *"cloud engineer"* may surface *"DevOps Engineer"* and *"Platform Engineer"*) -* The score drops for results that are less conceptually similar - ---- - -## 📐 Where This Fits in the Architecture - -``` -src/app/ -├── routes/ai/ -│ └── ai-vector-search/ -│ ├── ai-vector-search.component.ts ← search pipeline (debounce → vector call) -│ ├── ai-vector-search.component.html ← search input, score badges, results table -│ └── ai-vector-search.component.scss ← score-badge styles -├── services/api/ -│ └── ai.service.ts ← semanticPositionSearch() — calls POST /positions/semantic-search -└── app.routes.ts ← { path: 'vector-search', component: AiVectorSearchComponent } - registered under the 'ai' children block -``` - ---- - -## 📊 Real-World Impact - -**Before this article:** - -* ❌ Position search only matches on exact title or position number keywords -* ❌ Users must know the exact job title to find it -* ❌ Synonyms and related concepts are invisible to the search - -**After this article:** - -* ✅ Search by description: *"leadership role in engineering"* finds relevant positions -* ✅ Match score shows confidence — users can set their own threshold -* ✅ The position list component is unchanged — vector search is a separate AI-powered page -* ✅ Backend-agnostic — swap the vector store (Qdrant, pgvector, Azure AI Search) without touching Angular - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - ---- - -## 📖 Series Navigation - -**AngularNetTutorial Blog Series:** - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial -* [Run a Local LLM in Your .NET 10 API with Ollama](6.1-dotnet-ai-foundation.md) — AI Foundation (Series 6.1) -* [Build an HR AI Assistant That Knows Your Data](6.2-dotnet-ai-hr-assistant.md) — HR AI Assistant (Series 6.2) -* [Build a Dedicated AI Section in Angular with Submenu Navigation](6.3-angular-ai-chat-widget.md) — AI Submenu (Series 6.3) -* [Natural Language Employee Search in Angular Material](6.4-angular-ai-nl-search.md) — NL Search (Series 6.4) -* **This Article** — Semantic Position Search with Vector Embeddings (Series 6.6) - ---- - -**📌 Tags:** #angular #angularmaterial #typescript #ai #vectorsearch #embeddings #semanticsearch #rxjs #switchmap #hrtech #standalone #cleanarchitecture #aspnetcore #dotnet #fullstack #generativeai #locallm diff --git a/blogs/series-6-ai-app-features/6.7-dotnet-mssql-vector-search.md b/blogs/series-6-ai-app-features/6.7-dotnet-mssql-vector-search.md deleted file mode 100644 index 62a5c73..0000000 --- a/blogs/series-6-ai-app-features/6.7-dotnet-mssql-vector-search.md +++ /dev/null @@ -1,735 +0,0 @@ -# Semantic Position Search with SQL Server Native Vector Search - -## How to Use Ollama Embeddings + EF Core 10 VECTOR_DISTANCE to Find Positions by Meaning, Not Keywords - -Keyword search is fast but brittle. Type *"software engineer"* and you miss positions titled *"application developer"* or *"full-stack developer"*. Users must know the exact words in the database. - -Semantic search solves this: instead of matching characters, it matches **meaning**. Every position is stored as a vector of 768 floating-point numbers that encodes the semantic content of its title and description. When a user searches, the query is embedded into the same vector space, and SQL Server returns positions ranked by cosine distance — closest meaning first. - -This article adds semantic search to the **Position list page** using: - -* **Ollama `nomic-embed-text`** — a local embedding model, free to run, no API key -* **SQL Server native `vector(768)` column** — stored directly in the Positions table, no separate vector database -* **EF Core 10 `EF.Functions.VectorDistance`** — LINQ query, no raw SQL strings -* **Angular search bar** — same progressive-enhancement pattern as NL search in Article 6.5 - -![Vector Search page at /ai/vector-search — semantic position search powered by SQL Server native vector columns and Ollama nomic-embed-text embeddings](../../docs/screenshots/series-6-ai-app-features/ai-vector-search-empty.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. **This article builds on Article 6.1 (AI Foundation). The `IOllamaApiClient` singleton and `VectorSearchEnabled` feature flag pattern follow the same conventions established there.** - ---- - -## 🎓 What You'll Learn - -* **Embedding text with Ollama** — calling `EmbedAsync` on `IOllamaApiClient` to convert text to `float[]` -* **SQL Server native vector columns** — storing and querying `vector(768)` using EF Core 10 and `Microsoft.Data.SqlTypes.SqlVector` -* **`EF.Functions.VectorDistance`** — LINQ-native cosine distance, no raw SQL -* **On-demand embedding** — embedding at query time rather than at write time keeps the implementation simple and avoids a background job for this tutorial -* **Separate feature flag** — `VectorSearchEnabled` decouples this feature from `AiEnabled`, so readers without SQL Server 2025 are unaffected - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 6.1 complete** — `IOllamaApiClient` singleton registered, `OllamaSharp` installed -* **SQL Server 2022+ or Azure SQL** with vector support enabled (or SQL Server 2025) -* **`nomic-embed-text` pulled in Ollama** — `ollama pull nomic-embed-text` -* **`UseInMemoryDatabase: false`** — vector columns require a real SQL Server instance - -```bash -ollama pull nomic-embed-text -``` - ---- - -## 🎯 What We're Building - -A **semantic search bar** above the Position list: - -* User types *"cloud infrastructure specialist"* -* Angular sends to `POST /api/v1/positions/semantic-search` -* Handler embeds the query text via Ollama → `float[768]` -* EF Core queries `VECTOR_DISTANCE('cosine', SearchEmbedding, @queryVector)` on the Positions table -* Returns top-10 positions ranked by similarity, with a `score` (0–1, higher = closer match) -* Angular replaces the table with ranked results - -**On-demand embedding:** - -Positions are embedded **at query time against each stored embedding**. In production you'd pre-compute and store embeddings when positions are created or updated. For this tutorial, the `SearchEmbedding` column is populated by a one-time `POST /api/v1/positions/generate-embeddings` admin endpoint so you can seed the data and test immediately. - -**Hidden when `aiEnabled` is `false`:** - -The semantic search bar uses `*ngIf="aiEnabled"`. Readers without Ollama or SQL Server vector support see the standard keyword search form unchanged. - ---- - -## 🚀 Implementation - -### Step 1: Add `SearchEmbedding` to the Position Entity - -Open `TalentManagementAPI.Domain/Entities/Position.cs` and add: - -```csharp -/// -/// 768-dimensional embedding of PositionTitle + PositionDescription. -/// Used for semantic similarity search via VECTOR_DISTANCE. -/// Null until embeddings are generated. -/// -public float[]? SearchEmbedding { get; set; } -``` - ---- - -### Step 2: Map the Vector Column in EF Configuration - -Open `TalentManagementAPI.Infrastructure.Persistence/Contexts/ApplicationDbContextHelpers.cs` and add inside the `Position` entity configuration: - -```csharp -entity.Property(e => e.SearchEmbedding) - .HasColumnType("vector(768)") - .IsRequired(false); -``` - -**Why `vector(768)`?** - -`nomic-embed-text` outputs 768-dimensional embeddings. The dimension count must match the model output exactly — SQL Server enforces this at write time. If you switch to `mxbai-embed-large` (1024 dims) or `all-minilm` (384 dims), update the column type and the `IEmbeddingService` contract accordingly. - -**Schema note:** - -`EnsureCreated()` (used in development) will create the `vector(768)` column automatically from this configuration. For production with migrations, run: - -```bash -dotnet ef migrations add AddPositionSearchEmbedding -dotnet ef database update -``` - ---- - -### Step 3: Create the Embedding Service Interface - -Create `TalentManagementAPI.Application/Interfaces/IEmbeddingService.cs`: - -```csharp -namespace TalentManagementAPI.Application.Interfaces -{ - /// - /// Generates a float[] embedding vector for a given text input. - /// Implementations call a local or remote embedding model. - /// - public interface IEmbeddingService - { - Task EmbedAsync(string text, CancellationToken cancellationToken = default); - } -} -``` - ---- - -### Step 4: Create the Ollama Embedding Service - -Create `TalentManagementAPI.Infrastructure.Shared/Services/OllamaEmbeddingService.cs`: - -```csharp -using OllamaSharp; -using OllamaSharp.Models; -using TalentManagementAPI.Application.Interfaces; - -namespace TalentManagementAPI.Infrastructure.Shared.Services -{ - /// - /// Generates text embeddings using the Ollama /api/embed endpoint. - /// Uses a dedicated embedding model (e.g. nomic-embed-text) separate from the chat model. - /// - public sealed class OllamaEmbeddingService : IEmbeddingService - { - private readonly IOllamaApiClient _client; - private readonly string _embeddingModel; - - public OllamaEmbeddingService(IOllamaApiClient client, string embeddingModel) - { - _client = client; - _embeddingModel = embeddingModel; - } - - public async Task EmbedAsync(string text, CancellationToken cancellationToken = default) - { - var request = new EmbedRequest - { - Model = _embeddingModel, - Input = new List { text } - }; - - var response = await _client.EmbedAsync(request, cancellationToken).ConfigureAwait(false); - - if (response?.Embeddings is null || response.Embeddings.Count == 0) - throw new InvalidOperationException($"Ollama returned no embeddings for model '{_embeddingModel}'."); - - return response.Embeddings[0].Select(d => (float)d).ToArray(); - } - } -} -``` - -**Why a separate embedding model?** - -Chat models (`llama3.2`) and embedding models (`nomic-embed-text`) serve different purposes. Chat models generate text; embedding models encode meaning into fixed-length vectors. Using a chat model for embeddings produces poor-quality vectors. The `_embeddingModel` is configured independently from `Ollama:Model` in `appsettings.json`. - ---- - -### Step 5: Create the Semantic Search Query and Handler - -Create `TalentManagementAPI.Application/Features/Positions/Queries/SemanticSearch/SemanticPositionSearchQuery.cs`: - -```csharp -#nullable enable -using System.Diagnostics; -using Microsoft.Data.SqlTypes; -using Microsoft.EntityFrameworkCore; -using TalentManagementAPI.Application.Common.Results; -using TalentManagementAPI.Application.Interfaces; -using TalentManagementAPI.Application.Messaging; -using TalentManagementAPI.Infrastructure.Persistence.Contexts; - -namespace TalentManagementAPI.Application.Features.Positions.Queries.SemanticSearch -{ - public sealed class SemanticPositionSearchQuery : IRequest>> - { - public string QueryText { get; init; } = string.Empty; - public int TopK { get; init; } = 10; - } - - public sealed class SemanticPositionSearchQueryHandler - : IRequestHandler>> - { - private readonly IEmbeddingService _embeddingService; - private readonly ApplicationDbContext _dbContext; - - public SemanticPositionSearchQueryHandler( - IEmbeddingService embeddingService, - ApplicationDbContext dbContext) - { - _embeddingService = embeddingService; - _dbContext = dbContext; - } - - public async Task>> Handle( - SemanticPositionSearchQuery request, - CancellationToken cancellationToken) - { - var sw = Stopwatch.StartNew(); - - float[] queryVector; - try - { - queryVector = await _embeddingService - .EmbedAsync(request.QueryText, cancellationToken) - .ConfigureAwait(false); - } - catch (Exception ex) - { - return Result>.Failure( - $"Embedding service unavailable: {ex.Message}"); - } - - var sqlVector = new SqlVector(queryVector); - - var results = await _dbContext.Positions - .Where(p => p.SearchEmbedding != null) - .Include(p => p.Department) - .Include(p => p.SalaryRange) - .Select(p => new - { - Position = p, - Score = 1.0 - EF.Functions.VectorDistance("cosine", new SqlVector(p.SearchEmbedding!), sqlVector) - }) - .OrderByDescending(x => x.Score) - .Take(request.TopK) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - - sw.Stop(); - - var dtos = results.Select(r => new SemanticPositionResultDto - { - Id = r.Position.Id, - PositionNumber = r.Position.PositionNumber, - PositionTitle = r.Position.PositionTitle?.Value ?? string.Empty, - PositionDescription = r.Position.PositionDescription, - DepartmentName = r.Position.Department?.Name?.Value ?? string.Empty, - SalaryRangeName = r.Position.SalaryRange?.Name ?? string.Empty, - Score = Math.Round(r.Score, 4), - ExecutionTimeMs = sw.ElapsedMilliseconds, - }).ToList(); - - return Result>.Success(dtos); - } - } -} -``` - ---- - -### Step 6: Create the Result DTO - -Create `TalentManagementAPI.Application/Features/Positions/Queries/SemanticSearch/SemanticPositionResultDto.cs`: - -```csharp -namespace TalentManagementAPI.Application.Features.Positions.Queries.SemanticSearch -{ - public sealed class SemanticPositionResultDto - { - public Guid Id { get; init; } - public string PositionNumber { get; init; } = string.Empty; - public string PositionTitle { get; init; } = string.Empty; - public string PositionDescription { get; init; } = string.Empty; - public string DepartmentName { get; init; } = string.Empty; - public string SalaryRangeName { get; init; } = string.Empty; - public double Score { get; init; } - public long ExecutionTimeMs { get; init; } - } -} -``` - ---- - -### Step 7: Add Endpoints to PositionsController - -Open `TalentManagementAPI.WebApi/Controllers/v1/PositionsController.cs` and add two endpoints: - -```csharp -/// -/// Finds positions semantically similar to the query text using vector distance. -/// Requires VectorSearchEnabled feature flag and populated SearchEmbedding values. -/// -[HttpPost("semantic-search")] -[AllowAnonymous] -[ProducesResponseType(typeof(List), StatusCodes.Status200OK)] -[ProducesResponseType(StatusCodes.Status400BadRequest)] -[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] -public async Task SemanticSearch( - [FromBody] SemanticPositionSearchRequest request, - [FromServices] IFeatureManagerSnapshot featureManager, - CancellationToken cancellationToken) -{ - if (!await featureManager.IsEnabledAsync("VectorSearchEnabled")) - return Problem( - detail: "Vector search is disabled. Enable FeatureManagement:VectorSearchEnabled to use this endpoint.", - title: "Vector search is disabled", - statusCode: StatusCodes.Status503ServiceUnavailable); - - var result = await Mediator.Send( - new SemanticPositionSearchQuery { QueryText = request.QueryText, TopK = request.TopK }, - cancellationToken); - - return result.IsSuccess - ? Ok(result.Value) - : BadRequest(new { detail = result.Errors.FirstOrDefault() }); -} - -/// -/// Generates and stores SearchEmbedding for all positions that don't have one yet. -/// Run this once after seeding data to enable semantic search. -/// -[HttpPost("generate-embeddings")] -[Authorize] -[ProducesResponseType(StatusCodes.Status200OK)] -[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] -public async Task GenerateEmbeddings( - [FromServices] IFeatureManagerSnapshot featureManager, - [FromServices] IEmbeddingService embeddingService, - [FromServices] ApplicationDbContext dbContext, - CancellationToken cancellationToken) -{ - if (!await featureManager.IsEnabledAsync("VectorSearchEnabled")) - return Problem( - detail: "Vector search is disabled.", - title: "Vector search is disabled", - statusCode: StatusCodes.Status503ServiceUnavailable); - - var positions = dbContext.Positions - .Where(p => p.SearchEmbedding == null) - .ToList(); - - foreach (var position in positions) - { - var text = $"{position.PositionTitle?.Value} {position.PositionDescription}".Trim(); - position.SearchEmbedding = await embeddingService.EmbedAsync(text, cancellationToken); - } - - dbContext.Positions.UpdateRange(positions); - await dbContext.SaveChangesAsync(cancellationToken); - - return Ok(new { generated = positions.Count }); -} -``` - -Add the request record at the bottom of the file: - -```csharp -public record SemanticPositionSearchRequest( - [Required][MinLength(3)] string QueryText, - int TopK = 10); -``` - ---- - -### Step 8: Update appsettings.json - -```json -"FeatureManagement": { - "AiEnabled": false, - "CacheEnabled": true, - "VectorSearchEnabled": false -}, -"Ollama": { - "BaseUrl": "http://localhost:11434", - "Model": "llama3.2", - "EmbeddingModel": "nomic-embed-text", - "CacheTtlMinutes": 60 -} -``` - ---- - -### Step 9: Register IEmbeddingService - -Open `TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs` and add: - -```csharp -var embeddingModel = config["Ollama:EmbeddingModel"] ?? "nomic-embed-text"; -services.AddTransient(sp => new OllamaEmbeddingService( - sp.GetRequiredService(), - embeddingModel)); -``` - ---- - -### Step 10: Update the Angular AI Service - -Open `src/app/services/api/ai.service.ts` and add: - -```typescript -export interface SemanticPositionResult { - id: string; - positionNumber: string; - positionTitle: string; - positionDescription: string; - departmentName: string; - salaryRangeName: string; - score: number; - executionTimeMs: number; -} - -// In AiService class: -semanticPositionSearch(queryText: string, topK = 10): Observable { - return this.http.post( - `${this.apiUrl}/positions/semantic-search`, - { queryText, topK } - ); -} -``` - ---- - -### Step 11: Update the Position List Component - -Open `src/app/routes/positions/position-list.component.ts`. - -**Add imports:** - -```typescript -import { Subject } from 'rxjs'; -import { switchMap, debounceTime, distinctUntilChanged, catchError, takeUntil } from 'rxjs/operators'; -import { of } from 'rxjs'; -import { AiService, SemanticPositionResult } from '../../services/api/ai.service'; -import { environment } from '../../../environments/environment'; -``` - -**Add state properties:** - -```typescript -private aiService = inject(AiService); -private destroy$ = new Subject(); - -aiEnabled = environment.aiEnabled; -semanticQuery = ''; -semanticLoading = false; -semanticError = ''; -semanticResults: SemanticPositionResult[] | null = null; -private semanticSearch$ = new Subject(); -``` - -**Wire up in `ngOnInit`:** - -```typescript -this.setupSemanticSearch(); -``` - -**Add to `ngOnDestroy`:** - -```typescript -this.destroy$.next(); -this.destroy$.complete(); -``` - -**Add new methods:** - -```typescript -onSemanticQueryChange(value: string): void { - this.semanticQuery = value; - this.semanticSearch$.next(value); -} - -clearSemanticSearch(): void { - this.semanticQuery = ''; - this.semanticError = ''; - this.semanticResults = null; -} - -private setupSemanticSearch(): void { - this.semanticSearch$ - .pipe( - debounceTime(600), - distinctUntilChanged(), - switchMap(query => { - if (!query || query.length < 3) { - this.semanticResults = null; - this.semanticError = ''; - return of(null); - } - this.semanticLoading = true; - this.semanticError = ''; - return this.aiService.semanticPositionSearch(query).pipe( - catchError(err => { - this.semanticLoading = false; - this.semanticError = err?.error?.detail ?? 'Semantic search failed. Try rephrasing.'; - return of(null); - }) - ); - }), - takeUntil(this.destroy$) - ) - .subscribe(results => { - this.semanticLoading = false; - this.semanticResults = results; - }); -} -``` - ---- - -### Step 12: Update the Position List Template - -Open `src/app/routes/positions/position-list.component.html`. - -Add **above** the existing search filters `
`: - -```html - - - - - - psychology - Search by meaning - - - search - - - -
- warning - {{ semanticError }} -
-
-
-``` - -Below the semantic card, show semantic results **instead of** the standard table when results are present: - -```html - -
-

- auto_awesome - {{ semanticResults.length }} positions ranked by semantic similarity -

- - - - - - - - - - - - - - - - - - - - - - - -
Position #{{ r.positionNumber }}Position Title{{ r.positionTitle }}Department{{ r.departmentName }}Salary Range{{ r.salaryRangeName }}Score - {{ (r.score * 100).toFixed(1) }}% -
-
- - -
- -``` - ---- - -## 💻 Try It Yourself - -**Setup:** - -```bash -# Pull the embedding model -ollama pull nomic-embed-text -ollama serve - -# Enable flags in appsettings.json -"AiEnabled": true, -"VectorSearchEnabled": true, -"UseInMemoryDatabase": false -``` - -**Seed embeddings (one-time):** - -Navigate to `https://localhost:44378/swagger` → `POST /api/v1/positions/generate-embeddings` → Execute. - -![Swagger UI AI controller expanded — POST /ai/chat, POST /ai/hr-insight, POST /ai/nl-employee-search](../../docs/screenshots/series-6-ai-app-features/swagger-ai-endpoints.png) - -Expected response: `{ "generated": 25 }` (or however many positions exist) - -**Test semantic search via Swagger:** - -```json -{ "queryText": "cloud infrastructure specialist", "topK": 5 } -``` - -Expected: positions ranked by cosine similarity — titles like "DevOps Engineer", "Platform Engineer", "Systems Administrator" score higher than "Payroll Analyst". - -**Test via Angular UI:** - -Navigate to `http://localhost:4200` → Positions → type in the semantic search bar. - -![Angular Material data table listing employees — same sortable table pattern used for position results with semantic match scores](../../docs/screenshots/series-6-ai-app-features/employee-list-table.png) - -* *"data scientist"* — returns data-related positions ranked by similarity -* *"manages people and projects"* — returns manager/director positions -* *"builds web applications"* — returns developer/engineer positions - -**Test with `aiEnabled: false`** — semantic bar hidden, keyword filters work normally. - ---- - -## 📐 What Changed in the Architecture - -**API:** - -``` -Domain/Entities/ -└── Position.cs ← +SearchEmbedding float[]? - -Infrastructure.Persistence/Contexts/ -└── ApplicationDbContextHelpers.cs ← +vector(768) column mapping - -Application/Interfaces/ -└── IEmbeddingService.cs ← new: float[] EmbedAsync(text) - -Infrastructure.Shared/Services/ -└── OllamaEmbeddingService.cs ← new: calls IOllamaApiClient.EmbedAsync - -Application/Features/Positions/Queries/SemanticSearch/ -├── SemanticPositionSearchQuery.cs ← new: query + nested handler -└── SemanticPositionResultDto.cs ← new: result shape with Score - -WebApi/Controllers/v1/ -└── PositionsController.cs ← +POST semantic-search, +POST generate-embeddings - -WebApi/ -└── appsettings.json ← +VectorSearchEnabled, +EmbeddingModel -``` - -**Angular:** - -``` -src/app/services/api/ -└── ai.service.ts ← +SemanticPositionResult interface, +semanticPositionSearch() - -src/app/routes/positions/ -├── position-list.component.ts ← +AiService, semantic search state + stream -├── position-list.component.html ← +semantic search card + results table -└── position-list.component.scss ← +nl-search-card styles -``` - ---- - -## 📊 Keyword vs Semantic Search - -**Keyword search:** - -* Query: *"engineer"* -* Matches: positions where the title or description contains the string "engineer" -* Misses: "Application Developer", "Platform Specialist", "DevOps Practitioner" - -**Semantic search:** - -* Query: *"engineer"* -* Matches: positions semantically similar to engineering — including "Application Developer", "Platform Specialist", "Systems Architect" -* Ranked by cosine distance — closest meaning at the top - -The two search modes are **complementary**. Keyword filters are fast and precise when you know exact terms. Semantic search is better for discovery — finding what you mean, not what you type. - ---- - -## 🤝 Community & Support - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - ---- - -## 📖 Series Navigation - -* [Run a Local LLM in Your .NET 10 API with Ollama](6.1-dotnet-ai-foundation.md) — AI Foundation (Series 6.1) -* [Build an HR AI Assistant That Knows Your Data](6.2-dotnet-ai-hr-assistant.md) — HR AI Assistant (Series 6.2) -* [Add an AI Chat Widget to Angular with Material Design](6.3-angular-ai-chat-widget.md) — AI Chat Widget (Series 6.3) -* [AI-Generated Dashboard Insights in Angular Material](6.4-angular-ai-dashboard-insights.md) — Dashboard Insights (Series 6.4) -* [Natural Language Employee Search with LLM Query Parsing](6.5-dotnet-natural-language-search.md) — NL Search (Series 6.5) -* [Cache Your AI Responses: Save Time and API Costs](6.8-dotnet-ai-response-caching.md) — AI Caching (Series 6.8) -* **This Article** — Semantic Position Search with SQL Server Vector Search (Series 6.7) - ---- - -**📌 Tags:** #dotnet #csharp #ai #ollama #vectorsearch #semanticsearch #sqlserver #efcore #angular #angularmaterial #embeddings #locallm #cleanarchitecture #fullstack #generativeai diff --git a/blogs/series-6-ai-app-features/6.8-dotnet-ai-response-caching.md b/blogs/series-6-ai-app-features/6.8-dotnet-ai-response-caching.md deleted file mode 100644 index 93fb2d6..0000000 --- a/blogs/series-6-ai-app-features/6.8-dotnet-ai-response-caching.md +++ /dev/null @@ -1,426 +0,0 @@ -# Cache Your AI Responses: Save Time and API Costs - -## How to Add a Cache-Aside Layer to Your .NET LLM Service Without Changing a Single Caller - -Every call to `ChatAsync` hits Ollama, waits for inference, and returns a reply. For a local model that takes 2–5 seconds per response, this is noticeable. For a cloud model billed per token, it adds up fast. - -But many AI queries in a business app are **repetitive**. HR managers ask the same workforce questions. The natural language search bar sees the same queries. The dashboard insight is re-fetched on every page load. - -The fix is a **cache-aside decorator** — a wrapper around `IAiChatService` that checks the cache first, returns instantly on a hit, and only calls Ollama on a miss. No changes to any handler, controller, or query class. The callers don't know the cache exists. - -This article shows you how to build that decorator using the EasyCaching infrastructure already in the TalentManagement API, and how to expose cache status to API consumers via an `X-AI-Cache: HIT/MISS` response header. - -![HR Insight page showing AI response with execution time — the cache decorator reduces repeat query times from seconds to milliseconds](../../docs/screenshots/series-6-ai-app-features/ai-hr-insight-with-answer.png) - -📖 **Tutorial Repository:** [AngularNetTutorial on GitHub](https://github.com/workcontrolgit/AngularNetTutorial) - ---- - -This article is part of the **AngularNetTutorial** series. **This article builds on Article 6.1 (AI Foundation). The `IAiChatService`, `OllamaAiService`, and EasyCaching infrastructure created in earlier articles are reused here.** - ---- - -## 🎓 What You'll Learn - -* **Decorator pattern for services** — wrap an existing service implementation with new behavior without touching callers -* **Cache-aside with EasyCaching** — check cache → return on hit → call service on miss → store result -* **SHA256 cache keys** — deterministic, compact keys derived from the full prompt, collision-resistant -* **Scoped metadata services** — carry information (cache hit status) from a service layer call to a controller response without coupling layers -* **`X-AI-Cache` response header** — let API consumers, developers, and monitoring tools see exactly which responses came from cache - ---- - -## 📋 Prerequisites - -**Before following this article, you should have:** - -* **Article 6.1 complete** — `IAiChatService`, `OllamaAiService`, `AiController`, and EasyCaching infrastructure all in place -* **EasyCaching already installed** — `EasyCaching.Core` and `EasyCaching.InMemory` are already in `TalentManagementAPI.WebApi.csproj` -* **`CacheEnabled: true`** in `appsettings.json` for local testing (the existing `CachingOptions.Enabled` flag) - ---- - -## 🎯 What We're Building - -A **caching decorator** for `IAiChatService`: - -* Every `ChatAsync` call produces a **SHA256 cache key** from the system prompt + user message -* On **cache hit** — return the stored reply immediately (no Ollama call), set `X-AI-Cache: HIT` -* On **cache miss** — call `OllamaAiService.ChatAsync`, store the reply, set `X-AI-Cache: MISS` -* TTL is configurable via `Ollama:CacheTtlMinutes` in `appsettings.json` (default: 60 minutes) -* Zero changes to `GetHrInsightQuery`, `NlSearchQueryHandler`, or any existing handler — they all call `IAiChatService` and keep working unchanged - -**Why a decorator and not a pipeline behavior?** - -A MediatR pipeline behavior would only cache at the query level — you'd need one behavior per query type. A decorator on `IAiChatService` caches at the infrastructure level: every caller, every query, every endpoint, all in one place. Add it once, it covers the entire AI surface. - -**Why SHA256 for the cache key?** - -System prompts in this app can be hundreds of characters long. Using the raw prompt as a cache key is wasteful and fragile. SHA256 produces a compact 64-character hex string from any input, is deterministic (same inputs always produce the same key), and has no practical collisions. - ---- - -## 🚀 Implementation - -### Step 1: Add `CacheTtlMinutes` to appsettings.json - -Open `TalentManagementAPI.WebApi/appsettings.json` and add `CacheTtlMinutes` to the existing `Ollama` section: - -```json -"Ollama": { - "BaseUrl": "http://localhost:11434", - "Model": "llama3.2", - "CacheTtlMinutes": 60 -} -``` - -This controls how long an AI response stays cached. 60 minutes works well for: -* Dashboard insights — workforce data doesn't change by the minute -* HR questions — same question asked repeatedly during a session -* NL search — common queries like "find all engineers" resolve identically every time - -Set it lower (e.g., `5`) during development to test both HIT and MISS paths easily. - ---- - -### Step 2: Create the Response Metadata Interface - -Create `TalentManagementAPI.Application/Interfaces/IAiResponseMetadata.cs`: - -```csharp -namespace TalentManagementAPI.Application.Interfaces -{ - /// - /// Scoped service that carries AI response metadata (e.g., cache hit status) - /// from the service layer to the controller within a single request. - /// - public interface IAiResponseMetadata - { - bool WasCacheHit { get; set; } - } -} -``` - -This is a **scoped service** — one instance per HTTP request. The caching decorator writes `WasCacheHit = true/false`. The controller reads it and adds the `X-AI-Cache` header. No HTTP concerns bleed into the service layer. - ---- - -### Step 3: Create the Caching Decorator - -Create `TalentManagementAPI.Infrastructure.Shared/Services/CachingAiChatService.cs`: - -```csharp -#nullable enable -using System.Security.Cryptography; -using System.Text; -using TalentManagementAPI.Application.Interfaces; -using TalentManagementAPI.Application.Interfaces.Caching; - -namespace TalentManagementAPI.Infrastructure.Shared.Services -{ - /// - /// Decorator for IAiChatService that adds cache-aside behaviour. - /// Checks the cache first; on a miss, delegates to the inner service and stores the result. - /// Sets IAiResponseMetadata.WasCacheHit so controllers can emit X-AI-Cache headers. - /// - public sealed class CachingAiChatService : IAiChatService - { - private readonly IAiChatService _inner; - private readonly ICacheProvider _cache; - private readonly IAiResponseMetadata _metadata; - private readonly TimeSpan _ttl; - - public CachingAiChatService( - IAiChatService inner, - ICacheProvider cache, - IAiResponseMetadata metadata, - TimeSpan ttl) - { - _inner = inner; - _cache = cache; - _metadata = metadata; - _ttl = ttl; - } - - public async Task ChatAsync( - string message, - string? systemPrompt = null, - CancellationToken cancellationToken = default) - { - var cacheKey = BuildCacheKey(message, systemPrompt); - - var cached = await _cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false); - if (cached is not null) - { - _metadata.WasCacheHit = true; - return cached; - } - - var reply = await _inner.ChatAsync(message, systemPrompt, cancellationToken).ConfigureAwait(false); - - if (!string.IsNullOrEmpty(reply)) - { - await _cache.SetAsync( - cacheKey, - reply, - new CacheEntryOptions(_ttl), - cancellationToken).ConfigureAwait(false); - } - - _metadata.WasCacheHit = false; - return reply; - } - - /// - /// Builds a deterministic 64-char hex cache key from the prompt inputs. - /// SHA256 keeps keys compact regardless of how long the system prompt is. - /// - private static string BuildCacheKey(string message, string? systemPrompt) - { - var raw = $"{systemPrompt ?? string.Empty}|{message}"; - var hash = SHA256.HashData(Encoding.UTF8.GetBytes(raw)); - return $"ai:chat:{Convert.ToHexString(hash).ToLowerInvariant()}"; - } - } -} -``` - -**Key design decisions:** - -* **`_inner` is `IAiChatService`** — the decorator depends on the interface, not `OllamaAiService` directly. You could stack another decorator on top (e.g., a logging decorator) without changing this class. - -* **`cached is not null` check** — `ICacheProvider.GetAsync` returns `null` on a miss. An empty string `""` would be a valid (if unusual) cached reply, so we check for null specifically. - -* **Guard: `!string.IsNullOrEmpty(reply)`** — we don't cache empty responses. If Ollama returned nothing (service glitch, model timeout), we don't want to permanently cache an empty string. - -* **`_ttl` passed at construction** — keeps this class free of `IConfiguration` dependencies. The registration code (next step) reads the TTL from config and passes the resolved `TimeSpan`. - ---- - -### Step 4: Register the Decorator and Metadata Service - -Open `TalentManagementAPI.Infrastructure.Shared/ServiceRegistration.cs` and update the AI service registrations: - -```csharp -services.AddScoped(); - -var ttlMinutes = config.GetValue("Ollama:CacheTtlMinutes", 60); -var ttl = TimeSpan.FromMinutes(ttlMinutes); - -services.AddTransient(); -services.AddTransient(sp => new CachingAiChatService( - sp.GetRequiredService(), - sp.GetRequiredService(), - sp.GetRequiredService(), - ttl)); -``` - -Also add the concrete `AiResponseMetadata` implementation in the same file (or a separate small file): - -```csharp -internal sealed class AiResponseMetadata : IAiResponseMetadata -{ - public bool WasCacheHit { get; set; } -} -``` - -**Why `AddTransient()` and `AddTransient(...)`?** - -We need the DI container to resolve `OllamaAiService` by its concrete type (not by `IAiChatService`) so the decorator can wrap it. Registering `OllamaAiService` directly by its concrete type enables `sp.GetRequiredService()` in the factory. If we only registered it as `IAiChatService`, `GetRequiredService()` would throw. - ---- - -### Step 5: Add `X-AI-Cache` Header to AiController - -Open `TalentManagementAPI.WebApi/Controllers/v1/AiController.cs` and inject `IAiResponseMetadata`: - -```csharp -private readonly IAiChatService _aiChatService; -private readonly IFeatureManagerSnapshot _featureManager; -private readonly IAiResponseMetadata _aiMetadata; - -public AiController( - IAiChatService aiChatService, - IFeatureManagerSnapshot featureManager, - IAiResponseMetadata aiMetadata) -{ - _aiChatService = aiChatService; - _featureManager = featureManager; - _aiMetadata = aiMetadata; -} -``` - -Add a private helper to set the header after any AI endpoint call: - -```csharp -private void SetAiCacheHeader() -{ - Response.Headers["X-AI-Cache"] = _aiMetadata.WasCacheHit ? "HIT" : "MISS"; -} -``` - -Call it in each endpoint after the AI response: - -```csharp -// In Chat(): -var reply = await _aiChatService.ChatAsync(request.Message, request.SystemPrompt, cancellationToken); -SetAiCacheHeader(); -return Ok(new AiChatResponse(reply)); - -// In HrInsight(): -var result = await Mediator.Send(new GetHrInsightQuery { Question = request.Question }, cancellationToken); -SetAiCacheHeader(); -return Ok(result); - -// In NlEmployeeSearch(): -var result = await Mediator.Send(new NlSearchQuery { Query = request.Query }, cancellationToken); -SetAiCacheHeader(); -return result.IsSuccess - ? Ok(result.Value) - : BadRequest(new { detail = result.Errors.FirstOrDefault() }); -``` - ---- - -## 💻 Try It Yourself - -**Configure:** - -```json -// appsettings.json -"Ollama": { - "BaseUrl": "http://localhost:11434", - "Model": "llama3.2", - "CacheTtlMinutes": 5 -}, -"FeatureManagement": { - "AiEnabled": true, - "CacheEnabled": true -} -``` - -**Start the stack and verify via Swagger:** - -Navigate to `https://localhost:44378/swagger` → `POST /api/v1/ai/chat`. - -![Swagger UI AI controller expanded — POST /ai/chat, POST /ai/hr-insight, POST /ai/nl-employee-search](../../docs/screenshots/series-6-ai-app-features/swagger-ai-endpoints.png) - -```json -{ "message": "What is Clean Architecture?" } -``` - -**First call (MISS):** -* Takes 2–4 seconds -* Response header: `X-AI-Cache: MISS` - -**Second call (same body):** -* Returns in < 50ms -* Response header: `X-AI-Cache: HIT` - -**Test with `CacheEnabled: false`:** - -```json -"FeatureManagement": { - "AiEnabled": true, - "CacheEnabled": false -} -``` - -All calls return `X-AI-Cache: MISS` — the `EasyCachingProviderAdapter.IsCacheEnabled()` guard returns `false`, `GetAsync` always returns null, and the decorator always calls through to Ollama. - -**Test TTL expiry:** - -Set `CacheTtlMinutes: 1`, make a call, wait 90 seconds, call again. The second call returns `X-AI-Cache: MISS` and takes the full inference time again. - ---- - -## 📐 What Changed in the Architecture - -``` -Application/Interfaces/ -└── IAiResponseMetadata.cs ← new: carries cache hit status per request - -Infrastructure.Shared/Services/ -├── OllamaAiService.cs ← unchanged -└── CachingAiChatService.cs ← new: cache-aside decorator - -Infrastructure.Shared/ -└── ServiceRegistration.cs ← updated: register decorator + metadata - -WebApi/Controllers/v1/ -└── AiController.cs ← updated: inject metadata, add X-AI-Cache header - -WebApi/ -└── appsettings.json ← updated: +CacheTtlMinutes -``` - -**No changes to:** - -* `GetHrInsightQuery` — still calls `IAiChatService` unchanged -* `NlSearchQueryHandler` — still calls `IAiChatService` unchanged -* `EmployeesController` — untouched -* Any Domain or Infrastructure.Persistence code - ---- - -## 📊 Real-World Impact - -**Before this article:** - -* ❌ Every AI call goes to Ollama regardless of whether the same question was asked 30 seconds ago -* ❌ Dashboard insight re-fetches on every page load (same prompt every time) -* ❌ No visibility into which responses came from cache vs inference - -**After this article:** - -* ✅ Repeated queries return instantly — `X-AI-Cache: HIT` in < 50ms -* ✅ First-call latency (MISS) is unchanged — no overhead added to the inference path -* ✅ `CacheEnabled: false` disables caching globally without touching service code -* ✅ TTL configurable per environment — short for dev, longer for production -* ✅ Cache header visible in Swagger, browser DevTools, and any API monitoring tool - ---- - -## 🌟 Why the Decorator Pattern Scales - -The decorator approach applies to any cross-cutting concern you want to add to a service: - -**Other decorators you could stack on `IAiChatService`:** - -* **Logging decorator** — log every prompt and response with timing -* **Rate-limiting decorator** — reject calls above N per minute per user -* **Sanitization decorator** — strip PII from prompts before sending to the LLM -* **Fallback decorator** — try a backup model if the primary fails - -Each decorator wraps the previous one. The controller always calls `IAiChatService`. Registration order controls the wrapping order. No handler, query, or controller changes — ever. - ---- - -## 🤝 Community & Support - -**Questions or feedback?** The tutorial repository welcomes: - -* ⭐ **GitHub stars** — Help others discover it! -* 🐛 **Issue reports** — Found a bug or have a suggestion? -* 💬 **Discussions** — Ask questions, share your use cases -* 🚀 **Pull requests** — Improvements always appreciated - ---- - -## 📖 Series Navigation - -**AngularNetTutorial Blog Series:** - -* [Building Modern Web Applications with Angular, .NET, and OAuth 2.0](https://medium.com/scrum-and-coke/building-modern-web-applications-with-angular-net-and-oauth-2-0-complete-tutorial-series-7ea97ed3fc56) — Main tutorial -* [Run a Local LLM in Your .NET 10 API with Ollama](6.1-dotnet-ai-foundation.md) — AI Foundation (Series 6.1) -* [Build an HR AI Assistant That Knows Your Data](6.2-dotnet-ai-hr-assistant.md) — HR AI Assistant (Series 6.2) -* [Build a Dedicated AI Section in Angular with Submenu Navigation](6.3-angular-ai-chat-widget.md) — AI Submenu Refactor (Series 6.3) -* [Natural Language Employee Search in Angular Material](6.4-angular-ai-nl-search.md) — NL Search Angular (Series 6.4) -* [Natural Language Employee Search with LLM Query Parsing](6.5-dotnet-natural-language-search.md) — NL Search .NET Backend (Series 6.5) -* [Semantic Position Search with Vector Embeddings in Angular Material](6.6-angular-ai-vector-search.md) — Vector Search Angular (Series 6.6) -* [Semantic Position Search with SQL Server Native Vector Search](6.7-dotnet-mssql-vector-search.md) — Vector Search .NET Backend (Series 6.7) -* **This Article** — Cache Your AI Responses (Series 6.8) - ---- - -**📌 Tags:** #dotnet #csharp #ai #ollama #llm #caching #easycaching #designpatterns #decorator #cleanarchitecture #performance #webapi #aspnetcore #locallm #generativeai diff --git a/data/dashboard.json b/data/dashboard.json new file mode 100644 index 0000000..93b5aca --- /dev/null +++ b/data/dashboard.json @@ -0,0 +1,28 @@ +{ + "dashboard": [ + { + "type": "Total Sales", + "amount": 180200, + "progress": 50, + "date": 1427207139000 + }, + { + "type": "Revenue", + "amount": 70205, + "progress": 70, + "date": 1427412725000 + }, + { + "type": "Traffic", + "amount": 1291922, + "progress": 80, + "date": 1427546580000 + }, + { + "type": "New User", + "amount": 1922, + "progress": 40, + "date": 1427891640000 + } + ] +} diff --git a/data/menu.json b/data/menu.json new file mode 100644 index 0000000..c91ef17 --- /dev/null +++ b/data/menu.json @@ -0,0 +1,122 @@ +{ + "menu": [ + { + "route": "dashboard", + "name": "dashboard", + "type": "link", + "icon": "dashboard" + }, + { + "route": "employees", + "name": "employees", + "type": "sub", + "icon": "people", + "children": [ + { + "route": "", + "name": "employeeList", + "type": "link" + }, + { + "route": "create", + "name": "addEmployee", + "type": "link", + "permissions": { + "only": ["canAdd"] + } + } + ] + }, + { + "route": "departments", + "name": "departments", + "type": "sub", + "icon": "business", + "children": [ + { + "route": "", + "name": "departmentList", + "type": "link" + }, + { + "route": "create", + "name": "addDepartment", + "type": "link", + "permissions": { + "only": ["canAdd"] + } + } + ] + }, + { + "route": "positions", + "name": "positions", + "type": "sub", + "icon": "work", + "children": [ + { + "route": "", + "name": "positionList", + "type": "link" + }, + { + "route": "create", + "name": "addPosition", + "type": "link", + "permissions": { + "only": ["canAdd"] + } + } + ] + }, + { + "route": "salary-ranges", + "name": "salaryRanges", + "type": "sub", + "icon": "attach_money", + "children": [ + { + "route": "", + "name": "salaryRangeList", + "type": "link" + }, + { + "route": "create", + "name": "addSalaryRange", + "type": "link", + "permissions": { + "only": ["canAdd"] + } + } + ] + }, + { + "route": "ai", + "name": "ai", + "type": "sub", + "icon": "smart_toy", + "children": [ + { + "route": "assistant", + "name": "aiAssistant", + "type": "link" + }, + { + "route": "hr-insight", + "name": "aiHrInsight", + "type": "link" + }, + { + "route": "nl-search", + "name": "aiNlSearch", + "type": "link" + }, + { + "route": "vector-search", + "name": "aiVectorSearch", + "type": "link" + } + ] + } + ] +} diff --git a/docs/01-foundation.md b/docs/01-foundation.md deleted file mode 100644 index f8475c9..0000000 --- a/docs/01-foundation.md +++ /dev/null @@ -1,877 +0,0 @@ -# Part 1: Foundation — Understanding the CAT Pattern - -## Building Modern Web Applications with Angular, .NET, and OAuth 2.0 - -**[Tutorial Home](TUTORIAL.md)** | **[Next: Part 2 →](02-token-service-deep-dive.md)** - ---- - -Welcome to this comprehensive tutorial series that demonstrates building a modern, secure web application using the **CAT (Client, API Resource, Token Service)** pattern. This pattern represents industry best practices for building scalable, maintainable, and secure enterprise applications. - ---- - -## 📚 What You'll Learn - -* How to architect modern web applications using separation of concerns -* Implementing OAuth 2.0 and OpenID Connect (OIDC) authentication -* Building RESTful APIs with Clean Architecture -* Creating responsive UIs with Angular and Material Design -* Managing distributed codebases with Git submodules -* Securing APIs with JWT tokens -* Role-based access control (RBAC) - -## 👥 Who This Tutorial Is For - -* Full-stack developers looking to learn modern authentication patterns -* Teams building enterprise applications requiring secure authentication -* Architects designing microservices-based systems -* Developers transitioning to Angular and .NET stacks - ---- - -## 🎯 What is the CAT Pattern? - -The **CAT (Client, API Resource, Token Service)** pattern is an architectural approach that separates authentication, business logic, and user interface into three distinct tiers. - -### Architecture Overview - -``` -┌─────────────────┬─────────────────┬─────────────────┐ -│ Client │ API Resource │ Token Service │ -│ (Angular) │ (.NET API) │ (IdentityServer)│ -│ │ │ │ -│ • UI/UX │ • Business │ • Auth │ -│ • Routing │ Logic │ • Tokens │ -│ • State Mgmt │ • Data Access │ • Users │ -│ • API Calls │ • Validation │ • OAuth 2.0 │ -└─────────────────┴─────────────────┴─────────────────┘ - │ │ │ - └──────── HTTPS + JWT Tokens ───────┘ -``` - - -![Angular dashboard (anonymous access)](images/angular/application-dashboard-anonymous.png) -*Figure: Angular dashboard accessible before authentication.* - -### Why CAT Pattern? - -✅ **Separation of Concerns** — Each tier has a single, well-defined responsibility - -✅ **Independent Scaling** — Scale each component based on demand - -✅ **Technology Agnostic** — Swap implementations without affecting other tiers - -✅ **Security by Design** — Centralized authentication with token-based authorization - -✅ **Microservices Ready** — Foundation for transitioning to microservices architecture - ---- - -## 🏗️ High-Level Architecture - -Our application consists of three main components: - -### 1. **Angular Client (Port 4200)** -* Material Design UI -* OIDC Client authentication -* HTTP Interceptor adds Bearer tokens to requests - -### 2. **IdentityServer (Port 44310)** -* User authentication -* OAuth 2.0 / OIDC flows -* Token issuance and validation -* Client and scope management - -![IdentityServer Admin dashboard](images/identityserver/identityserver-admin-dashboard.png) -*Figure: IdentityServer Admin dashboard for authentication service management.* - -### 3. **ASP.NET Core Web API (Port 44378)** -* CRUD operations -* Business logic -* JWT authentication -* Role-based authorization - -![Swagger API endpoints overview](images/webapi/swagger-api-endpoints.png) -*Figure: Swagger UI for API endpoint discovery and testing.* - -### Authentication Flow - -``` - -![Angular login entry from user menu](images/angular/angular-login-page.png) -*Figure: User opens the top-right menu and starts sign-in from Angular.* - -![IdentityServer login page](images/angular/identityserver-login-ashtyn1.png) -*Figure: IdentityServer login page where user credentials are entered.* -1. User clicks "Login" in Angular - ↓ -2. Redirect to IdentityServer - ↓ -3. User enters credentials - ↓ -4. IdentityServer validates credentials - ↓ -5. Redirect back with authorization code - ↓ -6. Exchange code for tokens (PKCE) - ↓ -7. Store tokens in memory - ↓ -8. API requests include Bearer token - ↓ -9. API validates token against IdentityServer - ↓ -10. Return protected data -``` - ---- - -## 🔐 Key Security Features - -### OAuth 2.0 Authorization Code Flow with PKCE -Secure authentication for Single Page Applications with protection against authorization code interception. - -### JWT Bearer Token Authentication -Stateless API authentication with token-based authorization and scopes. - -### Role-Based Access Control (RBAC) -Fine-grained permissions using ngx-permissions and API endpoint protection. - -### Secure Token Storage -In-memory token storage (no localStorage) with automatic token refresh. - -### HTTPS Enforcement -All communication encrypted with CORS configuration for cross-origin requests. - ---- - -## 🚀 Getting Started - -### Prerequisites - -**Tools you'll need:** - -* **.NET SDK 10.0+** — Build and run .NET applications — [Download](https://dotnet.microsoft.com/download) -* **Node.js 20.x LTS** — Run Angular development server — [Download](https://nodejs.org/) -* **npm 10+** — Package manager for Node.js — Included with Node.js -* **Git (Latest)** — Version control and submodules — [Download](https://git-scm.com/) -* **Visual Studio Code** — Recommended code editor — [Download](https://code.visualstudio.com/) - -**Optional tools:** -* SQL Server Management Studio or Azure Data Studio for database management -* Postman for API testing -* Git GUI Client like GitHub Desktop or SourceTree - -### System Requirements - -* **Operating System:** Windows 10/11, macOS 10.15+, or Linux -* **RAM:** 8GB minimum (16GB recommended) -* **Disk Space:** 5GB free space -* **Browser:** Chrome, Edge, or Firefox (latest versions) - -### Clone the Repository - -```bash -# Clone with all submodules -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git - -cd AngularNetTutorial - -# Verify submodules are initialized -git submodule status -``` - -If you've already cloned without submodules: - -```bash -git submodule update --init --recursive -``` - -### Quick Start: Running All Components - -**⚠️ Important:** Start components in this specific order! - -#### Step 1: Start IdentityServer (Token Service) - -**Why First?** The API and Angular client both depend on IdentityServer for token validation. - -```bash -cd TokenService/Duende-IdentityServer/src/Duende.STS.Identity -dotnet restore -dotnet run -``` - -**Wait for:** `Now listening on: https://localhost:44310` - -**Verify:** Open browser to `https://localhost:44310` — you should see the IdentityServer login page - -#### Step 2: Start API Resource (Backend) - -**Why Second?** The API needs IdentityServer running to validate tokens. - -```bash -cd ApiResources/TalentManagement-API -dotnet restore -dotnet run -``` - -**Wait for:** `Now listening on: https://localhost:44378` - -**Verify:** Open browser to `https://localhost:44378/swagger` — you should see Swagger UI - -#### Step 3: Start Angular Client (Frontend) - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management -npm install -npm start -``` - -**Wait for:** `✔ Browser application bundle generation complete.` - -**Verify:** Open browser to `http://localhost:4200` — you should see the login page - -### Application URLs - -**Where to access each component:** - -* **Angular Client:** http://localhost:4200 — Main application UI -* **Web API:** https://localhost:44378 — RESTful API endpoints -* **Swagger UI:** https://localhost:44378/swagger — API documentation -* **IdentityServer:** https://localhost:44310 — Authentication server -* **IdentityServer Admin:** https://localhost:44303 — Admin panel -* **IdentityServer Admin API:** https://localhost:44302 — Admin API - -### First Login - -**Try it out:** - -1. Navigate to **http://localhost:4200** -2. Click **"Sign In"** -3. You'll be redirected to IdentityServer (https://localhost:44310) -4. Use test credentials: - * **Username:** `ashtyn1` - * **Password:** `Pa$$word123` -5. After successful login, you'll be redirected back to the Angular dashboard - -![Employee list page in Angular](images/angular/employee-list-page.png) -*Figure: Angular employee list page after successful authentication.* - -**Note:** For IdentityServer Admin UI (https://localhost:44303), use: - * **Username:** `admin` - * **Password:** `Pa$$word123` - -### Common Issues - -**IdentityServer won't start** -* **Problem:** Port 44310 already in use -* **Solution:** Kill process using the port or change port in `Properties/launchSettings.json` - -**API returns 401 Unauthorized** -* **Problem:** IdentityServer not running or URL mismatch -* **Solution:** Verify IdentityServer is running at https://localhost:44310 - -**Angular shows "invalid_scope" error** -* **Problem:** Scope mismatch between Angular config and IdentityServer -* **Solution:** Verify `environment.ts` scope matches `identityserverdata.json` - -**Submodules are empty** -* **Problem:** Cloned without `--recurse-submodules` -* **Solution:** Run `git submodule update --init --recursive` - ---- - -## 📦 Component Deep Dive - -### 1. Angular Client (Presentation Tier) - -**Repository:** [TalentManagement-Angular-Material](https://github.com/workcontrolgit/TalentManagement-Angular-Material) - -**Technology Stack:** - -* **Angular 20** — Frontend framework -* **Angular Material 20** — UI component library -* **ng-matero** — Admin dashboard template -* **angular-auth-oidc-client** — OIDC authentication -* **ngx-permissions** — Role-based permissions -* **RxJS 7.x** — Reactive programming -* **TypeScript 5.x** — Type-safe JavaScript -* **Chart.js** — Data visualization - -**Key Features:** - -* **Authentication & Authorization** - * OIDC authentication with automatic token refresh - * HTTP interceptor automatically adds Bearer token to API requests - * Route guards protect authenticated routes - * Role-based UI rendering using ngx-permissions - -* **UI Components** - * Material Design components (buttons, forms, tables, dialogs) - * Responsive layouts (mobile, tablet, desktop) - * Data tables with sorting, filtering, pagination - * Form validation with reactive forms - * Snackbar notifications for user feedback - -* **State Management** - * Service-based state management - * RxJS observables for reactive data flow - * BehaviorSubjects for shared state - -* **Code Organization** - * Feature modules (lazy-loaded for performance) - * Standalone components (Angular 20) - * Shared module for common components - * Core module for singleton services - -**Configuration Example (environment.ts):** - -```typescript -export const environment = { - production: false, - baseUrl: '', - useHash: false, - - // API endpoint - apiUrl: 'https://localhost:44378/api/v1', - - // IdentityServer configuration - identityServerUrl: 'https://localhost:44310', - clientId: 'TalentManagement', - scope: 'openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write', - - allowAnonymousAccess: true, -}; -``` - -**Important configuration points:** -* `apiUrl` must match the running API URL -* `identityServerUrl` must match the running IdentityServer URL -* `clientId` must match a client configured in IdentityServer -* `scope` must be a subset of scopes allowed for the client - -**Build Commands:** - -```bash -# Development server with live reload -npm start - -# Build for production -npm run build - -# Run unit tests -npm test - -# Lint code -npm run lint -``` - ---- - -### 2. API Resource (Business Logic Tier) - -**Repository:** [TalentManagement-API](https://github.com/workcontrolgit/TalentManagement-API) - -**Technology Stack:** - -* **ASP.NET Core 10** — Web API framework -* **Entity Framework Core 10** — ORM for database access -* **AutoMapper** — Object-to-object mapping -* **FluentValidation** — Request validation -* **Swashbuckle** — Swagger/OpenAPI documentation -* **SQL Server 2019+** — Database -* **Serilog** — Structured logging - -**Clean Architecture Layers:** - -``` -Domain/ -├── Entities/ # Domain entities (Employee, Department, Position) -├── Interfaces/ # Repository interfaces -└── Common/ # Base entities, value objects - -Application/ -├── DTOs/ # Data Transfer Objects -├── Mappings/ # AutoMapper profiles -├── Services/ # Business logic services -└── Validators/ # FluentValidation validators - -Infrastructure/ -├── Data/ # EF Core DbContext -├── Repositories/ # Repository implementations -└── Identity/ # Identity integration - -WebApi/ -├── Controllers/ # API endpoints -├── Middleware/ # Exception handling, logging -└── Extensions/ # Service registration -``` - -**Benefits of Clean Architecture:** - -* **Testability** — Each layer can be tested in isolation -* **Maintainability** — Changes in one layer don't affect others -* **Flexibility** — Easy to swap implementations (e.g., change database) -* **Separation of Concerns** — Each layer has a single responsibility - -**API Endpoints (Employees Example):** - -* **GET /api/v1/employees** — Get all employees (requires `read` scope) -* **GET /api/v1/employees/{id}** — Get employee by ID (requires `read` scope) -* **POST /api/v1/employees** — Create employee (requires `write` scope) -* **PUT /api/v1/employees/{id}** — Update employee (requires `write` scope) -* **DELETE /api/v1/employees/{id}** — Delete employee (requires `write` scope) - -**Authentication & Authorization Example:** - -```csharp -[Authorize(Policy = "ApiScope")] -[ApiController] -[Route("api/v1/[controller]")] -public class EmployeesController : ControllerBase -{ - [HttpGet] - [Authorize(Roles = "Admin,Manager")] - public async Task>> GetAll() - { - // Business logic here - } -} -``` - -**Configuration Example (appsettings.json):** - -```json -{ - "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=TalentManagementDb;Trusted_Connection=true;" - }, - - "IdentityServer": { - "Authority": "https://localhost:44310", - "ApiName": "app.api.talentmanagement", - "RequireHttpsMetadata": true - }, - - "Cors": { - "AllowedOrigins": ["http://localhost:4200"] - } -} -``` - -**Important configuration points:** -* `Authority` must match IdentityServer URL -* `ApiName` must match API resource name in IdentityServer -* `AllowedOrigins` must include Angular client URL - -**Build Commands:** - -```bash -# Restore NuGet packages -dotnet restore - -# Build solution -dotnet build - -# Run API -dotnet run --project src/WebApi - -# Apply database migrations -dotnet ef database update --project src/Infrastructure --startup-project src/WebApi - -# Run tests -dotnet test -``` - ---- - -### 3. Token Service (Authentication Tier) - -**Repository:** [Duende-IdentityServer](https://github.com/workcontrolgit/Duende-IdentityServer) - -**Technology Stack:** - -* **Duende IdentityServer 7.0** — OAuth 2.0 / OIDC server -* **ASP.NET Core Identity 10** — User management -* **Entity Framework Core 10** — Data access -* **SQL Server 2019+** — Database -* **Admin UI** — Web-based admin panel - -**OAuth 2.0 Flows Supported:** - -* **Authorization Code Flow with PKCE** — For Single Page Applications (SPAs) -* **Client Credentials Flow** — For service-to-service authentication -* **Resource Owner Password Flow** — For trusted applications -* **Hybrid Flow** — For server-side web applications - -**OpenID Connect Features:** - -* User authentication -* Single Sign-On (SSO) -* ID tokens with user claims -* UserInfo endpoint - -**Token Types:** - -**Access Token:** -* **Purpose:** Grant access to protected resources (APIs) -* **Format:** JWT or reference token -* **Lifetime:** Short (typically 1 hour) -* **Validated by:** Resource server (API) -* **Contains:** Scopes, client ID, user claims - -**ID Token:** -* **Purpose:** Prove user identity to the client -* **Format:** Always JWT -* **Lifetime:** Short (typically 5 minutes) -* **Validated by:** Client application -* **Contains:** User identity claims (name, email, roles) - -**Refresh Token:** -* **Purpose:** Obtain new access tokens without re-authentication -* **Format:** Opaque string (not JWT) -* **Lifetime:** Long (days, weeks, or months) -* **Validated by:** Authorization server (IdentityServer) -* **Contains:** Reference to user session - -**Configuration Example (identityserverdata.json):** - -```json -{ - "Clients": [ - { - "ClientId": "TalentManagement", - "ClientName": "Talent Management Angular App", - "AllowedGrantTypes": ["authorization_code"], - "RequirePkce": true, - "RequireClientSecret": false, - "AllowedScopes": [ - "openid", - "profile", - "email", - "roles", - "app.api.talentmanagement.read", - "app.api.talentmanagement.write" - ], - "RedirectUris": ["http://localhost:4200/callback"], - "PostLogoutRedirectUris": ["http://localhost:4200"], - "AllowedCorsOrigins": ["http://localhost:4200"], - "RequireConsent": false, - "AccessTokenLifetime": 3600, - "AllowOfflineAccess": true - } - ] -} -``` - -**Important configuration points:** - -* **ClientId** — Must match Angular `environment.ts` clientId -* **AllowedScopes** — Must include all scopes requested by Angular -* **RedirectUris** — Must match Angular callback URL -* **AllowedCorsOrigins** — Must include Angular app URL -* **RequirePkce** — Always true for SPAs (security best practice) -* **RequireClientSecret** — Always false for SPAs (can't store secrets) - -**User Seed Data Example (identitydata.json):** - -```json -{ - "Users": [ - { - "Username": "ashtyn1", - "Email": "ashtyn1@example.com", - "Password": "Pa$$word123", - "Roles": ["HRAdmin", "Manager"], - "Claims": [ - { "Type": "name", "Value": "Ashtyn Doe" }, - { "Type": "email", "Value": "ashtyn1@example.com" }, - { "Type": "role", "Value": "HRAdmin" }, - { "Type": "role", "Value": "Manager" } - ] - } - ] -} -``` - -**Build Commands:** - -```bash -# Restore packages -dotnet restore - -# Run IdentityServer -dotnet run --project src/Duende.STS.Identity - -# Run Admin UI -dotnet run --project src/Duende.Admin - -# Apply migrations -dotnet ef database update --project src/Duende.STS.Identity -``` - -**Admin UI Tasks:** - -Access the Admin UI at **https://localhost:44303** to manage: - -* **Clients** — Configure OAuth/OIDC clients -* **API Resources** — Define protected APIs -* **Identity Resources** — Define user claims -* **Users** — Manage user accounts -* **Roles** — Define user roles -* **API Scopes** — Define fine-grained permissions - -![IdentityServer clients configuration](images/identityserver/identityserver-clients-list.png) -*Figure: IdentityServer Clients list used to manage OAuth/OIDC client configuration.* - ---- - -## 💡 Benefits of the CAT Pattern - -### Scalability - -**Independent Deployment** -Deploy client, API, or auth server independently without affecting other components. - -**Horizontal Scaling** -Scale components based on load — scale API for high traffic while keeping IdentityServer lightweight. - -**CDN-Friendly** -Serve static Angular app from CDN for global distribution and faster load times. - -**Database Separation** -Separate databases for identity and application data improves security and performance. - -### Maintainability - -**Clear Boundaries** -Each component has well-defined responsibilities, making code easier to understand and modify. - -**Technology Flexibility** -Replace Angular with React or Vue without touching the API or authentication logic. - -**Team Organization** -Different teams can own different tiers — frontend team, backend team, security team. - -**Git Submodules** -Independent version control for each component allows parallel development. - -### Security - -**Centralized Authentication** -Single source of truth for user identity reduces attack surface. - -**Token-Based Authorization** -Stateless, scalable security model that works across microservices. - -**Scope-Based Access** -Fine-grained API permissions control what each client can access. - -**Security Updates** -Update auth server without affecting client or API code. - -### Testability - -**Unit Testing** -Test each layer in isolation with mock dependencies. - -**Integration Testing** -Test API endpoints with mock tokens without running IdentityServer. - -**E2E Testing** -Test complete flows across all tiers in staging environment. - -**Mocking** -Easy to mock external dependencies like databases and auth providers. - -### Developer Experience - -**Hot Reload** -Angular development server with live reload for instant feedback. - -**Swagger UI** -Interactive API testing without Postman or other tools. - -**TypeScript** -Type safety across frontend catches errors at compile time. - -**Separation of Concerns** -Work on UI without touching backend logic — faster iteration cycles. - -### Enterprise Readiness - -**Multiple Clients** -Support web, mobile, and desktop apps with single backend and auth server. - -**API Gateway Ready** -Easy integration with API gateways like Azure API Management or Kong. - -**Microservices Foundation** -Natural evolution to microservices architecture when needed. - -**Audit & Compliance** -Centralized logging and audit trails for regulatory compliance. - ---- - -## 📖 Tutorial Series Roadmap - -This tutorial is divided into 6 parts: - -### Part 1: Foundation -* **1.1 Understanding the CAT Pattern** (this document) -* **1.2 Setting Up Development Environment** -* **1.3 Running the Complete Stack** - -### Part 2: Token Service Deep Dive -* **2.1 OAuth 2.0 and OpenID Connect Fundamentals** -* **2.2 Duende IdentityServer Configuration** -* **2.3 Securing Your IdentityServer** - -### Part 3: API Resource Deep Dive -* **3.1 Clean Architecture in .NET** -* **3.2 Entity Framework Core** -* **3.3 API Authentication & Authorization** -* **3.4 Building RESTful APIs** - -### Part 4: Angular Client Deep Dive -* **4.1 Angular Application Architecture** -* **4.2 OIDC Authentication in Angular** -* **4.3 Material Design and ng-matero** -* **4.4 Calling Protected APIs** -* **4.5 Role-Based UI with ngx-permissions** - -### Part 5: Advanced Topics -* **5.1 Git Submodules Workflow** -* **5.2 Testing Strategies** -* **5.3 Deployment to Azure** -* **5.4 Monitoring and Logging** -* **5.5 Scaling the CAT Pattern** - -### Part 6: Real-World Features -* **6.1 Employee Management CRUD** -* **6.2 Dashboard with Analytics** -* **6.3 User Profile and Settings** -* **6.4 Advanced Search and Filtering** - ---- - -## 🎓 Next Steps - -### 1. Explore the Running Application - -**Try these actions:** -* Log in with test credentials (`ashtyn1` / `Pa$$word123`) -* Navigate through the dashboard -* View and manage employees -* Access IdentityServer Admin UI with admin credentials (`admin` / `Pa$$word123`) -* Create, edit, and delete an employee -* Check the Swagger UI for API documentation -* Inspect network requests in browser DevTools (note the Bearer token) - -### 2. Make Your First Change - -**Easy starter task: Add a new field to the Employee entity** - -Follow this workflow to understand how changes flow through all three tiers: - -1. Update `Domain/Entities/Employee.cs` (add property) -2. Create EF migration: `dotnet ef migrations add AddNewField` -3. Update database: `dotnet ef database update` -4. Update `Application/DTOs/EmployeeDto.cs` (add property) -5. Update Angular model in `shared/models/employee.model.ts` -6. Update Angular form to include new field -7. Test end-to-end - -### 3. Customize for Your Needs - -**Ideas to explore:** - -* **Change the theme** — Modify Angular Material theme colors -* **Add external login** — Integrate Google or Microsoft authentication -* **Add more entities** — Build out the domain model (Projects, Timesheets) -* **Switch databases** — Try PostgreSQL or MySQL instead of SQL Server -* **Add caching** — Implement Redis for API responses -* **Add email** — Implement email notifications with SendGrid -* **Add file upload** — Implement avatar or document upload - -### 4. Follow the Tutorial Series - -Start with **Part 2: Token Service Deep Dive** to understand: -* How IdentityServer issues tokens -* How the Angular client obtains tokens -* How the API validates tokens -* OAuth 2.0 flows in detail -* PKCE security for SPAs - ---- - -## 🔗 Learning Resources - -### Official Documentation - -* **Angular** — https://angular.dev/ -* **ASP.NET Core** — https://docs.microsoft.com/aspnet/core/ -* **Entity Framework Core** — https://docs.microsoft.com/ef/core/ -* **Duende IdentityServer** — https://docs.duendesoftware.com/identityserver/ -* **Material Design** — https://material.angular.io/ - -### OAuth 2.0 and OIDC - -* **OAuth 2.0** — https://oauth.net/2/ -* **OpenID Connect** — https://openid.net/connect/ -* **OAuth 2.0 Playground** — https://www.oauth.com/playground/ -* **JWT.io** — https://jwt.io/ (decode and inspect tokens) - -### Clean Architecture - -* **Clean Architecture** by Robert C. Martin -* **Domain-Driven Design** by Eric Evans -* **Microsoft Clean Architecture Template** — https://github.com/jasontaylordev/CleanArchitecture - -### Angular Best Practices - -* **Angular Style Guide** — https://angular.dev/style-guide -* **RxJS Best Practices** — https://rxjs.dev/guide/overview - ---- - -## 🎉 Conclusion - -The **CAT Pattern** provides a robust, scalable, and secure foundation for building modern web applications. By separating authentication, business logic, and presentation into distinct tiers, you gain: - -✅ **Security** — Industry-standard OAuth 2.0/OIDC authentication - -✅ **Scalability** — Independent scaling of each component - -✅ **Maintainability** — Clear separation of concerns - -✅ **Flexibility** — Technology-agnostic architecture - -This tutorial gives you a complete, working example to learn from, customize, and deploy to production. - -**Happy coding!** 🚀 - ---- - -## 🔗 Repository & Resources - -**Full source code:** [github.com/workcontrolgit/AngularNetTutorial](https://github.com/workcontrolgit/AngularNetTutorial) - -**Tutorial documentation:** Complete guides in the docs/ folder - -**Component repositories:** -* Angular Client: [TalentManagement-Angular-Material](https://github.com/workcontrolgit/TalentManagement-Angular-Material) -* .NET API: [TalentManagement-API](https://github.com/workcontrolgit/TalentManagement-API) -* IdentityServer: [Duende-IdentityServer](https://github.com/workcontrolgit/Duende-IdentityServer) - ---- - -**Next in series:** [Part 2 — Token Service Deep Dive →](02-token-service-deep-dive.md) - ---- - -*This tutorial series covers building production-ready applications with Angular 20, .NET 10, and Duende IdentityServer 7.0. Perfect for full-stack developers, architects, and teams building secure enterprise applications.* - - - diff --git a/docs/02-token-service-deep-dive.md b/docs/02-token-service-deep-dive.md deleted file mode 100644 index 84e57ba..0000000 --- a/docs/02-token-service-deep-dive.md +++ /dev/null @@ -1,655 +0,0 @@ -# Part 2: Token Service Deep Dive — Understanding OAuth 2.0, OpenID Connect, and Duende IdentityServer - -## Building Modern Web Applications with Angular, .NET, and OAuth 2.0 - -**[← Part 1: Foundation](01-foundation.md)** | **[Tutorial Home](TUTORIAL.md)** | **[Part 3: API Resource Deep Dive →](03-api-resource-deep-dive.md)** - ---- - -## 🔐 Introduction - -The **Token Service** is the security heart of the CAT pattern. It's responsible for: - -* **Authenticating users** — Verifying username/password credentials -* **Issuing tokens** — Providing access tokens and ID tokens to clients -* **Managing sessions** — Handling single sign-on (SSO) and logout -* **Protecting resources** — Defining what APIs clients can access -* **User management** — Storing user accounts, passwords, and profiles - -In this tutorial, we use **Duende IdentityServer 7.0**, which is a certified implementation of OAuth 2.0 and OpenID Connect (OIDC). Understanding these standards is crucial to securing your application properly. - -### Why Do We Need a Token Service? - -![IdentityServer Admin dashboard](images/identityserver/identityserver-admin-dashboard.png) -*Figure: IdentityServer Admin dashboard for token service operations.* - -Without a dedicated token service, you'd have to: - -* Store user credentials in every application -* Implement authentication logic in every client -* Manage password policies across multiple systems -* Handle token generation and validation manually -* Build your own user management UI - -A token service centralizes all of this, providing: - -✅ **Single Source of Truth** for user identity -✅ **Standardized Security** using OAuth 2.0/OIDC -✅ **Scalability** — Multiple clients can share one auth server -✅ **Compliance** — Easier to audit and secure -✅ **User Experience** — Single sign-on across applications - ---- - -## 📚 OAuth 2.0 Fundamentals - -### What is OAuth 2.0? - -**OAuth 2.0** is an **authorization framework** that allows applications to obtain limited access to user accounts on an HTTP service. It's NOT an authentication protocol (that's OIDC, which builds on OAuth 2.0). - -### Key Concepts - -#### 1. The Four Roles - -![Angular login entry](images/angular/angular-login-page.png) -*Figure: Angular client initiates OAuth flow from the top-right Login action.* - -OAuth 2.0 defines four key roles in the authentication and authorization process: - -* **Resource Owner** — The user who owns the data (in our app: the end user logging in) -* **Client** — Application requesting access (in our app: Angular SPA) -* **Authorization Server** — Issues tokens after authenticating user (in our app: IdentityServer) -* **Resource Server** — Hosts protected resources (in our app: .NET Web API) - -#### 2. Grant Types (Authorization Flows) - -OAuth 2.0 defines several "grant types" - ways for clients to obtain access tokens. Our application uses the **Authorization Code Flow with PKCE**. - -##### Authorization Code Flow with PKCE - -**PKCE** (Proof Key for Code Exchange, pronounced "pixie") is essential for Single Page Applications (SPAs) because they can't securely store secrets. - -**The Flow:** - -**Step 1:** Angular SPA generates a random `code_verifier` and creates `code_challenge` (hash of verifier) - -**Step 2:** Browser redirects to IdentityServer's `/authorize` endpoint with: -* `response_type=code` -* `client_id=TalentManagement` -* `redirect_uri=http://localhost:4200/callback` -* `code_challenge=abc123...` -* `code_challenge_method=S256` - -**Step 3:** IdentityServer shows login form, user enters credentials - -![IdentityServer login page](images/angular/identityserver-login-ashtyn1.png) -*Figure: IdentityServer login step in Authorization Code Flow with PKCE.* - -**Step 4:** IdentityServer redirects back to callback URL with authorization code: -* `http://localhost:4200/callback?code=xyz123` - -**Step 5:** Angular makes POST request to `/token` endpoint with: -* `grant_type=authorization_code` -* `code=xyz123` -* `code_verifier=original_random_string` -* `redirect_uri=http://localhost:4200/callback` - -**Step 6:** IdentityServer verifies that `hash(code_verifier)` matches the original `code_challenge` - -**Step 7:** IdentityServer returns tokens: -```json -{ - "access_token": "eyJhbGc...", - "id_token": "eyJhbGc...", - "refresh_token": "abc123..." -} -``` - -**Why PKCE?** - -Without PKCE, an attacker could intercept the authorization code and exchange it for tokens. PKCE prevents this by requiring the client to prove it's the same application that started the flow. - -#### 3. Scopes - -**Scopes** define what permissions the client is requesting. Think of them as "permissions" or "access levels." - -**Types of Scopes:** - -* **Identity Scopes** — User information (examples: `openid`, `profile`, `email`) -* **API Scopes** — API permissions (examples: `api.read`, `api.write`) - -**In Our Application:** - -``` -openid - Required for OIDC -profile - Access to user's profile (name, etc.) -email - Access to user's email -roles - Access to user's roles -app.api.talentmanagement.read - Read data from API -app.api.talentmanagement.write - Modify data via API -``` - -#### 4. Consent - -**Consent** is when the user explicitly grants permission to the client application. In our app, we've disabled consent (`RequireConsent: false`) for a smoother user experience since we control both the client and the server. - -In a third-party scenario (like "Sign in with Google"), users would see a consent screen listing the requested permissions. - ---- - -## 🔑 OpenID Connect (OIDC) - -### What is OpenID Connect? - -**OpenID Connect** is an **authentication layer** built on top of OAuth 2.0. While OAuth 2.0 handles authorization (what you can access), OIDC handles authentication (who you are). - -### OAuth 2.0 vs OIDC - -![IdentityServer logout confirmation](images/angular/identityserver-logout-intermediate.png) -*Figure: IdentityServer end-session confirmation page used in logout flow.* - -Understanding the difference between OAuth 2.0 and OIDC: - -**OAuth 2.0:** -* **Purpose:** Authorization -* **Question Answered:** "What can I do?" -* **Primary Token:** Access Token -* **Use Case:** Grant API access -* **User Info:** Not standardized - -**OpenID Connect (OIDC):** -* **Purpose:** Authentication -* **Question Answered:** "Who am I?" -* **Primary Token:** ID Token -* **Use Case:** Verify user identity -* **User Info:** Standardized claims - -### Key OIDC Concepts - -#### 1. ID Token - -An **ID Token** is a JWT (JSON Web Token) that contains information about the authenticated user. It's proof that the user successfully logged in. - -**Example ID Token (decoded):** - -```json -{ - "header": { - "alg": "RS256", - "kid": "abc123", - "typ": "JWT" - }, - "payload": { - "sub": "248289761001", // Subject - unique user ID - "name": "Alice Smith", - "given_name": "Alice", - "family_name": "Smith", - "email": "alice@example.com", - "email_verified": true, - "role": ["Admin", "Manager"], - "iss": "https://localhost:44310", // Issuer - who issued the token - "aud": "TalentManagement", // Audience - intended recipient - "iat": 1675000000, // Issued at (timestamp) - "exp": 1675003600, // Expires at (timestamp) - "auth_time": 1675000000, // When authentication occurred - "amr": ["pwd"] // Authentication method (password) - }, - "signature": "..." -} -``` - -**Key Claims:** - -* `sub` (subject) — Unique identifier for the user (never changes) -* `iss` (issuer) — URL of the IdentityServer that issued the token -* `aud` (audience) — Client ID this token is intended for -* `exp` (expiration) — Token expiration timestamp -* `iat` (issued at) — When the token was issued - -#### 2. UserInfo Endpoint - -The **UserInfo endpoint** (`/connect/userinfo`) returns additional claims about the authenticated user. The client calls this endpoint with an access token to get user details. - -```http -GET /connect/userinfo HTTP/1.1 -Host: localhost:44310 -Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI... - -Response: -{ - "sub": "248289761001", - "name": "Alice Smith", - "email": "alice@example.com", - "role": ["Admin"] -} -``` - -#### 3. Standard Claims - -OIDC defines standard claims for user information: - -* **`sub`** — Subject identifier (example: "248289761001") -* **`name`** — Full name (example: "Alice Smith") -* **`given_name`** — First name (example: "Alice") -* **`family_name`** — Last name (example: "Smith") -* **`email`** — Email address (example: "alice@example.com") -* **`email_verified`** — Email verified? (example: true) -* **`picture`** — Profile picture URL (example: "https://...") -* **`phone_number`** — Phone number (example: "+1-555-1234") -* **`address`** — Address (example: { "formatted": "123 Main St..." }) -* **`birthdate`** — Date of birth (example: "1990-01-01") -* **`locale`** — Locale/language (example: "en-US") -* **`zoneinfo`** — Time zone (example: "America/New_York") - ---- - -## 🎫 Understanding Tokens - -### Token Types - -#### 1. Access Token - -* **Purpose:** Grant access to protected resources (APIs) -* **Format:** JWT or reference token -* **Lifetime:** Short (typically 1 hour) -* **Validated by:** Resource server (API) -* **Contains:** Scopes, client ID, expiration - -**Example Access Token (JWT, decoded):** - -```json -{ - "header": { - "alg": "RS256", - "kid": "abc123", - "typ": "at+jwt" - }, - "payload": { - "iss": "https://localhost:44310", - "aud": "app.api.talentmanagement", - "client_id": "TalentManagement", - "sub": "248289761001", - "scope": [ - "app.api.talentmanagement.read", - "app.api.talentmanagement.write", - "openid", - "profile", - "email" - ], - "role": ["Admin"], - "name": "Alice Smith", - "email": "alice@example.com", - "iat": 1675000000, - "exp": 1675003600, - "nbf": 1675000000 - }, - "signature": "..." -} -``` - -#### 2. ID Token - -* **Purpose:** Prove user identity to the client -* **Format:** Always JWT -* **Lifetime:** Short (typically 5 minutes) -* **Validated by:** Client application -* **Contains:** User identity claims - -#### 3. Refresh Token - -* **Purpose:** Obtain new access tokens without re-authentication -* **Format:** Opaque string (not JWT) -* **Lifetime:** Long (days, weeks, or months) -* **Validated by:** Authorization server (IdentityServer) -* **Contains:** Reference to user session - -### JWT Structure - -A **JWT (JSON Web Token)** has three parts separated by dots: - -``` -eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c - -[ Header ] . [ Payload ] . [ Signature ] -``` - -#### Header - -```json -{ - "alg": "RS256", // Algorithm (RSA with SHA-256) - "kid": "123", // Key ID (identifies which signing key was used) - "typ": "JWT" // Type -} -``` - -#### Payload - -```json -{ - "sub": "1234567890", - "name": "Alice", - "role": "Admin", - "exp": 1675003600 -} -``` - -#### Signature - -The signature is created by: - -1. Taking the encoded header and payload -2. Concatenating with a dot: `encodedHeader.encodedPayload` -3. Signing with a private key using the algorithm specified in the header -4. Base64 encoding the result - ---- - -## 🏗️ Duende IdentityServer Overview - -### What is Duende IdentityServer? - -**Duende IdentityServer** is a .NET-based framework for implementing OAuth 2.0 and OpenID Connect. It's the successor to IdentityServer4 (now deprecated). - -**Key Features:** - -✅ Certified OpenID Connect implementation -✅ Supports all OAuth 2.0 flows -✅ Built on ASP.NET Core Identity for user management -✅ Extensible and customizable -✅ Production-ready with enterprise support -✅ Admin UI for managing clients, users, and resources - -### Duende IdentityServer Components - -Our IdentityServer setup consists of three main components: - -**STS.Identity (Port 44310):** -* Login UI and authentication flows -* OAuth 2.0 and OIDC endpoints -* Token issuance and validation -* UserInfo endpoint - - - -![IdentityServer clients list](images/identityserver/identityserver-clients-list.png) -*Figure: IdentityServer Clients list for OAuth/OIDC client management.* -* Configure API resources and scopes - - - -![IdentityServer Admin API Swagger](images/identityserver/identityserver-swagger-ui.png) -*Figure: Swagger UI for IdentityServer Admin API diagnostics and testing.* - ---- - -## ⚙️ Configuration Deep Dive - -### Configuration Files - -#### identityserverdata.json - Clients and Resources - -![TalentManagement client URL settings](images/identityserver/identityserver-client-talentmanagement-urls-tab.png) -*Figure: TalentManagement client URL configuration (redirect and logout URLs).* - -This file defines: -* **Identity Resources** — User information scopes -* **API Scopes** — Permissions for APIs -* **API Resources** — Protected APIs -* **Clients** — Applications that can request tokens - -**Full Configuration Example:** - -```json -{ - "IdentityServerData": { - "IdentityResources": [ - { - "Name": "openid", - "Enabled": true, - "Required": true, - "DisplayName": "Your user identifier", - "UserClaims": ["sub"] - }, - { - "Name": "profile", - "Enabled": true, - "DisplayName": "User profile", - "UserClaims": ["name", "family_name", "given_name"] - }, - { - "Name": "email", - "Enabled": true, - "DisplayName": "Your email address", - "UserClaims": ["email", "email_verified"] - }, - { - "Name": "roles", - "Enabled": true, - "DisplayName": "Roles", - "UserClaims": ["role"] - } - ], - "ApiScopes": [ - { - "Name": "app.api.talentmanagement.read", - "DisplayName": "Read access to Talent Management API", - "UserClaims": ["role", "name"] - } - ], - "ApiResources": [ - { - "Name": "app.api.talentmanagement", - "Scopes": ["app.api.talentmanagement.read"] - } - ], - "Clients": [ - { - "ClientId": "TalentManagement", - "ClientName": "Talent Management Angular App", - "AllowedGrantTypes": ["authorization_code"], - "RequirePkce": true, - "RequireClientSecret": false, - "AllowedScopes": ["openid", "profile", "email", "roles"], - "RedirectUris": ["http://localhost:4200/callback"], - "PostLogoutRedirectUris": ["http://localhost:4200"], - "AllowedCorsOrigins": ["http://localhost:4200"], - "RequireConsent": false, - "AccessTokenLifetime": 3600 - } - ] - } -} -``` - ---- - -## 🔒 Security Best Practices - -### 1. Always Use HTTPS in Production - -Tokens are sent in HTTP headers. Without HTTPS, they can be intercepted. - -**Development:** `http://localhost:4200` (acceptable) -**Production:** `https://yourdomain.com` (required) - -### 2. Use PKCE for SPAs - -![Advanced grant types settings](images/identityserver/identityserver-client-talentmanagement-advanced-grant-types.png) -*Figure: Advanced Grant Types settings for controlling OAuth flows per client.* - -SPAs can't securely store client secrets. PKCE prevents authorization code interception attacks. - -Always set: -* `RequirePkce: true` -* `RequireClientSecret: false` (for public clients like SPAs) - -### 3. Short Token Lifetimes - -![Advanced token settings](images/identityserver/identityserver-client-talentmanagement-advanced-token.png) -*Figure: Advanced Token settings for client token/session lifetime tuning.* - -**Recommended token lifetimes:** - -* **Access Token:** 1 hour — Frequently used, higher risk if compromised -* **ID Token:** 5 minutes — Only needed at login time -* **Refresh Token:** 30 days — Longer is acceptable since it's managed server-side - -### 4. Validate Redirect URIs - -Always specify exact redirect URIs to prevent open redirect attacks where an attacker could steal the authorization code. - -**Do:** -```json -"RedirectUris": ["http://localhost:4200/callback"] -``` - -**Don't:** -```json -"RedirectUris": ["http://localhost:4200/*"] // Too permissive -``` - ---- - -## 🧪 Testing and Debugging - -### Test /.well-known/openid-configuration - -This endpoint exposes IdentityServer's metadata: - -```bash -curl https://localhost:44310/.well-known/openid-configuration -``` - -**Returns:** -* Supported scopes -* Token endpoint URLs -* Supported grant types -* Available algorithms -* JWKS (JSON Web Key Set) endpoint - -**Use this to verify:** -* IdentityServer is running correctly -* Endpoints are accessible -* Configuration is as expected - ---- - -## ⚠️ Common Issues and Solutions - -### Issue: "invalid_scope" error - -**Problem:** Client requests a scope that's not in the `AllowedScopes` configuration - -**Symptoms:** -* Error during login -* Message: "invalid_scope" -* Login flow stops - -**Solution:** -1. Check `identityserverdata.json` client configuration -2. Verify `AllowedScopes` array includes all requested scopes -3. Restart IdentityServer after configuration changes - -**Example fix:** -```json -{ - "ClientId": "TalentManagement", - "AllowedScopes": [ - "openid", - "profile", - "email", - "roles", - "app.api.talentmanagement.read" // Make sure this matches what Angular requests - ] -} -``` - ---- - -### Issue: CORS errors in browser - -**Problem:** Angular origin not in `AllowedCorsOrigins` - -**Symptoms:** -* Browser console shows CORS errors -* Requests to IdentityServer fail -* Message: "Access to fetch... has been blocked by CORS policy" - -**Solution:** -Add Angular URL to `AllowedCorsOrigins` array: - -```json -{ - "ClientId": "TalentManagement", - "AllowedCorsOrigins": [ - "http://localhost:4200" - ] -} -``` - -**Important:** The URL must match exactly (including protocol and port) - ---- - -### Issue: Token expired - -**Problem:** Access token lifetime too short for user workflow - -**Symptoms:** -* API returns 401 Unauthorized -* User has to log in frequently -* Token works initially, then stops - -**Solution:** -Adjust `AccessTokenLifetime` in client configuration: - -```json -{ - "ClientId": "TalentManagement", - "AccessTokenLifetime": 3600 // 1 hour in seconds -} -``` - -**Note:** Use refresh tokens for longer sessions rather than extending access token lifetime excessively. - ---- - -## 📝 Summary - -In this deep dive, we covered: - -✅ **OAuth 2.0 Fundamentals** — Authorization framework, grant types, scopes -✅ **OpenID Connect** — Authentication layer, ID tokens, UserInfo endpoint -✅ **Tokens** — Access tokens, ID tokens, refresh tokens, JWT structure -✅ **Duende IdentityServer** — Architecture, project structure -✅ **Configuration** — Clients, resources, scopes, users -✅ **Security Best Practices** — HTTPS, PKCE, token lifetimes -✅ **Testing and Debugging** — Endpoint testing, common issues - -### Key Takeaways - -**OAuth 2.0 vs OIDC:** -* OAuth 2.0 = Authorization ("What can I do?") -* OIDC = Authentication ("Who am I?") - -**Token Types:** -* Access Token = API access -* ID Token = User identity -* Refresh Token = Long-lived session - -**Security Essentials:** -* Always use PKCE for SPAs -* Keep token lifetimes short -* Validate redirect URIs -* Use HTTPS in production - ---- - -**Next in series:** [Part 3 — API Resource Deep Dive →](03-api-resource-deep-dive.md) - -**Previous:** [← Part 1: Foundation — Understanding the CAT Pattern](01-foundation.md) - -**Tutorial Home:** [📚 Complete Tutorial Series](TUTORIAL.md) - - diff --git a/docs/03-api-resource-deep-dive.md b/docs/03-api-resource-deep-dive.md deleted file mode 100644 index eda65e4..0000000 --- a/docs/03-api-resource-deep-dive.md +++ /dev/null @@ -1,764 +0,0 @@ -# Part 3: API Resource Deep Dive — Clean Architecture, Entity Framework Core, and RESTful Design - -## Building Modern Web Applications with Angular, .NET, and OAuth 2.0 - -**[← Part 2: Token Service](02-token-service-deep-dive.md)** | **[Tutorial Home](TUTORIAL.md)** | **[Part 4: Angular Client Deep Dive →](04-angular-client-deep-dive.md)** - ---- - -## 🏗️ Introduction - -The **API Resource** is the business logic heart of the CAT pattern. It's responsible for: - -* **Managing business logic** — Implementing domain rules and workflows -* **Data persistence** — Storing and retrieving data using Entity Framework Core -* **Protecting resources** — Validating access tokens from IdentityServer -* **Exposing RESTful endpoints** — Providing structured API access with versioning -* **Enforcing authorization** — Controlling who can access what data - -Our **TalentManagement API** uses **.NET 10 Web API** with **Clean Architecture**, which provides clear separation of concerns and makes the codebase maintainable and testable. - -### Why Clean Architecture? - - -Without Clean Architecture, you'd have: - -* Business logic mixed with data access code -* Difficulty testing components in isolation -* Tight coupling between layers -* Hard-to-maintain monolithic code -* Challenges when changing databases or frameworks - -Clean Architecture provides: - -✅ **Separation of Concerns** — Each layer has a single responsibility -✅ **Testability** — Easy to unit test business logic -✅ **Flexibility** — Swap infrastructure without affecting business logic -✅ **Maintainability** — Clear structure that scales with team size -✅ **Domain-Driven Design** — Focus on business domain first - ---- - -## 📚 Clean Architecture Fundamentals - -### What is Clean Architecture? - -**Clean Architecture** (by Robert C. Martin) organizes code in concentric layers where dependencies point inward. The core business logic has no dependencies on external frameworks or databases. - -### The Four Layers - -**Domain Layer (Core):** -* **Purpose:** Business entities, value objects, and core logic -* **Dependencies:** None (pure business logic) -* **Contains:** Entities, value objects, enums, domain exceptions -* **Example:** `Employee` entity, `PersonName` value object, `Gender` enum - -**Application Layer:** -* **Purpose:** Use cases and application logic -* **Dependencies:** Domain layer only -* **Contains:** Commands, queries, interfaces, DTOs, validators -* **Example:** `CreateEmployeeCommand`, `GetEmployeesQuery` - -**Infrastructure Layer:** -* **Purpose:** External concerns (database, file system, APIs) -* **Dependencies:** Domain and Application layers -* **Contains:** EF Core implementations, repositories, external services -* **Includes:** `Infrastructure.Persistence`, `Infrastructure.Shared` - -**WebApi Layer (Presentation):** -* **Purpose:** HTTP endpoints and API contracts -* **Dependencies:** Application layer -* **Contains:** Controllers, filters, middleware, startup configuration -* **Example:** `EmployeesController`, API versioning - ---- - -## 🎯 Project Structure - -### Solution Organization - -``` -TalentManagement-API/ -├── TalentManagementAPI.Domain/ -│ ├── Common/ -│ │ ├── BaseEntity.cs -│ │ └── AuditableBaseEntity.cs -│ ├── Entities/ -│ │ ├── Employee.cs -│ │ ├── Department.cs -│ │ └── Position.cs -│ ├── Enums/ -│ │ └── Gender.cs -│ └── ValueObjects/ -│ └── PersonName.cs -│ -├── TalentManagementAPI.Application/ -│ ├── Features/ -│ │ └── Employees/ -│ │ ├── Commands/ -│ │ │ ├── CreateEmployee/ -│ │ │ │ ├── CreateEmployeeCommand.cs -│ │ │ │ └── CreateEmployeeCommandValidator.cs -│ │ │ ├── UpdateEmployee/ -│ │ │ └── DeleteEmployeeById/ -│ │ └── Queries/ -│ │ ├── GetEmployees/ -│ │ │ ├── GetEmployeesQuery.cs -│ │ │ └── GetEmployeesViewModel.cs -│ │ └── GetEmployeeById/ -│ ├── Interfaces/ -│ │ └── Repositories/ -│ │ ├── IGenericRepositoryAsync.cs -│ │ └── IEmployeeRepositoryAsync.cs -│ ├── Events/ -│ │ └── EmployeeChangedEvent.cs -│ └── Wrappers/ -│ ├── Result.cs -│ └── PagedResult.cs -│ -├── TalentManagementAPI.Infrastructure.Persistence/ -│ ├── Context/ -│ │ └── ApplicationDbContext.cs -│ ├── Repositories/ -│ │ ├── GenericRepositoryAsync.cs -│ │ └── EmployeeRepositoryAsync.cs -│ └── Seeds/ -│ └── DefaultData.cs -│ -├── TalentManagementAPI.Infrastructure.Shared/ -│ ├── Services/ -│ │ ├── DateTimeService.cs -│ │ └── EventDispatcher.cs -│ └── Mock/ -│ └── EmployeeBogusConfig.cs -│ -└── TalentManagementAPI.WebApi/ - ├── Controllers/ - │ └── v1/ - │ ├── BaseApiController.cs - │ └── EmployeesController.cs - ├── Program.cs - └── appsettings.json -``` - ---- - -## 🗃️ Domain Layer - -### Base Entities - -**BaseEntity.cs:** - -```csharp -namespace TalentManagementAPI.Domain.Common -{ - /// - /// Base class for all entities that have an ID property. - /// - public abstract class BaseEntity - { - /// - /// Unique identifier for this entity. - /// - public virtual Guid Id { get; set; } - } -} -``` - -**AuditableBaseEntity.cs:** - -```csharp -namespace TalentManagementAPI.Domain.Common -{ - // Abstract base class for entities that support auditing - public abstract class AuditableBaseEntity : BaseEntity - { - // The username of the user who created this entity - public string CreatedBy { get; set; } - // The timestamp when this entity was created - public DateTime Created { get; set; } - // The username of the user who last modified this entity - public string LastModifiedBy { get; set; } - // The timestamp when this entity was last modified - public DateTime? LastModified { get; set; } - } -} -``` - -### Value Objects - -**PersonName.cs:** - -```csharp -namespace TalentManagementAPI.Domain.ValueObjects -{ - public sealed class PersonName - { - public string FirstName { get; private set; } - public string MiddleName { get; private set; } - public string LastName { get; private set; } - - private PersonName() - { - } - - public PersonName(string firstName, string middleName, string lastName) - { - FirstName = Normalize(firstName); - MiddleName = string.IsNullOrWhiteSpace(middleName) ? null : Normalize(middleName); - LastName = Normalize(lastName); - - if (FirstName.Length == 0 || LastName.Length == 0) - { - throw new ArgumentException("First and last name are required."); - } - - if (FirstName.Length > 100 || LastName.Length > 100 || (MiddleName?.Length ?? 0) > 100) - { - throw new ArgumentException("Name parts must not exceed 100 characters."); - } - } - - public string FullName => - string.IsNullOrWhiteSpace(MiddleName) - ? $"{FirstName} {LastName}" - : $"{FirstName} {MiddleName} {LastName}"; - - public static string Normalize(string value) - { - var trimmed = (value ?? string.Empty).Trim(); - return string.Join(' ', trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries)); - } - } -} -``` - -### Entities - -**Employee.cs:** - -```csharp -using TalentManagementAPI.Domain.ValueObjects; - -namespace TalentManagementAPI.Domain.Entities -{ - public class Employee : AuditableBaseEntity - { - // Value Object for Name - public PersonName Name { get; set; } - - // Computed properties (not mapped to database) - [NotMapped] - public string FirstName => Name?.FirstName; - [NotMapped] - public string MiddleName => Name?.MiddleName; - [NotMapped] - public string LastName => Name?.LastName; - [NotMapped] - public string FullName => Name?.FullName; - - // Foreign Key for Position - public Guid PositionId { get; set; } - // Navigation Property for Position - public virtual Position Position { get; set; } - - // Foreign Key for Home Department - public Guid DepartmentId { get; set; } - // Navigation Property for Home Department - public virtual Department Department { get; set; } - - // Salary of the Employee - public decimal Salary { get; set; } - - public DateTime Birthday { get; set; } - public string Email { get; set; } - public Gender Gender { get; set; } - public string EmployeeNumber { get; set; } - public string Prefix { get; set; } - public string Phone { get; set; } - } -} -``` - -### Enums - -**Gender.cs:** - -```csharp -namespace TalentManagementAPI.Domain.Enums -{ - /// - /// Represents the gender of a person. - /// - public enum Gender - { - /// - /// Indicates that the person is male. - /// - Male, - - /// - /// Indicates that the person is female. - /// - Female - } -} -``` - ---- - -## 💼 Application Layer - -### CQRS Pattern with Custom Mediator - -The Application layer uses **CQRS (Command Query Responsibility Segregation)** to separate reads from writes, implemented with a **custom mediator pattern** (not the MediatR library). - -**Key Components:** -* **Custom Mediator** — `TalentManagementAPI.Application.Messaging.IMediator` (custom implementation) -* **Commands** — Modify state (Create, Update, Delete) -* **Queries** — Retrieve data (Read) -* **Result** — Standardized response wrapper -* **Pipeline Behaviors** — Validation, logging, caching decorators - -### Command Example: CreateEmployee - -**CreateEmployeeCommand.cs:** - -```csharp -using TalentManagementAPI.Application.Messaging; -using TalentManagementAPI.Application.Events; - -namespace TalentManagementAPI.Application.Features.Employees.Commands.CreateEmployee -{ - /// - /// Command to create a new employee. - /// - public class CreateEmployeeCommand : IRequest> - { - public string FirstName { get; set; } - public string MiddleName { get; set; } - public string LastName { get; set; } - public Guid PositionId { get; set; } - public Guid DepartmentId { get; set; } - public decimal Salary { get; set; } - public DateTime Birthday { get; set; } - public string Email { get; set; } - public Gender Gender { get; set; } - public string EmployeeNumber { get; set; } - public string Prefix { get; set; } - public string Phone { get; set; } - - public class CreateEmployeeCommandHandler : IRequestHandler> - { - private readonly IEmployeeRepositoryAsync _repository; - private readonly IMapper _mapper; - private readonly IEventDispatcher _eventDispatcher; - - public CreateEmployeeCommandHandler( - IEmployeeRepositoryAsync repository, - IMapper mapper, - IEventDispatcher eventDispatcher) - { - _repository = repository; - _mapper = mapper; - _eventDispatcher = eventDispatcher; - } - - public async Task> Handle(CreateEmployeeCommand request, CancellationToken cancellationToken) - { - var employee = _mapper.Map(request); - await _repository.AddAsync(employee); - await _eventDispatcher.PublishAsync(new EmployeeChangedEvent(employee.Id), cancellationToken); - return Result.Success(employee.Id); - } - } - } -} -``` - -**Key Features:** -- Command handler **nested inside command class** -- Uses **Repository pattern** (`IEmployeeRepositoryAsync`) -- Uses **AutoMapper** for DTO-to-Entity mapping -- Publishes **domain events** via `IEventDispatcher` -- Returns `Result` wrapper (not bare Guid) - -**CreateEmployeeCommandValidator.cs:** - -```csharp -using FluentValidation; - -namespace TalentManagementAPI.Application.Features.Employees.Commands.CreateEmployee -{ - public class CreateEmployeeCommandValidator : AbstractValidator - { - public CreateEmployeeCommandValidator() - { - RuleFor(v => v.FirstName) - .NotEmpty().WithMessage("First name is required.") - .MaximumLength(100).WithMessage("First name must not exceed 100 characters."); - - RuleFor(v => v.LastName) - .NotEmpty().WithMessage("Last name is required.") - .MaximumLength(100).WithMessage("Last name must not exceed 100 characters."); - - RuleFor(v => v.Email) - .NotEmpty().WithMessage("Email is required.") - .EmailAddress().WithMessage("Email must be valid."); - - RuleFor(v => v.Birthday) - .LessThan(DateTime.UtcNow.AddYears(-18)) - .WithMessage("Employee must be at least 18 years old."); - - RuleFor(v => v.Salary) - .GreaterThan(0).WithMessage("Salary must be greater than zero."); - } - } -} -``` - -### Query Example: GetEmployees - -**GetEmployeesQuery.cs:** - -```csharp -namespace TalentManagementAPI.Application.Features.Employees.Queries.GetEmployees -{ - /// - /// GetAllEmployeesQuery - handles media IRequest - /// QueryParameter - contains paging parameters - /// To add filter/search parameters, add search properties to the body of this class - /// - public class GetEmployeesQuery : QueryParameter, IRequest>> - { - public string LastName { get; set; } - public string FirstName { get; set; } - public string Email { get; set; } - public string EmployeeNumber { get; set; } - public string PositionTitle { get; set; } - - public ListParameter ShapeParameter { get; set; } - } - - public class GetAllEmployeesQueryHandler : IRequestHandler>> - { - private readonly IEmployeeRepositoryAsync _repository; - private readonly IModelHelper _modelHelper; - - public GetAllEmployeesQueryHandler(IEmployeeRepositoryAsync employeeRepository, IModelHelper modelHelper) - { - _repository = employeeRepository; - _modelHelper = modelHelper; - } - - public async Task>> Handle(GetEmployeesQuery request, CancellationToken cancellationToken) - { - var objRequest = request; - - //filtered fields security - if (!string.IsNullOrEmpty(objRequest.Fields)) - { - //limit to fields in view model - objRequest.Fields = _modelHelper.ValidateModelFields(objRequest.Fields); - } - else - { - //default fields from view model - objRequest.Fields = _modelHelper.GetModelFields(); - } - - // query based on filter - var qryResult = await _repository.GetEmployeeResponseAsync(objRequest); - var data = qryResult.data; - RecordsCount recordCount = qryResult.recordsCount; - - // response wrapper - return PagedResult>.Success(data, objRequest.PageNumber, objRequest.PageSize, recordCount); - } - } -} -``` - -**Key Features:** -- Inherits from `QueryParameter` (includes PageNumber, PageSize, OrderBy) -- Returns `PagedResult>` with pagination metadata -- Uses **field shaping** (dynamic field selection) -- Uses **IModelHelper** for security (validates requested fields) -- Returns `Entity` (dynamic ExpandoObject) not strongly-typed DTO - ---- - -## 🗄️ Infrastructure Layer - -### Repository Pattern - -**IGenericRepositoryAsync.cs:** - -```csharp -namespace TalentManagementAPI.Application.Interfaces.Repositories -{ - public interface IGenericRepositoryAsync where T : class - { - Task GetByIdAsync(Guid id); - Task> GetAllAsync(); - Task AddAsync(T entity); - Task UpdateAsync(T entity); - Task DeleteAsync(T entity); - } -} -``` - -**IEmployeeRepositoryAsync.cs:** - -```csharp -namespace TalentManagementAPI.Application.Interfaces.Repositories -{ - /// - /// Interface for retrieving paged employee response asynchronously. - /// - public interface IEmployeeRepositoryAsync : IGenericRepositoryAsync - { - /// - /// Retrieves a list of employees based on the provided query parameters asynchronously. - /// - /// The request parameters. - /// A tuple containing the list of employees and the total number of records. - Task<(IEnumerable data, RecordsCount recordsCount)> GetEmployeeResponseAsync(GetEmployeesQuery requestParameters); - } -} -``` - -**Key Features:** -- Generic repository base interface -- Returns tuple with data and record count -- Returns dynamic `Entity` (ExpandoObject) for field shaping - ---- - -## 🌐 WebApi Layer - -### Base API Controller - -**BaseApiController.cs:** - -```csharp -namespace TalentManagementAPI.WebApi.Controllers -{ - [ApiController] - [Route("api/v{version:apiVersion}/[controller]")] - public abstract class BaseApiController : ControllerBase - { - private IMediator _mediator; - protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService(); - } -} -``` - -### Employees Controller - - -**EmployeesController.cs:** - -```csharp -namespace TalentManagementAPI.WebApi.Controllers.v1 -{ - [ApiVersion("1.0")] - public class EmployeesController : BaseApiController - { - /// - /// Gets a list of employees based on the specified filter. - /// - /// The filter used to get the list of employees. - /// A list of employees. - [HttpGet] - [AllowAnonymous] - public async Task Get([FromQuery] GetEmployeesQuery filter) - { - return Ok(await Mediator.Send(filter)); - } - - /// - /// Gets an employee by identifier. - /// - /// Employee identifier. - [HttpGet("{id}")] - [AllowAnonymous] - public async Task Get(Guid id) - { - return Ok(await Mediator.Send(new GetEmployeeByIdQuery { Id = id })); - } - - /// - /// Creates a new employee. - /// - /// Employee payload. - [HttpPost] - [Authorize] - [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task Post(CreateEmployeeCommand command) - { - var result = await Mediator.Send(command); - return CreatedAtAction(nameof(Get), new { id = result.Value }, result); - } - - /// - /// Updates an existing employee. - /// - /// Employee identifier. - /// Update payload. - [HttpPut("{id}")] - [Authorize] - public async Task Put(Guid id, UpdateEmployeeCommand command) - { - if (id != command.Id) - { - return BadRequest(); - } - - return Ok(await Mediator.Send(command)); - } - - /// - /// Deletes an employee by identifier. - /// - /// Employee identifier. - [HttpDelete("{id}")] - [Authorize(Policy = AuthorizationConsts.AdminPolicy)] - public async Task Delete(Guid id) - { - return Ok(await Mediator.Send(new DeleteEmployeeByIdCommand { Id = id })); - } - } -} -``` - -**Key Features:** -- Inherits from `BaseApiController` (provides `Mediator` property) -- Uses `[ApiVersion("1.0")]` for API versioning -- Most GET endpoints are `[AllowAnonymous]` (for demo purposes) -- DELETE requires `AdminPolicy` authorization -- Returns `IActionResult` (not strongly-typed `ActionResult`) -- Uses `Guid` for all entity IDs - ---- - -## 🔒 Authentication & Authorization - -### How Token Validation Works - - -**Step 1:** Angular sends request with Bearer token - -```http -GET /api/v1/employees HTTP/1.1 -Host: localhost:44378 -Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI... -``` - -**Step 2:** JWT Bearer middleware validates the token: -* Verifies signature using IdentityServer's public key -* Checks `iss` (issuer) matches configured Authority -* Checks `aud` (audience) matches API resource name -* Checks `exp` (expiration) hasn't passed -* Extracts claims (scopes, user ID, roles) - -**Step 3:** Authorization policies check claims: -* `[Authorize]` — User must be authenticated -* `[Authorize(Policy = AuthorizationConsts.AdminPolicy)]` — User must have Admin role -* `[AllowAnonymous]` — No authentication required - -**Step 4:** If authorized, controller action executes - - -**Step 5:** If unauthorized, returns 401 (not authenticated) or 403 (not authorized) - ---- - - -## Web API Features in Swagger - -The screenshots below summarize the API surface and highlight API resources directly in Swagger. - -### API Overview - -![Swagger API endpoints overview](images/webapi/swagger-api-endpoints.png) -*Figure: Swagger overview of all TalentManagement API endpoint groups.* - -This view helps developers quickly understand versioned routes, available controllers, and endpoint discovery for testing. - -### Employee Resource (Primary Tutorial Path) - -![Employees resource in Swagger](images/webapi/swagger-employees-resource-expanded.png) -*Figure: Expanded Employees API resource with read/write endpoints.* - -This is the core tutorial API path with read/write endpoints used for end-to-end CRUD workflows. - -### Other Domain Resources - -![Departments API resource](images/webapi/swagger-departments-resource-expanded.png) -*Figure: Expanded Departments API resource in Swagger.* - -![Positions API resource](images/webapi/swagger-positions-resource-expanded.png) -*Figure: Expanded Positions API resource in Swagger.* - -![SalaryRanges API resource](images/webapi/swagger-salaryranges-resource-expanded.png) -*Figure: Expanded SalaryRanges API resource in Swagger.* - -These resources follow the same architectural patterns as Employees: versioned routes, authorization, validation, and repository-backed persistence. - -### Platform Resources (Cross-Cutting) - -![Dashboard API resource](images/webapi/swagger-dashboard-resource-expanded.png) -*Figure: Dashboard resource for aggregate and summary data endpoints.* - -![Cache API resource](images/webapi/swagger-cache-resource-expanded.png) -*Figure: Cache resource for cache management and refresh endpoints.* - -Dashboard and Cache support operational and cross-cutting scenarios beyond CRUD and are useful for production diagnostics and performance workflows. - -## 📝 Summary - -In this deep dive, we covered the **actual TalentManagement API** implementation: - -✅ **Clean Architecture** — Four layers with clear separation -✅ **Domain Layer** — Value Objects (PersonName), Base Entities with Guid IDs -✅ **Application Layer** — CQRS with custom mediator, nested handlers, Result wrapper, Repository pattern -✅ **Infrastructure Layer** — Generic repository with field shaping -✅ **WebApi Layer** — API versioning, BaseApiController, AllowAnonymous for demos -✅ **Key Patterns** — Repository, Event Dispatcher, Result wrapper, Field shaping - - -### Key Architectural Decisions - -**Why Value Objects?** -* PersonName encapsulates name validation logic -* Ensures first and last names are always valid -* Prevents invalid state in domain model - -**Why Repository Pattern?** -* Abstracts data access from business logic -* Easier to test (mock repositories) -* Can swap data providers without changing business logic - -**Why Result Wrapper?** -* Standardized error handling -* Avoids throwing exceptions for business rule violations -* Better API consistency - -**Why Field Shaping?** -* Allows clients to request only needed fields -* Reduces bandwidth -* Improves performance - ---- - -**Next in series:** [Part 4 — Angular Client Deep Dive →](04-angular-client-deep-dive.md) - -**Previous:** [← Part 2: Token Service Deep Dive](02-token-service-deep-dive.md) - -**Tutorial Home:** [📚 Complete Tutorial Series](TUTORIAL.md) - - - - diff --git a/docs/04-angular-client-deep-dive.md b/docs/04-angular-client-deep-dive.md deleted file mode 100644 index 1ca83da..0000000 --- a/docs/04-angular-client-deep-dive.md +++ /dev/null @@ -1,788 +0,0 @@ -# Part 4: Angular Client Deep Dive — Modern SPA with Material Design and OIDC - -## Building Modern Web Applications with Angular, .NET, and OAuth 2.0 - -**[← Part 3: API Resource](03-api-resource-deep-dive.md)** | **[Tutorial Home](TUTORIAL.md)** | **[Part 5: Advanced Topics →](05-advanced-topics.md)** - ---- - -## 🎨 Introduction - -The **Angular Client** is the presentation layer of the CAT pattern. It's responsible for: - -* **User interface** — Providing an intuitive, responsive UI with Material Design -* **Authentication** — Managing user login/logout with OIDC using `angular-oauth2-oidc` -* **API communication** — Calling protected API endpoints with Bearer tokens -* **State management** — Managing application and user state -* **Authorization** — Showing/hiding UI based on user roles and permissions - -Our **TalentManagement Angular** app uses **Angular 20** (standalone components) with **Angular Material**, **ng-matero** admin template, and **angular-oauth2-oidc** library for OIDC integration. - -![Anonymous Angular dashboard](images/angular/application-dashboard-anonymous.png) -*Figure: Angular dashboard before authentication, showing public entry experience.* - -### Why Angular with Material Design? - -Angular with Material Design provides: - -✅ **Type Safety** — TypeScript catches errors at compile time -✅ **Component Architecture** — Reusable, maintainable UI components -✅ **Reactive Programming** — RxJS for handling async operations -✅ **Enterprise Ready** — Battle-tested in production applications -✅ **Rich UI Components** — Material Design components out of the box -✅ **Accessibility** — Built-in ARIA support and keyboard navigation - ---- - -## 📚 Angular Fundamentals - -### Standalone Components - -Angular 20 uses standalone components by default, eliminating the need for NgModules. - -**Traditional (NgModule-based):** -```typescript -@NgModule({ - declarations: [AppComponent], - imports: [BrowserModule, MatButtonModule], - bootstrap: [AppComponent] -}) -export class AppModule { } -``` - -**Modern (Standalone):** -```typescript -@Component({ - selector: 'app-root', - standalone: true, - imports: [MatButtonModule], - template: `` -}) -export class AppComponent { } -``` - -### Component Lifecycle - -Angular components have a well-defined lifecycle: - -* **`ngOnInit()`** — Initialize component, fetch data -* **`ngOnChanges()`** — React to input property changes -* **`ngOnDestroy()`** — Cleanup (unsubscribe, clear timers) -* **`ngAfterViewInit()`** — Access child components after view initialization - -### Dependency Injection - -Angular's DI system provides services to components: - -```typescript -@Component({ - selector: 'app-employees', - standalone: true, - imports: [CommonModule, MatTableModule] -}) -export class EmployeesComponent { - private employeeService = inject(EmployeeService); - private router = inject(Router); - - // Or using constructor injection - constructor( - private employeeService: EmployeeService, - private router: Router - ) { } -} -``` - -![Employee list UI with Material table](images/angular/employee-list-page.png) -*Figure: Angular Material-based employee list UI with table-driven presentation.* - ---- - -## 🏗️ Project Structure - -### Application Architecture - -``` -talent-management/ -├── src/ -│ ├── app/ -│ │ ├── core/ -│ │ │ ├── authentication/ -│ │ │ │ ├── oidc-auth.service.ts -│ │ │ │ ├── auth.service.ts -│ │ │ │ └── auth.guard.ts -│ │ │ ├── interceptors/ -│ │ │ │ └── auth.interceptor.ts -│ │ │ └── services/ -│ │ │ -│ │ ├── shared/ -│ │ │ ├── components/ -│ │ │ └── pipes/ -│ │ │ -│ │ ├── services/ -│ │ │ └── api/ -│ │ │ ├── base-api.service.ts -│ │ │ ├── employee.service.ts -│ │ │ ├── department.service.ts -│ │ │ └── position.service.ts -│ │ │ -│ │ ├── models/ -│ │ │ ├── employee.model.ts -│ │ │ ├── department.model.ts -│ │ │ ├── position.model.ts -│ │ │ ├── gender.enum.ts -│ │ │ └── pagination.model.ts -│ │ │ -│ │ ├── config/ -│ │ │ └── auth.config.ts -│ │ │ -│ │ ├── routes/ -│ │ │ └── routes.ts -│ │ │ -│ │ └── app.component.ts -│ │ -│ ├── environments/ -│ │ ├── environment.ts -│ │ └── environment.prod.ts -│ │ -│ ├── assets/ -│ ├── styles/ -│ ├── index.html -│ └── main.ts -│ -├── angular.json -├── package.json -└── tsconfig.json -``` - ---- - -## 🔐 OIDC Authentication - -### Configuration - -**environment.ts:** - -```typescript -export const environment = { - production: false, - baseUrl: '', - useHash: false, - - // API Configuration - apiUrl: 'https://localhost:44378/api/v1', - - // Duende IdentityServer Configuration - identityServerUrl: 'https://localhost:44310', - clientId: 'TalentManagement', - scope: 'openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write', - - // Feature Flags - allowAnonymousAccess: true, -}; -``` - -### Auth Configuration - -![Angular login entry point](images/angular/angular-login-page.png) -*Figure: Login entry from the top-right user menu in the Angular client.* - -**auth.config.ts:** - -```typescript -import { AuthConfig } from 'angular-oauth2-oidc'; -import { environment } from '../../environments/environment'; - -export const authConfig: AuthConfig = { - // Duende IdentityServer URL - issuer: environment.identityServerUrl, - - // URL of the Angular app (where IdentityServer will redirect after login) - redirectUri: window.location.origin + '/callback', - - // URL to redirect after logout - postLogoutRedirectUri: window.location.origin, - - // The Angular app's client ID as registered with IdentityServer - clientId: environment.clientId, - - // Requested scopes - scope: environment.scope, - - // Use Authorization Code Flow with PKCE (most secure for SPAs) - responseType: 'code', - - // Show debug information in console (disable in production) - showDebugInformation: !environment.production, - - // Enable silent refresh for automatic token renewal - useSilentRefresh: true, - - // Silent refresh redirect URI - silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', - - // Time before token expires to trigger silent refresh (in seconds) - silentRefreshTimeout: 5000, - - // Timeout for silent refresh (in milliseconds) - timeoutFactor: 0.75, - - // Session checks interval (in milliseconds) - sessionChecksEnabled: true, - - // Clear hash after login - clearHashAfterLogin: true, - - // Disable strict discovery document validation for local development - strictDiscoveryDocumentValidation: false, - - // Skip issuer check (only for local development with localhost) - skipIssuerCheck: !environment.production, - - // Require HTTPS (should be true in production) - requireHttps: environment.production, - - // Request access token - requestAccessToken: true, -}; -``` - -### OIDC Auth Service - -**oidc-auth.service.ts:** - -```typescript -import { Injectable, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { OAuthService, OAuthEvent } from 'angular-oauth2-oidc'; -import { BehaviorSubject, Subject, filter } from 'rxjs'; -import { authConfig } from '../../config/auth.config'; - -export interface UserInfo { - sub: string; - name?: string; - email?: string; - role?: string | string[]; - [key: string]: any; -} - -@Injectable({ - providedIn: 'root', -}) -export class OidcAuthService { - private oauthService = inject(OAuthService); - private router = inject(Router); - - private isAuthenticatedSubject = new BehaviorSubject(false); - public isAuthenticated$ = this.isAuthenticatedSubject.asObservable(); - - private userInfoSubject = new BehaviorSubject(null); - public userInfo$ = this.userInfoSubject.asObservable(); - - // Event emitter for permission refresh - private permissionsChangeSubject = new Subject(); - public permissionsChange$ = this.permissionsChangeSubject.asObservable(); - - constructor() { - this.configureOAuth(); - } - - /** - * Configure OAuth service with auth config - */ - private configureOAuth(): void { - this.oauthService.configure(authConfig); - - // Subscribe to token events - this.oauthService.events - .pipe(filter(e => e.type === 'token_received')) - .subscribe(() => { - this.handleSuccessfulLogin(); - }); - - this.oauthService.events - .pipe(filter(e => e.type === 'token_error' || e.type === 'token_refresh_error')) - .subscribe(() => { - console.error('Token error occurred'); - }); - - // Setup automatic silent refresh - this.oauthService.setupAutomaticSilentRefresh(); - } - - /** - * Initialize authentication - loads discovery document and tries to process login - */ - async initAuth(): Promise { - try { - // Load discovery document (OIDC metadata from /.well-known/openid-configuration) - await this.oauthService.loadDiscoveryDocument(); - - // Try to login using authorization code flow (processes callback if present) - await this.oauthService.tryLogin(); - - // Check if we have a valid access token - if (this.oauthService.hasValidAccessToken()) { - await this.handleSuccessfulLogin(); - return true; - } - - return false; - } catch (error) { - console.error('Error during authentication initialization:', error); - return false; - } - } - - /** - * Initiate login flow - redirects to IdentityServer - */ - login(targetUrl?: string): void { - if (targetUrl) { - this.oauthService.initCodeFlow(targetUrl); - } else { - this.oauthService.initCodeFlow(); - } - } - - /** - * Logout - clears tokens and redirects to IdentityServer logout - */ - logout(): void { - this.oauthService.logOut(); - this.isAuthenticatedSubject.next(false); - this.userInfoSubject.next(null); - } - - /** - * Get access token - */ - getAccessToken(): string { - return this.oauthService.getAccessToken(); - } - - /** - * Check if user is authenticated - */ - isAuthenticated(): boolean { - return this.oauthService.hasValidAccessToken(); - } - - /** - * Get user info claims - */ - getUserInfo(): UserInfo | null { - const claims = this.oauthService.getIdentityClaims() as UserInfo; - return claims || null; - } - - /** - * Handle successful login - */ - private async handleSuccessfulLogin(): Promise { - const userInfo = this.getUserInfo(); - this.userInfoSubject.next(userInfo); - this.isAuthenticatedSubject.next(true); - this.permissionsChangeSubject.next(); - } -} -``` - -### Auth Guard - -![IdentityServer login from Angular flow](images/angular/identityserver-login-ashtyn1.png) -*Figure: Redirected IdentityServer login page during Angular OIDC code flow.* - -**auth.guard.ts:** - -```typescript -import { inject } from '@angular/core'; -import { Router, CanActivateFn } from '@angular/router'; -import { OidcAuthService } from './oidc-auth.service'; - -export const authGuard: CanActivateFn = (route, state) => { - const authService = inject(OidcAuthService); - const router = inject(Router); - - if (!authService.isAuthenticated()) { - // Store the attempted URL for redirecting - authService.login(state.url); - return false; - } - - return true; -}; -``` - -### Auth Interceptor - -![Profile page after authenticated navigation](images/angular/profile-overview-page.png) -*Figure: Profile page reached after successful authentication and guarded routing.* - -**auth.interceptor.ts:** - -```typescript -import { HttpInterceptorFn } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { OidcAuthService } from '../authentication/oidc-auth.service'; -import { environment } from '../../../environments/environment'; - -export const authInterceptor: HttpInterceptorFn = (req, next) => { - const authService = inject(OidcAuthService); - - // Only add token to API requests - if (!req.url.startsWith(environment.apiUrl)) { - return next(req); - } - - // Get access token - const token = authService.getAccessToken(); - - // Clone request and add authorization header - if (token) { - const clonedRequest = req.clone({ - headers: req.headers.set('Authorization', `Bearer ${token}`) - }); - return next(clonedRequest); - } - - return next(req); -}; -``` - ---- - -## 🌐 API Communication - -### Models - -**employee.model.ts:** - -```typescript -import { Gender } from './gender.enum'; - -/** - * Employee entity - */ -export interface Employee { - id: string; - firstName: string; - middleName?: string; - lastName: string; - prefix?: string; - email: string; - phone?: string; - employeeNumber: string; - positionId: string; - positionTitle?: string; - departmentId: string; - departmentName?: string; - salary: number; - birthday?: string; - gender: Gender; - createdAt?: string; - lastModifiedAt?: string; -} - -/** - * Create Employee Command - */ -export interface CreateEmployeeCommand { - employeeNumber: string; - prefix?: string; - firstName: string; - middleName?: string; - lastName: string; - birthday: string; - gender: Gender; - email: string; - phone: string; - salary: number; - positionId: string; - departmentId: string; -} - -/** - * Update Employee Command - */ -export interface UpdateEmployeeCommand { - id: string; - employeeNumber: string; - prefix?: string; - firstName: string; - middleName?: string; - lastName: string; - birthday: string; - gender: Gender; - email: string; - phone: string; - salary: number; - positionId: string; - departmentId: string; -} -``` - -**gender.enum.ts:** - -```typescript -export enum Gender { - Male = 0, - Female = 1 -} -``` - -**pagination.model.ts:** - -```typescript -/** - * Query parameters for API requests - */ -export interface QueryParams { - pageNumber?: number; - pageSize?: number; - orderBy?: string; - fields?: string; - [key: string]: any; -} - -/** - * Pagination parameters - */ -export interface PaginationParams { - pageNumber: number; - pageSize: number; -} - -/** - * Paged response from API - */ -export interface PagedResponse { - succeeded: boolean; - message: string | null; - errors: string[] | null; - value: T[]; - pageNumber: number; - pageSize: number; - totalPages: number; - totalRecords: number; -} -``` - -### Base API Service - -![Employee search and filtering UI](images/angular/employee-search-filtering-ui.png) -*Figure: Search and filtering behavior that maps to query parameters in API requests.* - -**base-api.service.ts:** - -```typescript -import { HttpClient, HttpParams } from '@angular/common/http'; -import { inject } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; -import { PagedResponse, QueryParams } from '../../models'; - -/** - * Base API Service - * Provides common HTTP methods for all entity services - */ -export abstract class BaseApiService { - protected http = inject(HttpClient); - protected apiUrl = environment.apiUrl; - - /** - * Abstract property for entity endpoint - * Must be implemented by derived classes - */ - protected abstract readonly endpoint: string; - - /** - * Get list of entities - */ - getAll(params?: QueryParams): Observable { - const httpParams = this.buildHttpParams(params); - return this.http.get>(`${this.apiUrl}/${this.endpoint}`, { params: httpParams }) - .pipe( - map(response => response.value) - ); - } - - /** - * Get paged list of entities with full response - */ - getAllPaged(params?: QueryParams): Observable> { - const httpParams = this.buildHttpParams(params); - return this.http.get>(`${this.apiUrl}/${this.endpoint}`, { params: httpParams }); - } - - /** - * Get entity by ID - */ - getById(id: string): Observable { - return this.http.get>(`${this.apiUrl}/${this.endpoint}/${id}`) - .pipe( - map(response => response.value as T) - ); - } - - /** - * Create new entity - */ - create(data: Partial): Observable { - return this.http.post(`${this.apiUrl}/${this.endpoint}`, data) - .pipe( - map(response => { - // Handle wrapped response with value property containing the ID - if (response && 'value' in response && typeof response.value === 'string') { - // API returns { value: "guid-string" } - return { id: response.value } as T; - } - // Handle normal entity response - return response as T; - }) - ); - } - - /** - * Update existing entity - */ - update(id: string, data: Partial): Observable { - return this.http.put(`${this.apiUrl}/${this.endpoint}/${id}`, data); - } - - /** - * Delete entity - */ - delete(id: string): Observable { - return this.http.delete(`${this.apiUrl}/${this.endpoint}/${id}`); - } - - /** - * Build HttpParams from query parameters - */ - protected buildHttpParams(params?: QueryParams): HttpParams { - let httpParams = new HttpParams(); - - if (params) { - Object.keys(params).forEach(key => { - const value = params[key]; - if (value !== null && value !== undefined) { - httpParams = httpParams.set(key, value.toString()); - } - }); - } - - return httpParams; - } -} -``` - -**Key Features:** -* Generic base class for all API services -* Handles `PagedResponse` wrapper from API -* Automatically unwraps `value` property -* Builds `HttpParams` from query parameters -* All IDs are strings (Guids) - -### Employee Service - -![Employee CRUD operations in Angular](images/angular/employee-crud-operations.png) -*Figure: CRUD actions in Angular that map to create/update/delete API calls.* - -**employee.service.ts:** - -```typescript -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { - Employee, - CreateEmployeeCommand, - UpdateEmployeeCommand, - PagedResponse, -} from '../../models'; -import { BaseApiService } from './base-api.service'; - -/** - * Employee API Service - */ -@Injectable({ - providedIn: 'root', -}) -export class EmployeeService extends BaseApiService { - protected readonly endpoint = 'Employees'; - - /** - * Create new employee - */ - createEmployee(command: CreateEmployeeCommand): Observable { - return this.create(command); - } - - /** - * Update existing employee - */ - updateEmployee(command: UpdateEmployeeCommand): Observable { - return this.update(command.id, command); - } -} -``` - -**Key Features:** -* Extends `BaseApiService` -* Inherits all CRUD methods (getAll, getById, create, update, delete) -* Only needs to define `endpoint` property -* Can add custom methods as needed - -![Employee create form](images/angular/employee-form.png) -*Figure: Employee create form that sends command payloads through EmployeeService.* - ---- - -## 📝 Summary - -In this deep dive, we covered the **actual TalentManagement Angular** implementation: - -✅ **Angular Fundamentals** — Standalone components, DI with inject() -✅ **OIDC Authentication** — Using `angular-oauth2-oidc` library -✅ **Auth Configuration** — AuthConfig with PKCE, silent refresh -✅ **Auth Service** — OidcAuthService with BehaviorSubjects -✅ **Auth Guard** — Functional guard with CanActivateFn -✅ **Auth Interceptor** — Functional interceptor for adding Bearer tokens -✅ **API Communication** — BaseApiService generic pattern -✅ **Models** — Employee with Guid IDs, Gender enum, PagedResponse wrapper -✅ **Best Practices** — RxJS observables, TypeScript interfaces, dependency injection - -### Key Architectural Decisions - -**Why angular-oauth2-oidc?** -* Mature, well-maintained library -* Full OIDC support with PKCE -* Automatic token refresh -* Session management -* Discovery document support - -**Why BaseApiService?** -* Code reuse across all entity services -* Consistent API patterns -* Centralized error handling -* Easy to extend and customize - -**Why Guid as string?** -* C# Guid → JSON string -* JavaScript doesn't have Guid type -* String is appropriate for IDs in TypeScript - -**Why PagedResponse wrapper?** -* API returns standardized wrapper with metadata -* Includes succeeded, message, errors properties -* Pagination info (pageNumber, totalRecords) -* Needs unwrapping to get actual data - ---- - -**Next in series:** [Part 5 — Advanced Topics →](05-advanced-topics.md) - -**Previous:** [← Part 3: API Resource Deep Dive](03-api-resource-deep-dive.md) - -**Tutorial Home:** [📚 Complete Tutorial Series](TUTORIAL.md) - diff --git a/docs/05-advanced-topics.md b/docs/05-advanced-topics.md deleted file mode 100644 index 924fb81..0000000 --- a/docs/05-advanced-topics.md +++ /dev/null @@ -1,1094 +0,0 @@ -# Part 5: Advanced Topics — Git Submodules, Testing, Deployment, and Production - -## Building Modern Web Applications with Angular, .NET, and OAuth 2.0 - -**[← Part 4: Angular Client](04-angular-client-deep-dive.md)** | **[Tutorial Home](TUTORIAL.md)** | **[Part 6: Real-World Features →](06-real-world-features.md)** - ---- - -## 🚀 Introduction - -This guide covers advanced topics essential for taking your CAT pattern application from development to production: - -* **Git Submodules** — Managing multi-repository architecture -* **Testing Strategies** — Unit, integration, and end-to-end testing -* **CI/CD Pipelines** — Automated builds and deployments -* **Docker Containerization** — Consistent environments across dev and production -* **Production Deployment** — Azure, AWS, and on-premise options -* **Monitoring & Logging** — Application insights and observability -* **Performance Optimization** — Scaling and optimization techniques -* **Security Hardening** — Production security best practices - ---- - -## 📦 Git Submodules Deep Dive - -### Why Use Git Submodules? - -The CAT pattern uses Git submodules to keep each component in its own repository while maintaining a parent repository that ties everything together. - -**Benefits:** -✅ Independent version control for each component -✅ Teams can work on components independently -✅ Easy to share components across projects -✅ Clear separation of concerns -✅ Flexible deployment (deploy components separately) - -**Trade-offs:** -❌ More complex than monorepo -❌ Requires understanding of submodule commands -❌ Can be confusing for team members new to submodules - -### Working with Submodules - -**Initial Clone:** - -```bash -# Clone with all submodules -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git - -# If already cloned without submodules -git submodule update --init --recursive -``` - -**Checking Submodule Status:** - -```bash -# View current commit for each submodule -git submodule status - -# Output example: -# 7a3b2c1d... Clients/TalentManagement-Angular-Material (heads/develop) -# 8e4f9a2b... ApiResources/TalentManagement-API (heads/master) -# 9c5d6e3f... TokenService/Duende-IdentityServer (heads/master) -``` - -**Making Changes in a Submodule:** - -```bash -# 1. Navigate to submodule -cd Clients/TalentManagement-Angular-Material - -# 2. Check out the branch you want to work on -git checkout develop - -# 3. Make your changes -# ... edit files ... - -# 4. Commit changes in the submodule -git add . -git commit -m "Add new employee feature" -git push origin develop - -# 5. Return to parent repository -cd ../.. - -# 6. Update parent to reference new commit -git add Clients/TalentManagement-Angular-Material -git commit -m "Update Angular client submodule to latest" -git push -``` - -**Updating Submodules to Latest:** - -```bash -# Pull latest changes in parent -git pull - -# Update all submodules to their latest commits -git submodule update --remote --merge - -# Or update specific submodule -git submodule update --remote --merge Clients/TalentManagement-Angular-Material -``` - -**Common Submodule Workflows:** - -**Workflow 1: Update parent after submodule changes** -```bash -cd ApiResources/TalentManagement-API -git pull origin master -cd ../.. -git add ApiResources/TalentManagement-API -git commit -m "Update API submodule" -git push -``` - -**Workflow 2: Switch submodule branches** -```bash -cd Clients/TalentManagement-Angular-Material -git fetch -git checkout feature/new-dashboard -cd ../.. -git add Clients/TalentManagement-Angular-Material -git commit -m "Switch Angular client to new dashboard feature" -``` - -**Workflow 3: Clone and immediately start working** -```bash -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git -cd AngularNetTutorial/Clients/TalentManagement-Angular-Material -git checkout develop -# Start working... -``` - ---- - -## 🧪 Testing Strategies - -### Testing Pyramid - -The testing pyramid shows the recommended distribution of tests: - -**End-to-End Tests (10%)** — Full application testing -* **Tools:** Playwright, Cypress -* **Purpose:** Verify complete user flows -* **Example:** Login → Navigate → Create Employee → Verify in list - -**Integration Tests (20%)** — Component interaction testing -* **Tools:** .NET WebApplicationFactory, Angular TestBed -* **Purpose:** Test multiple components together -* **Example:** Controller → Service → Database - -**Unit Tests (70%)** — Individual component testing -* **Tools:** xUnit, Jasmine, Jest -* **Purpose:** Test business logic in isolation -* **Example:** Validator, service method, pure function - -### .NET API Testing - -**Unit Test Example:** - -```csharp -using Xunit; -using FluentAssertions; -using TalentManagement.Application.Employees.Commands.CreateEmployee; - -namespace TalentManagement.Application.Tests.Employees -{ - public class CreateEmployeeCommandValidatorTests - { - private readonly CreateEmployeeCommandValidator _validator; - - public CreateEmployeeCommandValidatorTests() - { - _validator = new CreateEmployeeCommandValidator(); - } - - [Fact] - public void Should_Have_Error_When_FirstName_Is_Empty() - { - // Arrange - var command = new CreateEmployeeCommand { FirstName = "" }; - - // Act - var result = _validator.Validate(command); - - // Assert - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == "FirstName"); - } - - [Theory] - [InlineData("test@example.com", true)] - [InlineData("invalid-email", false)] - [InlineData("", false)] - public void Should_Validate_Email_Format(string email, bool expected) - { - // Arrange - var command = new CreateEmployeeCommand - { - FirstName = "John", - LastName = "Doe", - Email = email, - DateOfBirth = DateTime.Now.AddYears(-25), - HireDate = DateTime.Now - }; - - // Act - var result = _validator.Validate(command); - - // Assert - result.IsValid.Should().Be(expected); - } - } -} -``` - -**Integration Test Example:** - -```csharp -using Microsoft.AspNetCore.Mvc.Testing; -using System.Net.Http.Json; - -namespace TalentManagement.WebApi.Tests.Integration -{ - public class EmployeesControllerTests : IClassFixture> - { - private readonly HttpClient _client; - - public EmployeesControllerTests(WebApplicationFactory factory) - { - _client = factory.CreateClient(); - } - - [Fact] - public async Task GetEmployees_WithoutAuth_Returns401() - { - // Act - var response = await _client.GetAsync("/api/v1/employees"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); - } - - [Fact] - public async Task GetEmployees_WithValidToken_ReturnsEmployees() - { - // Arrange - _client.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", GetValidTestToken()); - - // Act - var response = await _client.GetAsync("/api/v1/employees"); - var employees = await response.Content.ReadFromJsonAsync>(); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - employees.Should().NotBeNull(); - } - } -} -``` - -### Angular Testing - -**Component Test Example:** - -```typescript -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { of, throwError } from 'rxjs'; -import { EmployeeFormComponent } from './employee-form.component'; -import { EmployeeService } from '../../../core/services/employee.service'; - -describe('EmployeeFormComponent', () => { - let component: EmployeeFormComponent; - let fixture: ComponentFixture; - let employeeService: jasmine.SpyObj; - - beforeEach(async () => { - const spy = jasmine.createSpyObj('EmployeeService', ['createEmployee', 'getEmployee']); - - await TestBed.configureTestingModule({ - imports: [EmployeeFormComponent], - providers: [ - provideHttpClientTesting(), - { provide: EmployeeService, useValue: spy } - ] - }).compileComponents(); - - employeeService = TestBed.inject(EmployeeService) as jasmine.SpyObj; - fixture = TestBed.createComponent(EmployeeFormComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize form with empty values', () => { - expect(component.form.get('firstName')?.value).toBe(''); - expect(component.form.get('email')?.value).toBe(''); - }); - - it('should mark form invalid when required fields empty', () => { - expect(component.form.valid).toBeFalse(); - }); - - it('should call createEmployee on submit', () => { - // Arrange - component.form.patchValue({ - firstName: 'John', - lastName: 'Doe', - email: 'john@example.com', - dateOfBirth: new Date('1990-01-01'), - hireDate: new Date(), - departmentId: 1 - }); - employeeService.createEmployee.and.returnValue(of(123)); - - // Act - component.onSubmit(); - - // Assert - expect(employeeService.createEmployee).toHaveBeenCalled(); - }); -}); -``` - -### E2E Testing with Playwright - -**Installation:** - -```bash -npm init playwright@latest -``` - -**E2E Test Example:** - -```typescript -import { test, expect } from '@playwright/test'; - -test.describe('Employee Management', () => { - test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:4200'); - - // Login - await page.click('button:has-text("Login")'); - await page.fill('input[name="username"]', 'ashtyn1'); - await page.fill('input[name="password"]', 'Pa$$word123'); - await page.click('button[type="submit"]'); - - // Wait for redirect - await page.waitForURL('**/dashboard'); - }); - - test('should create new employee', async ({ page }) => { - // Navigate to employees - await page.click('a:has-text("Employees")'); - await expect(page).toHaveURL('**/employees'); - - // Click add button - await page.click('button:has-text("Add Employee")'); - - // Fill form - await page.fill('input[formControlName="firstName"]', 'Jane'); - await page.fill('input[formControlName="lastName"]', 'Smith'); - await page.fill('input[formControlName="email"]', 'jane.smith@example.com'); - await page.fill('input[formControlName="phoneNumber"]', '555-1234'); - - // Select department - await page.click('mat-select[formControlName="departmentId"]'); - await page.click('mat-option:has-text("IT")'); - - // Submit - await page.click('button:has-text("Save")'); - - // Verify redirect to list - await expect(page).toHaveURL('**/employees'); - - // Verify employee in list - await expect(page.locator('table')).toContainText('Jane Smith'); - }); - - test('should edit employee', async ({ page }) => { - await page.goto('http://localhost:4200/employees'); - - // Click edit button for first employee - await page.click('button[matTooltip="Edit"]'); - - // Update email - await page.fill('input[formControlName="email"]', 'updated@example.com'); - - // Save - await page.click('button:has-text("Save")'); - - // Verify update - await expect(page.locator('table')).toContainText('updated@example.com'); - }); -}); -``` - -**Run E2E Tests:** - -```bash -# Run all tests -npx playwright test - -# Run with UI -npx playwright test --ui - -# Run specific test -npx playwright test employee-management.spec.ts - -# Debug mode -npx playwright test --debug -``` - ---- - -## 🐳 Docker Containerization - -### Why Docker? - -**Benefits:** -✅ Consistent environments (dev, staging, production) -✅ Easy deployment and scaling -✅ Isolation from host system -✅ Version control for infrastructure -✅ Simplified CI/CD pipelines - -### Dockerfiles - -**Angular Dockerfile:** - -```dockerfile -# Build stage -FROM node:20-alpine AS build - -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install dependencies -RUN npm ci - -# Copy source code -COPY . . - -# Build application -RUN npm run build -- --configuration production - -# Production stage -FROM nginx:alpine - -# Copy built app to nginx -COPY --from=build /app/dist/talent-management /usr/share/nginx/html - -# Copy nginx configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] -``` - -**nginx.conf for Angular:** - -```nginx -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - # Angular routing - location / { - try_files $uri $uri/ /index.html; - } - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } -} -``` - -**.NET API Dockerfile:** - -```dockerfile -# Build stage -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build - -WORKDIR /src - -# Copy solution and project files -COPY ["src/TalentManagement.WebApi/TalentManagement.WebApi.csproj", "src/TalentManagement.WebApi/"] -COPY ["src/TalentManagement.Application/TalentManagement.Application.csproj", "src/TalentManagement.Application/"] -COPY ["src/TalentManagement.Domain/TalentManagement.Domain.csproj", "src/TalentManagement.Domain/"] -COPY ["src/TalentManagement.Infrastructure/TalentManagement.Infrastructure.csproj", "src/TalentManagement.Infrastructure/"] - -# Restore dependencies -RUN dotnet restore "src/TalentManagement.WebApi/TalentManagement.WebApi.csproj" - -# Copy remaining source code -COPY . . - -# Build application -WORKDIR "/src/src/TalentManagement.WebApi" -RUN dotnet build "TalentManagement.WebApi.csproj" -c Release -o /app/build - -# Publish stage -FROM build AS publish -RUN dotnet publish "TalentManagement.WebApi.csproj" -c Release -o /app/publish /p:UseAppHost=false - -# Runtime stage -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final - -WORKDIR /app - -# Copy published app -COPY --from=publish /app/publish . - -EXPOSE 80 -EXPOSE 443 - -ENTRYPOINT ["dotnet", "TalentManagement.WebApi.dll"] -``` - -**IdentityServer Dockerfile:** - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build - -WORKDIR /src - -COPY ["src/Duende.STS.Identity/Duende.STS.Identity.csproj", "src/Duende.STS.Identity/"] -RUN dotnet restore "src/Duende.STS.Identity/Duende.STS.Identity.csproj" - -COPY . . -WORKDIR "/src/src/Duende.STS.Identity" -RUN dotnet build "Duende.STS.Identity.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "Duende.STS.Identity.csproj" -c Release -o /app/publish - -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final -WORKDIR /app -COPY --from=publish /app/publish . - -EXPOSE 80 -EXPOSE 443 - -ENTRYPOINT ["dotnet", "Duende.STS.Identity.dll"] -``` - -### Docker Compose - -**docker-compose.yml:** - -```yaml -version: '3.8' - -services: - # SQL Server Database - sqlserver: - image: mcr.microsoft.com/mssql/server:2022-latest - environment: - - ACCEPT_EULA=Y - - SA_PASSWORD=YourStrong@Password123 - - MSSQL_PID=Developer - ports: - - "1433:1433" - volumes: - - sqlserver-data:/var/opt/mssql - healthcheck: - test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "YourStrong@Password123" -Q "SELECT 1" - interval: 10s - timeout: 3s - retries: 10 - - # IdentityServer - identityserver: - build: - context: ./TokenService/Duende-IdentityServer - dockerfile: Dockerfile - environment: - - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=https://+:443;http://+:80 - - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=IdentityServerDb;User Id=sa;Password=YourStrong@Password123;TrustServerCertificate=True - ports: - - "44310:443" - - "5000:80" - depends_on: - sqlserver: - condition: service_healthy - volumes: - - ./TokenService/Duende-IdentityServer/certs:/app/certs:ro - - # Web API - api: - build: - context: ./ApiResources/TalentManagement-API - dockerfile: Dockerfile - environment: - - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=https://+:443;http://+:80 - - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=TalentManagementDb;User Id=sa;Password=YourStrong@Password123;TrustServerCertificate=True - - IdentityServer__Authority=https://identityserver:443 - ports: - - "44378:443" - - "5001:80" - depends_on: - - sqlserver - - identityserver - - # Angular Client - angular: - build: - context: ./Clients/TalentManagement-Angular-Material/talent-management - dockerfile: Dockerfile - ports: - - "4200:80" - depends_on: - - api - - identityserver - -volumes: - sqlserver-data: -``` - -**Running with Docker Compose:** - -```bash -# Build and start all services -docker-compose up --build - -# Start in detached mode -docker-compose up -d - -# View logs -docker-compose logs -f - -# Stop all services -docker-compose down - -# Stop and remove volumes -docker-compose down -v -``` - ---- - -## 🔄 CI/CD Pipelines - -### GitHub Actions - -**.github/workflows/api-ci.yml:** - -```yaml -name: API CI/CD - -on: - push: - branches: [ main, develop ] - paths: - - 'ApiResources/TalentManagement-API/**' - pull_request: - branches: [ main ] - -jobs: - build-and-test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '10.0.x' - - - name: Restore dependencies - run: dotnet restore ApiResources/TalentManagement-API - - - name: Build - run: dotnet build ApiResources/TalentManagement-API --no-restore - - - name: Run tests - run: dotnet test ApiResources/TalentManagement-API --no-build --verbosity normal - - - name: Build Docker image - if: github.ref == 'refs/heads/main' - run: | - cd ApiResources/TalentManagement-API - docker build -t talentmanagement-api:${{ github.sha }} . - - - name: Push to Docker Hub - if: github.ref == 'refs/heads/main' - run: | - echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - docker tag talentmanagement-api:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/talentmanagement-api:latest - docker push ${{ secrets.DOCKER_USERNAME }}/talentmanagement-api:latest -``` - -**.github/workflows/angular-ci.yml:** - -```yaml -name: Angular CI/CD - -on: - push: - branches: [ main, develop ] - paths: - - 'Clients/TalentManagement-Angular-Material/**' - pull_request: - branches: [ main ] - -jobs: - build-and-test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '20' - - - name: Install dependencies - run: | - cd Clients/TalentManagement-Angular-Material/talent-management - npm ci - - - name: Lint - run: | - cd Clients/TalentManagement-Angular-Material/talent-management - npm run lint - - - name: Run tests - run: | - cd Clients/TalentManagement-Angular-Material/talent-management - npm run test -- --watch=false --browsers=ChromeHeadless - - - name: Build - run: | - cd Clients/TalentManagement-Angular-Material/talent-management - npm run build -- --configuration production - - - name: Build Docker image - if: github.ref == 'refs/heads/main' - run: | - cd Clients/TalentManagement-Angular-Material/talent-management - docker build -t talentmanagement-angular:${{ github.sha }} . -``` - ---- - -## ☁️ Production Deployment - -### Azure Deployment - -**Deploying to Azure App Service:** - -**1. Create Azure Resources:** - -```bash -# Create resource group -az group create --name rg-talentmanagement --location eastus - -# Create App Service Plan -az appservice plan create \ - --name plan-talentmanagement \ - --resource-group rg-talentmanagement \ - --sku B1 \ - --is-linux - -# Create Web Apps -az webapp create \ - --resource-group rg-talentmanagement \ - --plan plan-talentmanagement \ - --name api-talentmanagement \ - --runtime "DOTNET|10.0" - -az webapp create \ - --resource-group rg-talentmanagement \ - --plan plan-talentmanagement \ - --name identityserver-talentmanagement \ - --runtime "DOTNET|10.0" - -# Create Azure SQL Database -az sql server create \ - --name sql-talentmanagement \ - --resource-group rg-talentmanagement \ - --location eastus \ - --admin-user sqladmin \ - --admin-password YourStrong@Password123 - -az sql db create \ - --resource-group rg-talentmanagement \ - --server sql-talentmanagement \ - --name TalentManagementDb \ - --service-objective S0 -``` - -**2. Configure App Settings:** - -```bash -# Set connection string -az webapp config connection-string set \ - --resource-group rg-talentmanagement \ - --name api-talentmanagement \ - --settings DefaultConnection="Server=tcp:sql-talentmanagement.database.windows.net,1433;Database=TalentManagementDb;User ID=sqladmin;Password=YourStrong@Password123;Encrypt=True;TrustServerCertificate=False;" \ - --connection-string-type SQLAzure - -# Set app settings -az webapp config appsettings set \ - --resource-group rg-talentmanagement \ - --name api-talentmanagement \ - --settings IdentityServer__Authority=https://identityserver-talentmanagement.azurewebsites.net -``` - -**3. Deploy from GitHub Actions:** - -Add to workflow file: - -```yaml -- name: Deploy to Azure Web App - uses: azure/webapps-deploy@v2 - with: - app-name: 'api-talentmanagement' - publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} - package: './publish' -``` - -### AWS Deployment - -**Using AWS Elastic Beanstalk:** - -```bash -# Initialize EB CLI -eb init -p "Docker running on 64bit Amazon Linux 2023" talent-management-api - -# Create environment -eb create production-env - -# Deploy -eb deploy - -# Set environment variables -eb setenv ASPNETCORE_ENVIRONMENT=Production \ - ConnectionStrings__DefaultConnection="Server=..." \ - IdentityServer__Authority="https://..." -``` - ---- - -## 📊 Monitoring & Logging - -### Application Insights (.NET) - -**Install NuGet Package:** - -```bash -dotnet add package Microsoft.ApplicationInsights.AspNetCore -``` - -**Configure in Program.cs:** - -```csharp -builder.Services.AddApplicationInsightsTelemetry(options => -{ - options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"]; -}); -``` - -**Custom Telemetry:** - -```csharp -public class EmployeesController : ControllerBase -{ - private readonly TelemetryClient _telemetry; - - public EmployeesController(TelemetryClient telemetry) - { - _telemetry = telemetry; - } - - [HttpPost] - public async Task CreateEmployee(CreateEmployeeCommand command) - { - var stopwatch = Stopwatch.StartNew(); - - try - { - var result = await _mediator.Send(command); - - _telemetry.TrackEvent("EmployeeCreated", new Dictionary - { - { "EmployeeId", result.ToString() }, - { "Department", command.DepartmentId.ToString() } - }); - - return Ok(result); - } - catch (Exception ex) - { - _telemetry.TrackException(ex); - throw; - } - finally - { - stopwatch.Stop(); - _telemetry.TrackMetric("EmployeeCreation_Duration", stopwatch.ElapsedMilliseconds); - } - } -} -``` - -### Structured Logging with Serilog - -**Install Packages:** - -```bash -dotnet add package Serilog.AspNetCore -dotnet add package Serilog.Sinks.Console -dotnet add package Serilog.Sinks.File -dotnet add package Serilog.Sinks.Seq -``` - -**Configure:** - -```csharp -using Serilog; - -Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .Enrich.FromLogContext() - .WriteTo.Console() - .WriteTo.File("logs/app-.txt", rollingInterval: RollingInterval.Day) - .WriteTo.Seq("http://localhost:5341") - .CreateLogger(); - -builder.Host.UseSerilog(); -``` - ---- - -## ⚡ Performance Optimization - -### API Optimization - -**1. Enable Response Caching:** - -```csharp -builder.Services.AddResponseCaching(); - -app.UseResponseCaching(); - -[HttpGet] -[ResponseCache(Duration = 60)] // Cache for 60 seconds -public async Task>> GetEmployees() -{ - return Ok(await _mediator.Send(new GetEmployeesQuery())); -} -``` - -**2. Enable Response Compression:** - -```csharp -builder.Services.AddResponseCompression(options => -{ - options.EnableForHttps = true; - options.Providers.Add(); -}); - -app.UseResponseCompression(); -``` - -**3. Use Asynchronous Operations:** - -```csharp -// Good -public async Task> GetEmployeesAsync() -{ - return await _context.Employees.ToListAsync(); -} - -// Bad -public List GetEmployees() -{ - return _context.Employees.ToList(); // Blocking -} -``` - -### Angular Optimization - -**1. Lazy Loading:** - -```typescript -const routes: Routes = [ - { - path: 'employees', - loadChildren: () => import('./features/employees/employees.routes') - } -]; -``` - -**2. OnPush Change Detection:** - -```typescript -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class EmployeeListComponent { } -``` - -**3. TrackBy for Lists:** - -```html - -``` - -```typescript -trackById(index: number, item: Employee): number { - return item.id; -} -``` - ---- - -## 🔒 Production Security - -### Security Checklist - -**IdentityServer:** -✅ Use HTTPS in production -✅ Strong signing certificates -✅ Secure connection strings (Azure Key Vault, AWS Secrets Manager) -✅ Enable rate limiting -✅ Configure CORS properly -✅ Use strong password policies - -**API:** -✅ Validate all inputs -✅ Use parameterized queries (EF Core does this) -✅ Enable HTTPS redirection -✅ Configure CORS restrictively -✅ Implement rate limiting -✅ Use API keys or OAuth scopes for third-party access - -**Angular:** -✅ Sanitize user inputs -✅ Use Content Security Policy headers -✅ Enable HTTPS -✅ Protect against XSS (Angular does automatically) -✅ Don't store sensitive data in localStorage -✅ Use HttpOnly cookies when possible - ---- - -## 📝 Summary - -In this advanced topics guide, we covered: - -✅ **Git Submodules** — Managing multi-repository architecture effectively -✅ **Testing Strategies** — Unit, integration, and E2E testing -✅ **Docker** — Containerization for consistent deployments -✅ **CI/CD** — Automated pipelines with GitHub Actions -✅ **Cloud Deployment** — Azure and AWS deployment strategies -✅ **Monitoring** — Application Insights and Serilog -✅ **Performance** — Optimization techniques for API and Angular -✅ **Security** — Production security best practices - ---- - -**Next in series:** [Part 6 — Real-World Features →](06-real-world-features.md) - -**Previous:** [← Part 4: Angular Client Deep Dive](04-angular-client-deep-dive.md) - -**Tutorial Home:** [📚 Complete Tutorial Series](TUTORIAL.md) - diff --git a/docs/06-real-world-features.md b/docs/06-real-world-features.md deleted file mode 100644 index 2b03ddb..0000000 --- a/docs/06-real-world-features.md +++ /dev/null @@ -1,658 +0,0 @@ -# Part 6: Real-World Features — CRUD Operations, Dashboard, Search, and Analytics - -## Building Modern Web Applications with Angular, .NET, and OAuth 2.0 - -**[← Part 5: Advanced Topics](05-advanced-topics.md)** | **[Tutorial Home](TUTORIAL.md)** - ---- - -## 🎯 Introduction - -This final part demonstrates real-world features aligned with the **actual TalentManagement project** implementation: - -* **Complete CRUD Operations** — Full Create, Read, Update, Delete workflows with Guid IDs -* **Custom Mediator Pattern** — Using custom IMediator implementation (not MediatR library) -* **Repository Pattern** — Using IEmployeeRepositoryAsync instead of direct DbContext -* **Result Wrapper** — Handling Result and PagedResult responses -* **Base API Service** — Generic service pattern for Angular -* **Event Dispatching** — Domain events for employee changes -* **Field Shaping** — Dynamic field selection in API responses - ---- - -## 📋 Complete CRUD Implementation - -### Backend: Full Employee CRUD with Repository Pattern - -**GetEmployeeByIdQuery.cs:** - -```csharp -using TalentManagementAPI.Application.Messaging; -using TalentManagementAPI.Application.Interfaces.Repositories; -using Mapster; - -namespace TalentManagementAPI.Application.Features.Employees.Queries.GetEmployeeById -{ - public class GetEmployeeByIdQuery : IRequest> - { - public Guid Id { get; set; } - } - - public class GetEmployeeByIdQueryHandler - : IRequestHandler> - { - private readonly IEmployeeRepositoryAsync _repository; - private readonly IMapper _mapper; - - public GetEmployeeByIdQueryHandler( - IEmployeeRepositoryAsync repository, - IMapper mapper) - { - _repository = repository; - _mapper = mapper; - } - - public async Task> Handle( - GetEmployeeByIdQuery request, - CancellationToken cancellationToken) - { - var employee = await _repository.GetByIdAsync(request.Id); - - if (employee == null) - { - return Result.Failure($"Employee with ID {request.Id} not found."); - } - - var dto = _mapper.Map(employee); - return Result.Success(dto); - } - } -} -``` - -**UpdateEmployeeCommand.cs:** - -```csharp -using TalentManagementAPI.Application.Messaging; -using TalentManagementAPI.Application.Events; -using TalentManagementAPI.Domain.ValueObjects; - -namespace TalentManagementAPI.Application.Features.Employees.Commands.UpdateEmployee -{ - public class UpdateEmployeeCommand : IRequest> - { - public Guid Id { get; set; } - public string FirstName { get; set; } - public string MiddleName { get; set; } - public string LastName { get; set; } - public Guid PositionId { get; set; } - public Guid DepartmentId { get; set; } - public decimal Salary { get; set; } - public DateTime Birthday { get; set; } - public string Email { get; set; } - public Gender Gender { get; set; } - public string EmployeeNumber { get; set; } - public string Prefix { get; set; } - public string Phone { get; set; } - - public class UpdateEmployeeCommandHandler : IRequestHandler> - { - private readonly IEmployeeRepositoryAsync _repository; - private readonly IEventDispatcher _eventDispatcher; - - public UpdateEmployeeCommandHandler( - IEmployeeRepositoryAsync repository, - IEventDispatcher eventDispatcher) - { - _repository = repository; - _eventDispatcher = eventDispatcher; - } - - public async Task> Handle( - UpdateEmployeeCommand request, - CancellationToken cancellationToken) - { - var employee = await _repository.GetByIdAsync(request.Id); - - if (employee == null) - { - return Result.Failure($"Employee with ID {request.Id} not found."); - } - - // Update employee properties - employee.Name = new PersonName(request.FirstName, request.MiddleName, request.LastName); - employee.PositionId = request.PositionId; - employee.DepartmentId = request.DepartmentId; - employee.Salary = request.Salary; - employee.Birthday = request.Birthday; - employee.Email = request.Email; - employee.Gender = request.Gender; - employee.EmployeeNumber = request.EmployeeNumber; - employee.Prefix = request.Prefix; - employee.Phone = request.Phone; - - await _repository.UpdateAsync(employee); - await _eventDispatcher.PublishAsync(new EmployeeChangedEvent(employee.Id), cancellationToken); - - return Result.Success(employee.Id); - } - } - } -} -``` - -**DeleteEmployeeByIdCommand.cs:** - -```csharp -using TalentManagementAPI.Application.Messaging; - -namespace TalentManagementAPI.Application.Features.Employees.Commands.DeleteEmployeeById -{ - public class DeleteEmployeeByIdCommand : IRequest> - { - public Guid Id { get; set; } - - public class DeleteEmployeeByIdCommandHandler : IRequestHandler> - { - private readonly IEmployeeRepositoryAsync _repository; - - public DeleteEmployeeByIdCommandHandler(IEmployeeRepositoryAsync repository) - { - _repository = repository; - } - - public async Task> Handle( - DeleteEmployeeByIdCommand request, - CancellationToken cancellationToken) - { - var employee = await _repository.GetByIdAsync(request.Id); - - if (employee == null) - { - return Result.Failure($"Employee with ID {request.Id} not found."); - } - - await _repository.DeleteAsync(employee); - - return Result.Success(request.Id); - } - } - } -} -``` - -**Key Features:** -- Uses **custom mediator pattern** (`TalentManagementAPI.Application.Messaging`, not MediatR library) -- Uses **Repository pattern** (`IEmployeeRepositoryAsync`) -- Returns `Result` wrapper (not bare values) -- Uses `Guid` for all entity IDs -- Updates **PersonName** value object -- Publishes **domain events** on changes -- Proper null checking with failure results - ---- - -### Frontend: Complete CRUD with BaseApiService - -**Employee List Component:** - -```typescript -import { Component, OnInit, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MatTableModule } from '@angular/material/table'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatDialog } from '@angular/material/dialog'; -import { Router } from '@angular/router'; -import { EmployeeService } from '../../../services/api/employee.service'; -import { Employee } from '../../../models/employee.model'; -import { PagedResponse, QueryParams } from '../../../models/pagination.model'; - -@Component({ - selector: 'app-employee-list', - standalone: true, - imports: [ - CommonModule, - MatTableModule, - MatButtonModule, - MatIconModule - ], - templateUrl: './employee-list.component.html' -}) -export class EmployeeListComponent implements OnInit { - private employeeService = inject(EmployeeService); - private router = inject(Router); - private dialog = inject(MatDialog); - - employees: Employee[] = []; - displayedColumns = ['employeeNumber', 'firstName', 'lastName', 'email', - 'positionTitle', 'departmentName', 'salary', 'actions']; - loading = false; - - // Pagination - totalRecords = 0; - pageNumber = 1; - pageSize = 10; - - ngOnInit() { - this.loadEmployees(); - } - - loadEmployees() { - this.loading = true; - - const params: QueryParams = { - pageNumber: this.pageNumber, - pageSize: this.pageSize - }; - - this.employeeService.getAllPaged(params).subscribe({ - next: (response: PagedResponse) => { - this.employees = response.value; - this.totalRecords = response.totalRecords; - this.loading = false; - }, - error: (error) => { - console.error('Error loading employees:', error); - this.loading = false; - } - }); - } - - viewEmployee(id: string) { - this.router.navigate(['/employees', id]); - } - - editEmployee(id: string) { - this.router.navigate(['/employees', id, 'edit']); - } - - deleteEmployee(employee: Employee) { - if (confirm(`Are you sure you want to delete ${employee.firstName} ${employee.lastName}?`)) { - this.employeeService.delete(employee.id).subscribe({ - next: () => { - this.loadEmployees(); - }, - error: (error) => { - console.error('Error deleting employee:', error); - } - }); - } - } - - createEmployee() { - this.router.navigate(['/employees', 'new']); - } - - onPageChange(event: any) { - this.pageNumber = event.pageIndex + 1; - this.pageSize = event.pageSize; - this.loadEmployees(); - } -} -``` - -**Key Features:** -* Uses `getAllPaged()` to get `PagedResponse` -* Extracts `value` property for employee array -* Uses `totalRecords` for pagination -* All IDs are strings (Guids) -* Uses inherited methods from `BaseApiService` - -**Employee Form Component:** - -```typescript -import { Component, OnInit, inject } from '@angular/core'; -import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; -import { EmployeeService } from '../../../services/api/employee.service'; -import { DepartmentService } from '../../../services/api/department.service'; -import { PositionService } from '../../../services/api/position.service'; -import { CreateEmployeeCommand, UpdateEmployeeCommand } from '../../../models/employee.model'; -import { Gender } from '../../../models/gender.enum'; - -@Component({ - selector: 'app-employee-form', - standalone: true, - imports: [CommonModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, - MatButtonModule, MatSelectModule, MatDatepickerModule], - templateUrl: './employee-form.component.html' -}) -export class EmployeeFormComponent implements OnInit { - private fb = inject(FormBuilder); - private employeeService = inject(EmployeeService); - private departmentService = inject(DepartmentService); - private positionService = inject(PositionService); - private router = inject(Router); - private route = inject(ActivatedRoute); - - form!: FormGroup; - isEditMode = false; - employeeId?: string; - - departments: Department[] = []; - positions: Position[] = []; - - genders = [ - { value: Gender.Male, label: 'Male' }, - { value: Gender.Female, label: 'Female' } - ]; - - ngOnInit() { - this.initForm(); - this.loadDepartments(); - this.loadPositions(); - this.checkEditMode(); - } - - initForm() { - this.form = this.fb.group({ - employeeNumber: ['', [Validators.required]], - prefix: [''], - firstName: ['', [Validators.required, Validators.maxLength(100)]], - middleName: ['', [Validators.maxLength(100)]], - lastName: ['', [Validators.required, Validators.maxLength(100)]], - email: ['', [Validators.required, Validators.email]], - phone: ['', [Validators.required]], - birthday: ['', Validators.required], - gender: [Gender.Male, Validators.required], - salary: [0, [Validators.required, Validators.min(0)]], - departmentId: ['', Validators.required], - positionId: ['', Validators.required] - }); - } - - loadDepartments() { - this.departmentService.getAll().subscribe({ - next: (data) => { - this.departments = data; - } - }); - } - - loadPositions() { - this.positionService.getAll().subscribe({ - next: (data) => { - this.positions = data; - } - }); - } - - checkEditMode() { - const id = this.route.snapshot.paramMap.get('id'); - if (id && id !== 'new') { - this.isEditMode = true; - this.employeeId = id; - this.loadEmployee(this.employeeId); - } - } - - loadEmployee(id: string) { - this.employeeService.getById(id).subscribe({ - next: (employee) => { - this.form.patchValue({ - employeeNumber: employee.employeeNumber, - prefix: employee.prefix, - firstName: employee.firstName, - middleName: employee.middleName, - lastName: employee.lastName, - email: employee.email, - phone: employee.phone, - birthday: employee.birthday, - gender: employee.gender, - salary: employee.salary, - departmentId: employee.departmentId, - positionId: employee.positionId - }); - }, - error: (error) => { - console.error('Error loading employee:', error); - } - }); - } - - onSubmit() { - if (this.form.invalid) { - return; - } - - const formValue = this.form.value; - - if (this.isEditMode && this.employeeId) { - const command: UpdateEmployeeCommand = { - id: this.employeeId, - ...formValue - }; - - this.employeeService.updateEmployee(command).subscribe({ - next: () => { - this.router.navigate(['/employees']); - }, - error: (error) => { - console.error('Error updating employee:', error); - } - }); - } else { - const command: CreateEmployeeCommand = formValue; - - this.employeeService.createEmployee(command).subscribe({ - next: () => { - this.router.navigate(['/employees']); - }, - error: (error) => { - console.error('Error creating employee:', error); - } - }); - } - } - - cancel() { - this.router.navigate(['/employees']); - } -} -``` - -**Key Features:** -* Loads departments and positions for dropdowns -* Uses `Gender` enum (0 = Male, 1 = Female) -* All IDs are strings (Guids) -* Uses `CreateEmployeeCommand` and `UpdateEmployeeCommand` interfaces -* Handles both create and edit modes -* Uses `birthday` field (not `dateOfBirth`) -* Uses `phone` field (not `phoneNumber`) - ---- - -## 📊 Result Wrapper Handling - -### Understanding Result - -The API returns all responses wrapped in `Result` or `PagedResult`: - -**Result structure:** -```typescript -interface Result { - succeeded: boolean; - message: string | null; - errors: string[] | null; - value: T; -} -``` - -**PagedResponse structure:** -```typescript -interface PagedResponse { - succeeded: boolean; - message: string | null; - errors: string[] | null; - value: T[]; - pageNumber: number; - pageSize: number; - totalPages: number; - totalRecords: number; -} -``` - -### Handling in Angular - -The `BaseApiService` automatically unwraps the `value` property: - -```typescript -// BaseApiService handles unwrapping -getById(id: string): Observable { - return this.http.get>(`${this.apiUrl}/${this.endpoint}/${id}`) - .pipe( - map(response => response.value as T) // Unwraps automatically - ); -} -``` - -**For error handling:** - -```typescript -this.employeeService.getById(id).subscribe({ - next: (employee) => { - // employee is already unwrapped - console.log(employee); - }, - error: (error) => { - // Handle HTTP errors - if (error.error && error.error.errors) { - console.error('Validation errors:', error.error.errors); - } - } -}); -``` - ---- - -## 🔍 Advanced Search with Field Shaping - -### Backend: Field Shaping Support - -The API supports dynamic field selection using `IModelHelper`: - -**GetEmployeesQuery with field shaping:** - -```csharp -public class GetEmployeesQuery : QueryParameter, IRequest>> -{ - public string LastName { get; set; } - public string FirstName { get; set; } - public string Email { get; set; } - public string EmployeeNumber { get; set; } - public string PositionTitle { get; set; } - - // Inherited from QueryParameter: - // public int PageNumber { get; set; } - // public int PageSize { get; set; } - // public string OrderBy { get; set; } - // public string Fields { get; set; } // For field shaping -} -``` - -**Handler with field validation:** - -```csharp -public async Task>> Handle(GetEmployeesQuery request, CancellationToken cancellationToken) -{ - var objRequest = request; - - // Validate and filter fields for security - if (!string.IsNullOrEmpty(objRequest.Fields)) - { - // Limit to fields in view model - objRequest.Fields = _modelHelper.ValidateModelFields(objRequest.Fields); - } - else - { - // Default fields from view model - objRequest.Fields = _modelHelper.GetModelFields(); - } - - // Query based on filter - var qryResult = await _repository.GetEmployeeResponseAsync(objRequest); - var data = qryResult.data; - RecordsCount recordCount = qryResult.recordsCount; - - // Response wrapper - return PagedResult>.Success(data, objRequest.PageNumber, objRequest.PageSize, recordCount); -} -``` - -### Frontend: Using Field Shaping - -```typescript -// Request specific fields only -const params: QueryParams = { - pageNumber: 1, - pageSize: 10, - fields: 'id,firstName,lastName,email,departmentName' -}; - -this.employeeService.getAllPaged(params).subscribe({ - next: (response) => { - // Response contains only requested fields - console.log(response.value); - } -}); -``` - -**Benefits:** -* Reduces bandwidth -* Improves performance -* Client controls response payload -* Security: only allowed fields can be requested - ---- - -## 📝 Summary - -In this final part, we covered **real-world features** aligned with the actual project: - -✅ **Complete CRUD** — Using Repository pattern, Result wrapper, Guid IDs -✅ **Value Objects** — PersonName in domain layer -✅ **Domain Events** — EmployeeChangedEvent for side effects -✅ **Base API Service** — Generic Angular service with PagedResponse handling -✅ **Proper Models** — CreateEmployeeCommand, UpdateEmployeeCommand -✅ **Field Shaping** — Dynamic field selection for performance -✅ **Result Wrapper** — Standardized error handling -✅ **Pagination** — Full support with totalRecords, pageNumber, pageSize - -### Series Completion - -**Congratulations!** You've completed the entire tutorial series on building modern web applications with the CAT pattern. You now have: - -✅ **Solid foundation** in OAuth 2.0 and OIDC authentication -✅ **Clean Architecture** knowledge with Repository and Value Object patterns -✅ **Modern Angular** skills with BaseApiService and angular-oauth2-oidc -✅ **Production patterns** including Result wrapper, domain events, field shaping -✅ **Real-world implementation** based on actual production code - -### Key Patterns Learned - -**Backend:** -* Clean Architecture with proper layer separation -* Repository pattern with IEmployeeRepositoryAsync -* CQRS with MediatR (nested handlers) -* Value Objects (PersonName) -* Domain Events (IEventDispatcher) -* Result wrapper pattern -* Field shaping with IModelHelper - -**Frontend:** -* BaseApiService generic pattern -* angular-oauth2-oidc for authentication -* PagedResponse handling -* Reactive forms with validation -* RxJS observables -* TypeScript interfaces for type safety - ---- - -**Previous:** [← Part 5: Advanced Topics](05-advanced-topics.md) - -**Tutorial Home:** [📚 Complete Tutorial Series](TUTORIAL.md) - -**Start from beginning:** [Part 1: Foundation →](01-foundation.md) - diff --git a/docs/CODE-MAP.md b/docs/CODE-MAP.md deleted file mode 100644 index db25b0c..0000000 --- a/docs/CODE-MAP.md +++ /dev/null @@ -1,475 +0,0 @@ -# CODE-MAP.md — Developer Navigation Guide - -## 🗺️ Quick Navigation - -This guide helps you find exactly what you need in the CAT Pattern tutorial repository. - -**New here?** Start with [README.md](../README.md) → [TUTORIAL.md](TUTORIAL.md) → [Labs](labs/README.md) - ---- - -## 📚 Documentation Structure - -### Getting Started -| File | Purpose | When to Read | -|------|---------|--------------| -| [README.md](../README.md) | Repository overview, quick start | First time setup | -| [SETUP-SUBMODULES.md](../SETUP-SUBMODULES.md) | Git submodules guide | Working with submodules | -| [CLAUDE.md](../CLAUDE.md) | Developer guide for Claude Code | Using AI assistance | - -### Tutorial Series (Theory) -| Part | File | Topics | Audience | -|------|------|--------|----------| -| **Part 1** | [01-foundation.md](01-foundation.md) | CAT Pattern, Architecture, Quick Start | Architects, New Developers | -| **Part 2** | [02-token-service-deep-dive.md](02-token-service-deep-dive.md) | OAuth 2.0, OIDC, Duende IdentityServer | Security Engineers | -| **Part 3** | [03-api-resource-deep-dive.md](03-api-resource-deep-dive.md) | Clean Architecture, EF Core, CQRS | Backend Developers | -| **Part 4** | [04-angular-client-deep-dive.md](04-angular-client-deep-dive.md) | Angular, Material Design, OIDC | Frontend Developers | -| **Part 5** | [05-advanced-topics.md](05-advanced-topics.md) | Git, Testing, Docker, CI/CD, Production | DevOps, Full-Stack | -| **Part 6** | [06-real-world-features.md](06-real-world-features.md) | CRUD, Dashboard, Search, Analytics | All Developers | - -### Hands-On Labs (Practice) -| Lab | File | Practice Area | Prerequisites | -|-----|------|---------------|---------------| -| **LAB-01** | [labs/LAB-01-verify-setup.md](labs/LAB-01-verify-setup.md) | Setup verification, Git submodules | None | -| **LAB-02** | [labs/LAB-02-inspect-tokens.md](labs/LAB-02-inspect-tokens.md) | JWT tokens, DevTools, OAuth debugging | LAB-01 | -| **LAB-03** | [labs/LAB-03-extend-api.md](labs/LAB-03-extend-api.md) | Domain → Migration → Application → Testing | LAB-01, LAB-02 | -| **LAB-04** | [labs/LAB-04-build-component.md](labs/LAB-04-build-component.md) | Angular component, Material Design, RxJS | LAB-03 | -| **LAB-05** | [labs/LAB-05-write-tests.md](labs/LAB-05-write-tests.md) | Unit tests (xUnit, Jasmine, TestBed) | LAB-04 | -| **LAB-06** | [labs/LAB-06-docker-deployment.md](labs/LAB-06-docker-deployment.md) | Docker, Docker Compose, Containers | LAB-05 | - -### Lab Solutions (Reference) -| Solution | File | Description | -|----------|------|-------------| -| **LAB-03 Solution** | [labs/solutions/LAB-03-solution.md](labs/solutions/LAB-03-solution.md) | Complete code for extending API | -| **LAB-04 Solution** | [labs/solutions/LAB-04-solution.md](labs/solutions/LAB-04-solution.md) | Complete Angular component code | -| **LAB-05 Solution** | [labs/solutions/LAB-05-solution.md](labs/solutions/LAB-05-solution.md) | Complete unit test examples | - ---- - -## 🏗️ Repository Structure - -``` -AngularNetTutorial/ -├── docs/ # 📚 All documentation -│ ├── TUTORIAL.md # Tutorial series index -│ ├── CODE-MAP.md # This file -│ ├── 01-foundation.md # Part 1: CAT Pattern overview -│ ├── 02-token-service-deep-dive.md -│ ├── 03-api-resource-deep-dive.md -│ ├── 04-angular-client-deep-dive.md -│ ├── 05-advanced-topics.md -│ ├── 06-real-world-features.md -│ └── labs/ # 🧪 Hands-on practice -│ ├── README.md # Labs overview -│ ├── LAB-01-verify-setup.md -│ ├── LAB-02-inspect-tokens.md -│ ├── LAB-03-extend-api.md -│ ├── LAB-04-build-component.md -│ ├── LAB-05-write-tests.md -│ ├── LAB-06-docker-deployment.md -│ └── solutions/ # Lab solutions -│ ├── LAB-03-solution.md -│ ├── LAB-04-solution.md -│ └── LAB-05-solution.md -│ -├── Clients/ # 🎨 Frontend applications -│ └── TalentManagement-Angular-Material/ -│ └── talent-management/ -│ ├── src/ -│ │ ├── app/ -│ │ │ ├── core/ # Services, guards, interceptors -│ │ │ ├── features/ # Feature modules (employees, dashboard) -│ │ │ └── shared/ # Shared components -│ │ └── environments/ # Environment configs -│ └── docs/ # Angular-specific documentation -│ -├── ApiResources/ # 🔧 Backend APIs -│ └── TalentManagement-API/ -│ ├── TalentManagementAPI.Domain/ -│ │ ├── Entities/ # Employee, Department, Position -│ │ ├── Enums/ # Gender, etc. -│ │ └── ValueObjects/ # PersonName -│ ├── TalentManagementAPI.Application/ -│ │ ├── Features/ -│ │ │ └── Employees/ -│ │ │ ├── Commands/ # CreateEmployee, UpdateEmployee -│ │ │ └── Queries/ # GetEmployees, GetEmployee -│ │ └── Interfaces/ # Repository interfaces -│ ├── TalentManagementAPI.Infrastructure.Persistence/ -│ │ ├── Contexts/ # ApplicationDbContext -│ │ ├── Configurations/ # EF Core entity configs -│ │ └── Migrations/ # Database migrations -│ ├── TalentManagementAPI.Infrastructure.Shared/ -│ └── TalentManagementAPI.WebApi/ -│ └── Controllers/ # API endpoints -│ -└── TokenService/ # 🔐 Authentication - └── Duende-IdentityServer/ - └── src/ - ├── Duende.STS.Identity/ # IdentityServer - ├── Duende.Admin/ # Admin UI - └── Duende.Admin.Api/ # Admin API -``` - ---- - -## 🔍 Find What You Need - -### "I want to understand the architecture" - -**Start here:** -1. [01-foundation.md](01-foundation.md) — CAT Pattern overview -2. [README.md](../README.md) — Architecture diagram - -**Deep dives:** -- Token Service: [02-token-service-deep-dive.md](02-token-service-deep-dive.md) -- API Resource: [03-api-resource-deep-dive.md](03-api-resource-deep-dive.md) -- Angular Client: [04-angular-client-deep-dive.md](04-angular-client-deep-dive.md) - ---- - -### "I want to learn by doing" - -**Follow the labs in order:** -1. [LAB-01: Verify Setup](labs/LAB-01-verify-setup.md) -2. [LAB-02: Inspect JWT Tokens](labs/LAB-02-inspect-tokens.md) -3. [LAB-03: Extend API](labs/LAB-03-extend-api.md) -4. [LAB-04: Build Angular Component](labs/LAB-04-build-component.md) -5. [LAB-05: Write Unit Tests](labs/LAB-05-write-tests.md) -6. [LAB-06: Docker Containerization](labs/LAB-06-docker-deployment.md) - ---- - -### "I need to fix authentication issues" - -**OAuth 2.0 / OIDC:** -- [02-token-service-deep-dive.md](02-token-service-deep-dive.md) — Token types, flows, debugging -- [LAB-02: Inspect JWT Tokens](labs/LAB-02-inspect-tokens.md) — Hands-on token inspection - -**Common issues:** -- "invalid_scope" error → [02-token-service-deep-dive.md:628](02-token-service-deep-dive.md) -- Token expiration → [LAB-02: Step 5](labs/LAB-02-inspect-tokens.md#step-5-test-token-expiration) -- CORS errors → [CLAUDE.md](../CLAUDE.md) — "Troubleshooting Authentication Issues" - ---- - -### "I want to add a new API endpoint" - -**Clean Architecture workflow:** -1. Read: [03-api-resource-deep-dive.md](03-api-resource-deep-dive.md) — Layers explained -2. Practice: [LAB-03: Extend API](labs/LAB-03-extend-api.md) — Add Notes property -3. Reference: [labs/solutions/LAB-03-solution.md](labs/solutions/LAB-03-solution.md) — Complete code - -**Key files to modify:** -- `ApiResources/TalentManagement-API/TalentManagementAPI.Domain/Entities/Employee.cs` — Domain entity -- `ApiResources/TalentManagement-API/TalentManagementAPI.Application/Features/Employees/Commands/` — Commands -- `ApiResources/TalentManagement-API/TalentManagementAPI.WebApi/Controllers/` — Controller - ---- - -### "I want to build an Angular component" - -**Angular + Material Design:** -1. Read: [04-angular-client-deep-dive.md](04-angular-client-deep-dive.md) — Angular patterns -2. Practice: [LAB-04: Build Angular Component](labs/LAB-04-build-component.md) — Search component -3. Reference: [labs/solutions/LAB-04-solution.md](labs/solutions/LAB-04-solution.md) — Complete code - -**Key patterns:** -- Standalone components → [04-angular-client-deep-dive.md:130](04-angular-client-deep-dive.md) -- Reactive forms → [LAB-04: Step 3](labs/LAB-04-build-component.md#step-3-implement-component) -- RxJS operators → [04-angular-client-deep-dive.md:458](04-angular-client-deep-dive.md) - ---- - -### "I want to write tests" - -**Unit testing:** -1. Read: [05-advanced-topics.md:137](05-advanced-topics.md) — Testing pyramid -2. Practice: [LAB-05: Write Unit Tests](labs/LAB-05-write-tests.md) — xUnit + Jasmine -3. Reference: [labs/solutions/LAB-05-solution.md](labs/solutions/LAB-05-solution.md) — Test examples - -**Test types:** -- FluentValidation tests → [LAB-05: Step 3](labs/LAB-05-write-tests.md#step-3-write-validator-tests) -- Angular service tests → [LAB-05: Step 5](labs/LAB-05-write-tests.md#step-5-create-service-test-file) -- Component tests → [LAB-05: Step 7](labs/LAB-05-write-tests.md#step-7-create-component-test-file) - ---- - -### "I want to containerize the application" - -**Docker + Docker Compose:** -1. Read: [05-advanced-topics.md:421](05-advanced-topics.md) — Docker fundamentals -2. Practice: [LAB-06: Docker Containerization](labs/LAB-06-docker-deployment.md) — Full stack -3. Run: `docker-compose up --build` - -**Dockerfiles:** -- Angular: [LAB-06: Step 1](labs/LAB-06-docker-deployment.md#step-1-create-angular-dockerfile) -- API: [LAB-06: Step 6](labs/LAB-06-docker-deployment.md#step-6-create-api-dockerfile) -- IdentityServer: [LAB-06: Step 9](labs/LAB-06-docker-deployment.md#step-9-create-identityserver-dockerfile) - ---- - -### "I want to deploy to production" - -**Production deployment:** -1. [05-advanced-topics.md:660](05-advanced-topics.md) — CI/CD pipelines -2. [05-advanced-topics.md:719](05-advanced-topics.md) — Cloud deployment (Azure, AWS) -3. [05-advanced-topics.md:951](05-advanced-topics.md) — Production security - -**Key topics:** -- GitHub Actions → [05-advanced-topics.md:665](05-advanced-topics.md) -- Azure deployment → [05-advanced-topics.md:769](05-advanced-topics.md) -- Monitoring → [05-advanced-topics.md:900](05-advanced-topics.md) - ---- - -### "I want to work with Git submodules" - -**Submodules guide:** -- [SETUP-SUBMODULES.md](../SETUP-SUBMODULES.md) — Complete submodules guide -- [CLAUDE.md](../CLAUDE.md) — "Working with Git Submodules" section -- [05-advanced-topics.md:24](05-advanced-topics.md) — Advanced submodule workflows - -**Common commands:** -```bash -# Clone with submodules -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git - -# Initialize submodules after clone -git submodule update --init --recursive - -# Pull latest changes -git submodule update --remote --merge -``` - ---- - -## 🎯 Learning Paths - -### Path 1: Backend Developer - -**Focus:** .NET, Clean Architecture, EF Core - -1. [01-foundation.md](01-foundation.md) — Overview -2. [03-api-resource-deep-dive.md](03-api-resource-deep-dive.md) — API deep dive -3. [LAB-01: Verify Setup](labs/LAB-01-verify-setup.md) -4. [LAB-03: Extend API](labs/LAB-03-extend-api.md) -5. [LAB-05: Write Unit Tests](labs/LAB-05-write-tests.md) — .NET tests -6. [LAB-06: Docker Deployment](labs/LAB-06-docker-deployment.md) - ---- - -### Path 2: Frontend Developer - -**Focus:** Angular, Material Design, OIDC - -1. [01-foundation.md](01-foundation.md) — Overview -2. [04-angular-client-deep-dive.md](04-angular-client-deep-dive.md) — Angular deep dive -3. [LAB-01: Verify Setup](labs/LAB-01-verify-setup.md) -4. [LAB-02: Inspect JWT Tokens](labs/LAB-02-inspect-tokens.md) -5. [LAB-04: Build Angular Component](labs/LAB-04-build-component.md) -6. [LAB-05: Write Unit Tests](labs/LAB-05-write-tests.md) — Angular tests - ---- - -### Path 3: Security Engineer - -**Focus:** OAuth 2.0, OIDC, IdentityServer - -1. [01-foundation.md](01-foundation.md) — Overview -2. [02-token-service-deep-dive.md](02-token-service-deep-dive.md) — Auth deep dive -3. [LAB-01: Verify Setup](labs/LAB-01-verify-setup.md) -4. [LAB-02: Inspect JWT Tokens](labs/LAB-02-inspect-tokens.md) -5. [05-advanced-topics.md:951](05-advanced-topics.md) — Production security - ---- - -### Path 4: DevOps Engineer - -**Focus:** Docker, CI/CD, Cloud Deployment - -1. [01-foundation.md](01-foundation.md) — Overview -2. [05-advanced-topics.md](05-advanced-topics.md) — Advanced topics -3. [LAB-01: Verify Setup](labs/LAB-01-verify-setup.md) -4. [LAB-06: Docker Deployment](labs/LAB-06-docker-deployment.md) -5. [05-advanced-topics.md:660](05-advanced-topics.md) — CI/CD - ---- - -### Path 5: Full-Stack Developer - -**Complete everything in order:** - -1. [TUTORIAL.md](TUTORIAL.md) — Start here -2. Parts 1-6 (all theory documents) -3. LAB-01 through LAB-06 (all labs) -4. [06-real-world-features.md](06-real-world-features.md) — Production patterns - ---- - -## 🔗 External Resources - -### Technology Documentation -- [.NET 10 Documentation](https://docs.microsoft.com/aspnet/core/) -- [Angular Documentation](https://angular.dev/) -- [Duende IdentityServer Docs](https://docs.duendesoftware.com/) -- [Entity Framework Core](https://docs.microsoft.com/ef/core/) -- [Angular Material](https://material.angular.io/) - -### OAuth 2.0 / OIDC -- [OAuth 2.0 Specification](https://oauth.net/2/) -- [OpenID Connect Specification](https://openid.net/connect/) -- [JWT.io](https://jwt.io/) — Token decoder - -### Docker -- [Docker Documentation](https://docs.docker.com/) -- [Docker Compose Reference](https://docs.docker.com/compose/) - ---- - -## 📞 Getting Help - -### In-Repository Resources -1. Check [README.md](../README.md) — Quick troubleshooting -2. Review [CLAUDE.md](../CLAUDE.md) — Common issues -3. Search labs for similar problems - -### External Help -- **GitHub Issues:** [Report issues](https://github.com/workcontrolgit/AngularNetTutorial/issues) -- **Pull Requests:** [Contribute improvements](https://github.com/workcontrolgit/AngularNetTutorial/pulls) - ---- - -## 🎓 Glossary - -### Common Terms - -**CAT Pattern** — Client, API Resource, Token Service architecture pattern - -**OIDC** — OpenID Connect, authentication protocol built on OAuth 2.0 - -**JWT** — JSON Web Token, secure token format - -**PKCE** — Proof Key for Code Exchange, security extension for OAuth 2.0 - -**CQRS** — Command Query Responsibility Segregation pattern - -**Clean Architecture** — Four-layer architecture (Domain, Application, Infrastructure, WebApi) - -**EF Core** — Entity Framework Core, ORM for .NET - -**MediatR** — Mediator pattern library for CQRS - -**FluentValidation** — Validation library for .NET - -**RxJS** — Reactive Extensions for JavaScript - -**Material Design** — Google's design system - -**TestBed** — Angular testing utility - -**xUnit** — .NET testing framework - ---- - -## 📊 Quick Reference - -### Application URLs (Development) -- **Angular Client:** http://localhost:4200 -- **Web API:** https://localhost:44378 -- **Swagger UI:** https://localhost:44378/swagger -- **IdentityServer:** https://localhost:44310 -- **IdentityServer Admin:** https://localhost:44303 - -### Test Credentials -**Angular Application:** -- Username: `ashtyn1` -- Password: `Pa$$word123` - -**IdentityServer Admin:** -- Username: `admin` -- Password: `Pa$$word123` - -### Default Ports -- Angular: 4200 -- API: 44378 (HTTPS), 5001 (HTTP) -- IdentityServer: 44310 (HTTPS), 5000 (HTTP) -- IdentityServer Admin: 44303 -- IdentityServer Admin API: 44302 -- SQL Server: 1433 - ---- - -## 🚀 Quick Commands - -### Running the Stack - -```bash -# Start IdentityServer -cd TokenService/Duende-IdentityServer/src/Duende.STS.Identity -dotnet run - -# Start API -cd ApiResources/TalentManagement-API -dotnet run - -# Start Angular -cd Clients/TalentManagement-Angular-Material/talent-management -npm start -``` - -### Docker - -```bash -# Build and start all services -docker-compose up --build - -# Stop all services -docker-compose down - -# View logs -docker-compose logs -f -``` - -### Testing - -```bash -# Run .NET tests -cd ApiResources/TalentManagement-API -dotnet test - -# Run Angular tests -cd Clients/TalentManagement-Angular-Material/talent-management -npm test -``` - -### Git Submodules - -```bash -# Clone with submodules -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git - -# Update submodules -git submodule update --remote --merge - -# Check submodule status -git submodule status -``` - ---- - -## 📝 Version Information - -**Tutorial Version:** 1.0 -**Last Updated:** February 2026 - -**Component Versions:** -- Angular: 20.x -- .NET: 10.0 -- Duende IdentityServer: 7.0 -- Node.js: 20.x LTS -- SQL Server: 2022 - ---- - -*Need to add something to this map? [Contribute!](https://github.com/workcontrolgit/AngularNetTutorial/pulls)* diff --git a/docs/CREDENTIALS-UPDATE.md b/docs/CREDENTIALS-UPDATE.md deleted file mode 100644 index 3e5c8f6..0000000 --- a/docs/CREDENTIALS-UPDATE.md +++ /dev/null @@ -1,165 +0,0 @@ -# Credentials Update Summary - -## Overview - -Updated all tutorial documentation to reflect the correct test credentials used in the TalentManagement application. - -## Credentials - -### Angular Application User - -Used for logging into the Angular client application: - -``` -Username: ashtyn1 -Password: Pa$$word123 -``` - -**Roles:** HRAdmin, Manager - -**Purpose:** -- Main application login -- Full access to employee management features -- Can create, read, update, and delete employees -- Used in screenshot automation (`capture-angular.js`) - -### IdentityServer Admin UI - -Used for accessing the IdentityServer administration interface: - -``` -Username: admin -Password: Pa$$word123 -``` - -**Purpose:** -- Access IdentityServer Admin UI (https://localhost:44303) -- Manage clients, API resources, and identity resources -- Configure OAuth 2.0 / OIDC settings -- Used in screenshot automation (`capture-identityserver.js`) - -## Files Updated - -### Documentation Files - -1. **docs/01-foundation.md** - - Line 237-244: Updated login credentials section - - Line 559-574: Updated user seed data example - - Line 742-745: Updated "Try these actions" credentials - -2. **docs/05-advanced-topics.md** - - Line 349-350: Updated Playwright test credentials - -3. **README.md** - - Added "Test Credentials" section after line 89 - - Documents both Angular user and IdentityServer admin credentials - -4. **CLAUDE.md** - - Line 239: Added specific credentials to verification steps - - Added Admin UI access section - -### Screenshot Scripts - -All screenshot automation scripts already use the correct credentials: - -1. **scripts/capture-angular.js** - - Uses: `ashtyn1` / `Pa$$word123` - - Captures: Angular application screenshots with login flow - -2. **scripts/capture-identityserver.js** - - Uses: `admin` / `Pa$$word123` - - Captures: IdentityServer login and admin UI screenshots - -3. **scripts/capture-webapi.js** - - No credentials required (Swagger UI is public) - -## Changes Made - -### From Old Credentials - -**Before:** -``` -Username: alice -Password: Pass123$ -``` - -**After:** -``` -Username: ashtyn1 -Password: Pa$$word123 -``` - -### User Profile Changes - -**Before (alice):** -- Email: alice@example.com -- Name: Alice Smith -- Roles: Admin - -**After (ashtyn1):** -- Email: ashtyn1@example.com -- Name: Ashtyn Doe -- Roles: HRAdmin, Manager - -## Verification - -To verify the credentials work: - -### Angular Application -```bash -1. Start all three services -2. Navigate to http://localhost:4200 -3. Click "Sign In with Identity Server" -4. Enter username: ashtyn1 -5. Enter password: Pa$$word123 -6. Should redirect to dashboard with full access -``` - -### IdentityServer Admin UI -```bash -1. Navigate to https://localhost:44303 -2. Enter username: admin -3. Enter password: Pa$$word123 -4. Should see IdentityServer admin dashboard -``` - -## Screenshot Organization - -All screenshots are organized in: - -``` -docs/images/ -├── angular/ (7 screenshots) -│ ├── angular-login-page.png -│ ├── identityserver-login-ashtyn1.png ← Shows ashtyn1 credentials -│ └── ... -└── identityserver/ (3 screenshots) - ├── identityserver-login.png - ├── identityserver-login-admin.png ← Shows admin credentials - └── identityserver-admin-ui.png -``` - -## Documentation References - -All credential references are now consistent across: - -- ✅ Tutorial documentation (docs/*.md) -- ✅ README.md (main repository guide) -- ✅ CLAUDE.md (developer guide) -- ✅ Screenshot automation scripts -- ✅ Screenshot README (scripts/README.md) - -## Testing Checklist - -- [x] Angular login with ashtyn1 works -- [x] IdentityServer admin login with admin works -- [x] Screenshot automation scripts use correct credentials -- [x] All documentation updated -- [x] Screenshots show correct credentials in login pages - -## Notes - -- The password `Pa$$word123` uses double dollar signs (`$$`) which may need escaping in some contexts -- The `ashtyn1` user has both HRAdmin and Manager roles, providing full access to all features -- The `admin` user is specific to IdentityServer administration, not the Angular application -- All screenshots captured with these credentials are saved in organized folders diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md deleted file mode 100644 index ba2aa55..0000000 --- a/docs/TUTORIAL.md +++ /dev/null @@ -1,262 +0,0 @@ -# Building Modern Web Applications with Angular, .NET, and OAuth 2.0 — Complete Tutorial Series - -Learn how to build secure, scalable enterprise applications using the **CAT Pattern** (Client, API Resource, Token Service) with Angular 20, .NET 10, and Duende IdentityServer, plus comprehensive **end-to-end testing** with Playwright. - -## 📚 Tutorial Series - -### Part 1: Foundation -**[01-foundation.md](01-foundation.md)** — Understanding the CAT Pattern - -**Topics covered:** -* What is the CAT Pattern? -* High-level architecture -* Key security features -* Getting started (prerequisites, installation) -* Quick start guide (running all components) -* Component deep dive (Angular, API, IdentityServer) -* Benefits of the CAT Pattern -* Next steps and customization ideas - -**Perfect for:** Developers new to the CAT pattern, architects evaluating patterns, teams starting new projects - ---- - -### Part 2: Token Service Deep Dive -**[02-token-service-deep-dive.md](02-token-service-deep-dive.md)** — OAuth 2.0, OIDC, and Duende IdentityServer - -**Topics covered:** -* OAuth 2.0 fundamentals -* OpenID Connect (OIDC) -* Understanding tokens (Access, ID, Refresh) -* Duende IdentityServer overview -* Configuration deep dive -* Authentication flows (PKCE) -* Security best practices -* Testing and debugging -* Common issues and solutions - -**Perfect for:** Understanding authentication and authorization, implementing OAuth 2.0/OIDC, securing SPAs - ---- - -### Part 3: API Resource Deep Dive -**[03-api-resource-deep-dive.md](03-api-resource-deep-dive.md)** — Clean Architecture, Entity Framework Core, and RESTful Design - -**Topics covered:** -* Clean Architecture fundamentals -* Domain Layer (entities, enums, exceptions) -* Application Layer (CQRS with MediatR, commands, queries, validators) -* Infrastructure Layer (EF Core, DbContext, configurations) -* WebApi Layer (controllers, authentication, authorization) -* JWT Bearer token validation -* Scope-based and role-based authorization -* Testing strategies (unit and integration tests) -* Best practices (DTOs, async/await, error handling, pagination) - -**Perfect for:** Understanding Clean Architecture, implementing CQRS, securing APIs with JWT tokens - ---- - -### Part 4: Angular Client Deep Dive -**[04-angular-client-deep-dive.md](04-angular-client-deep-dive.md)** — Modern SPA with Material Design and OIDC - -**Topics covered:** -* Angular fundamentals (standalone components, DI, lifecycle) -* OIDC authentication setup and configuration -* Auth service, guards, and HTTP interceptors -* API communication and services -* Material Design UI components -* Forms (reactive forms, validation) -* Routing and navigation -* Role-based UI with ngx-permissions -* State management with Signals -* Component testing and service testing - -**Perfect for:** Building modern Angular SPAs, implementing OIDC authentication, Material Design UIs - ---- - -### Part 5: Advanced Topics -**[05-advanced-topics.md](05-advanced-topics.md)** — Git Submodules, Testing, Deployment, and Production - -**Topics covered:** -* Git submodules deep dive (workflows, commands, best practices) -* Testing strategies (unit, integration, E2E) -* **Playwright E2E testing submodule** (separate repo for tests) -* Test automation for authentication flows -* Running tests across all CAT components -* Docker containerization (Dockerfiles, docker-compose) -* CI/CD pipelines (GitHub Actions) -* Production deployment (Azure, AWS) -* Monitoring and logging (Application Insights, Serilog) -* Performance optimization (caching, compression, lazy loading) -* Production security hardening - -**Perfect for:** DevOps, production deployments, CI/CD automation, containerization, test automation - ---- - -### Part 6: Real-World Features -**[06-real-world-features.md](06-real-world-features.md)** — CRUD Operations, Dashboard, Search, and Analytics - -**Topics covered:** -* Complete CRUD implementation (Create, Read, Update, Delete) -* Dashboard with KPIs and analytics -* Charts and data visualization (Chart.js) -* Advanced search and filtering -* Pagination with server-side paging -* Data export (Excel, CSV) -* Multi-criteria search -* Real-world production patterns - -**Perfect for:** Implementing production features, data visualization, search functionality - ---- - -## 🏗️ Repository Architecture - -This tutorial repository uses **Git submodules** to organize the CAT Pattern into four independent components: - -### Core Components (CAT Pattern) - -1. **Client** — `Clients/TalentManagement-Angular-Material/` - * Angular 20 + Material Design SPA - * OIDC authentication with PKCE - * Repository: https://github.com/workcontrolgit/TalentManagement-Angular-Material - -2. **API Resource** — `ApiResources/TalentManagement-API/` - * .NET 10 Web API with Clean Architecture - * JWT Bearer token validation - * Repository: https://github.com/workcontrolgit/TalentManagement-API - -3. **Token Service** — `TokenService/Duende-IdentityServer/` - * Duende IdentityServer 7.0 - * OAuth 2.0 / OpenID Connect provider - * Repository: https://github.com/workcontrolgit/Duende-IdentityServer - -### Testing Component - -4. **E2E Tests** — `Tests/AngularNetTutorial-Playwright/` - * Playwright end-to-end tests - * Full authentication flow testing - * Cross-component integration tests - * Repository: https://github.com/workcontrolgit/AngularNetTutorial-Playwright - -**Why Git Submodules?** -* Each component can be developed, versioned, and tested independently -* Reuse components across multiple projects -* Clear separation of concerns -* Independent CI/CD pipelines per component - -**Getting Started:** -```bash -# Clone with all submodules -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git - -# Or initialize submodules after cloning -git submodule update --init --recursive -``` - -See **[SETUP-SUBMODULES.md](../SETUP-SUBMODULES.md)** for detailed Git submodule instructions. - ---- - -## 🧪 Hands-On Labs - -**Want to learn by doing?** Follow our progressive hands-on labs that teach practical skills by building with the actual source code. - -**[View All Labs →](labs/README.md)** - -### Quick Start Labs - -**LAB-01: Setup Verification (20 minutes)** -* **Focus:** Verify development environment and Git submodules -* **Prerequisites:** Git, .NET SDK 10.0+, Node.js 20.x -* **Start here:** [LAB-01: Verify Setup](labs/LAB-01-verify-setup.md) - -**LAB-02: JWT Token Inspection (30 minutes)** -* **Focus:** Understand OAuth 2.0 tokens and authentication flow -* **Prerequisites:** LAB-01 completed -* **Continue to:** [LAB-02: Inspect JWT Tokens](labs/LAB-02-inspect-tokens.md) - -**LAB-03: Clean Architecture Vertical Slice (30 minutes)** -* **Focus:** Extend API with new property through all layers -* **Prerequisites:** LAB-01, LAB-02 completed -* **Continue to:** [LAB-03: Extend API](labs/LAB-03-extend-api.md) - -**LAB-04: Angular Material Component (45 minutes)** -* **Focus:** Build search component with Material Design and RxJS -* **Prerequisites:** LAB-03 completed -* **Continue to:** [LAB-04: Build Component](labs/LAB-04-build-component.md) - -**LAB-05: Unit Testing (60 minutes)** -* **Focus:** Write comprehensive unit tests for backend and frontend -* **Prerequisites:** LAB-04 completed -* **Continue to:** [LAB-05: Write Tests](labs/LAB-05-write-tests.md) - -**LAB-06: Docker Containerization (60 minutes)** -* **Focus:** Containerize all three CAT Pattern components -* **Prerequisites:** LAB-05 completed -* **Continue to:** [LAB-06: Docker Deployment](labs/LAB-06-docker-deployment.md) - -**LAB-07: E2E Testing with Playwright (45 minutes)** -* **Focus:** Write and run end-to-end tests for authentication flows -* **Prerequisites:** LAB-06 completed -* **Continue to:** [LAB-07: E2E Testing](labs/LAB-07-e2e-testing.md) - -### What You'll Build - -✅ **LAB-03:** Add a "Notes" field to Employee entity (Domain → Migration → Application → Testing) -✅ **LAB-04:** Build an Angular search component with Material Design, reactive forms, and RxJS -✅ **LAB-05:** Write unit tests for validators, services, and components -✅ **LAB-06:** Containerize all three CAT Pattern components with Docker Compose -✅ **LAB-07:** Write Playwright E2E tests for authentication and CRUD operations - -**[Start with LAB-01: Verify Setup →](labs/LAB-01-verify-setup.md)** - ---- - -## 📖 How to Use This Tutorial - -### For Learning Theory -1. Start with **[Part 1: Foundation](01-foundation.md)** to understand the architecture -2. Continue with **[Part 2: Token Service](02-token-service-deep-dive.md)** to understand authentication -3. Follow the series sequentially for best results - -### For Hands-On Practice -1. Complete **[LAB-01: Verify Setup](labs/LAB-01-verify-setup.md)** to ensure your environment is ready -2. Follow labs **LAB-02 through LAB-06** in sequence -3. Reference tutorial parts as needed for deeper understanding - -### Recommended Learning Path -**Best approach for mastering the CAT Pattern:** - -1. **Read Part 1** (Foundation) — Understand the big picture -2. **Complete LAB-01** — Verify your setup works -3. **Read Part 2** (Token Service) — Learn authentication theory -4. **Complete LAB-02** — Inspect JWT tokens hands-on -5. **Read Part 3** (API Resource) — Learn Clean Architecture -6. **Complete LAB-03** — Build vertical slice through API -7. **Read Part 4** (Angular Client) — Learn Angular patterns -8. **Complete LAB-04** — Build Angular component -9. **Complete LAB-05** — Add unit tests -10. **Read Part 5** (Advanced Topics) — Learn Docker, deployment & testing -11. **Complete LAB-06** — Containerize the application -12. **Complete LAB-07** — Write E2E tests with Playwright -13. **Read Part 6** (Real-World Features) — Production patterns - ---- - -## 🗺️ Quick Navigation - -### Essential Guides -* **[Repository README](../README.md)** — Project overview and quick start -* **[Setup Submodules Guide](../SETUP-SUBMODULES.md)** — Git submodules setup instructions -* **[CODE-MAP.md](CODE-MAP.md)** — Find anything in the codebase quickly -* **[CLAUDE.md](../CLAUDE.md)** — Developer guide for AI assistance - -### Labs & Solutions -* **[All Labs](labs/README.md)** — Hands-on practice exercises -* **[Lab Solutions](labs/solutions/)** — Complete solution code for LAB-03, LAB-04, LAB-05 - - diff --git a/docs/deployment/azure-bicep-infrastructure-plan.md b/docs/deployment/azure-bicep-infrastructure-plan.md deleted file mode 100644 index ed5350b..0000000 --- a/docs/deployment/azure-bicep-infrastructure-plan.md +++ /dev/null @@ -1,695 +0,0 @@ -# Azure Bicep Infrastructure Plan - -## What is Azure Bicep? - -**Azure Bicep is a declarative Infrastructure as Code (IaC) language** for defining and deploying Azure resources. Instead of clicking through the Azure Portal to create resources, you write a `.bicep` file that describes *what* you want — and Azure creates it. - -### Bicep vs. the Azure Portal - -Without Bicep, setting up this project's Azure infrastructure means: - -1. Log into portal.azure.com -2. Manually create a Resource Group -3. Manually create an App Service Plan -4. Manually create two Web Apps -5. Manually create a SQL Server and two databases -6. Repeat for every new environment (staging, production) - -With Bicep, all of that becomes **one command**: - -```bash -az deployment group create \ - --resource-group rg-talent-dev \ - --template-file infra/main.bicep \ - --parameters infra/parameters/dev.bicepparam -``` - -Run it once and all resources are created. Run it again and Azure only updates what changed. Tear down the environment and recreate it identically in minutes. - -### How Bicep Works - -**Yes — Bicep templates are deployed using the Azure CLI** (`az` command) or Azure PowerShell. The typical workflow is: - -``` -Write .bicep file → Run az CLI command → Azure creates resources -``` - -Bicep is not a script that runs step by step. It is a **declaration** of the desired end state. Azure reads the file and figures out what to create, update, or leave alone. - -### Bicep vs. ARM Templates - -Bicep compiles down to ARM (Azure Resource Manager) templates — the native Azure deployment format. Bicep is a cleaner, more readable syntax that Microsoft built on top of ARM. You never need to write raw ARM JSON; write Bicep and the compiler handles the rest. - -### Prerequisites for Running Bicep - -Install these tools once: - -```bash -# 1. Install Azure CLI -# Windows: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-windows -winget install Microsoft.AzureCLI - -# 2. Install Bicep CLI (included with Azure CLI 2.20+, or install separately) -az bicep install - -# 3. Log in to Azure -az login - -# 4. Set your target subscription -az account set --subscription "Your Subscription Name" -``` - -### A Minimal Bicep Example - -Here is what a Bicep file looks like — this creates one App Service Plan: - -```bicep -param location string = 'eastus' -param appServicePlanName string = 'asp-talent-b1-dev' - -resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { - name: appServicePlanName - location: location - sku: { - name: 'B1' - tier: 'Basic' - } -} -``` - -Deploy it with: - -```bash -az group create --name rg-talent-dev --location eastus -az deployment group create \ - --resource-group rg-talent-dev \ - --template-file infra/main.bicep -``` - -### Bicep in GitHub Actions - -In this project, Bicep will be run from GitHub Actions as part of the CI/CD pipeline — so infrastructure changes are version-controlled and deployed automatically alongside code changes. - -```yaml -- name: Deploy Bicep infrastructure - uses: azure/arm-deploy@v1 - with: - resourceGroupName: rg-talent-dev - template: infra/main.bicep - parameters: infra/parameters/dev.bicepparam -``` - ---- - -## Purpose - -This document defines the infrastructure that should be provisioned with Bicep for the low-cost Azure deployment design. - -The goal is to express the target Azure resources as Infrastructure as Code instead of relying on ad hoc portal setup. - -## Bicep Scope - -The Bicep template should provision: - -1. App Service Plan -2. API Web App -3. IdentityServer Web App -4. Angular Static Web App -5. Azure SQL logical server -6. API database -7. IdentityServer database -8. optional Application Insights later, if budget allows - -## Is Bicep a Separate Project or Repository? - -**No — Bicep lives inside the parent repository, not in a separate repo.** - -This project uses Git submodules. The parent repository (`AngularNetTutorial`) is the right place for Bicep because it orchestrates the full stack. The submodules (API, Angular, IdentityServer) contain only their own application code. - -``` -AngularNetTutorial/ ← parent repo — Bicep lives HERE -├── infra/ ← all Bicep files -│ ├── main.bicep ← entry point, composes all modules -│ ├── modules/ ← one file per resource type -│ │ ├── appServicePlan.bicep -│ │ ├── webApp.bicep -│ │ ├── staticWebApp.bicep -│ │ └── sqlServer.bicep -│ └── parameters/ ← one file per environment -│ ├── dev.bicepparam -│ └── prod.bicepparam -├── .github/ -│ └── workflows/ -│ ├── deploy-infra.yml ← runs Bicep (infrastructure only) -│ ├── deploy-api.yml ← deploys .NET API -│ ├── deploy-identityserver.yml -│ └── deploy-angular.yml ← deploys Angular to Static Web Apps -├── ApiResources/TalentManagement-API/ ← git submodule -├── Clients/TalentManagement-Angular-Material/ ← git submodule -├── TokenService/Duende-IdentityServer/ ← git submodule -└── Tests/AngularNetTutorial-Playwright/ ← git submodule -``` - -### Why Not a Separate Repo? - -A separate "infrastructure repo" is appropriate for large teams where a platform/ops team owns infrastructure independently of application code. For this project: - -- the infrastructure is tightly coupled to one application stack -- Bicep changes and app code changes are often committed together -- a single parent repo keeps the full picture in one place -- the `infra/` folder is small — it does not warrant its own repo - -### Four Separate Workflows, Not One - -Infrastructure and application deployments use **separate GitHub Actions workflows**: - -| Workflow | Trigger | What It Does | -|---|---|---| -| `deploy-infra.yml` | Manual or push to `infra/` | Runs Bicep — creates/updates Azure resources | -| `deploy-api.yml` | Push to `ApiResources/` submodule | Builds and deploys .NET API | -| `deploy-identityserver.yml` | Push to `TokenService/` submodule | Builds and deploys IdentityServer | -| `deploy-angular.yml` | Push to `Clients/` submodule | Builds Angular and deploys to Static Web Apps | - -**Why separate?** Infrastructure rarely changes. Keeping it in its own workflow means an API code change does not re-run Bicep. It also makes it easier to run Bicep manually (first time setup) without triggering an application deployment. - -### Deploy Order (First Time Setup) - -On first setup, run the workflows in this order: - -``` -1. deploy-infra.yml ← provision all Azure resources first -2. deploy-identityserver.yml ← IdentityServer must be up before API can validate tokens -3. deploy-api.yml ← API needs IdentityServer running -4. deploy-angular.yml ← Angular can deploy any time after infra is ready -``` - -After the first setup, each workflow runs independently on its own trigger. - ---- - -## Recommended File Location - -Create Bicep assets under: - -``` -infra/ -├── main.bicep -├── modules/ -│ ├── appServicePlan.bicep -│ ├── webApp.bicep -│ ├── staticWebApp.bicep -│ └── sqlServer.bicep -└── parameters/ - ├── dev.bicepparam - └── prod.bicepparam -``` - -### GitHub Actions Workflow File - -Create `.github/workflows/deploy-infra.yml` in the parent repository: - -```yaml -name: Deploy Infrastructure (Bicep) - -on: - workflow_dispatch: # manual trigger — run this first on new environments - push: - branches: [main] - paths: - - 'infra/**' # only runs when Bicep files change - -permissions: - id-token: write # required for Azure OpenID Connect (OIDC) login - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to Azure (OIDC — no stored secrets) - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Create Resource Group (if it does not exist) - run: | - az group create \ - --name rg-talent-dev \ - --location eastus - - - name: Deploy Bicep infrastructure - uses: azure/arm-deploy@v2 - with: - resourceGroupName: rg-talent-dev - template: infra/main.bicep - parameters: infra/parameters/dev.bicepparam - failOnStdErr: false -``` - -### Safeguarding AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID - -#### Where These Values Live - -**These three IDs are never written inside any Bicep file or committed to source control.** They are stored exclusively as encrypted GitHub repository secrets and referenced in the workflow using `${{ secrets.SECRET_NAME }}` syntax. - -```yaml -# In the workflow file — the actual values are NEVER visible here -- uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} # resolved at runtime from GitHub Secrets - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} -``` - -GitHub Secrets are: -- **encrypted at rest** — GitHub encrypts the value when you save it -- **masked in logs** — if a secret value appears in a workflow log, GitHub replaces it with `***` -- **never exposed to forks** — pull requests from forked repositories cannot access repository secrets -- **not visible after entry** — once saved, the value cannot be retrieved from the GitHub UI - -#### Why OIDC — No Password Stored - -The traditional approach to GitHub → Azure authentication uses a **client secret** (a password): - -``` -GitHub stores password → sends password to Azure → Azure validates password -``` - -The problem: that password is a long-lived credential. If it leaks (log file, PR comment, accident), an attacker has access until someone rotates it. - -**OIDC (OpenID Connect) eliminates the password entirely:** - -``` -GitHub generates a short-lived JWT for this specific job run -→ Azure validates the JWT against the trusted GitHub OIDC issuer -→ Azure issues a short-lived access token (valid ~1 hour) -→ Job runs and token expires automatically -``` - -No password exists to leak. Even if an attacker intercepted the access token, it expires when the job ends. - -#### Azure Side Setup (One-Time, Done in Azure Portal) - -Before the workflow can authenticate, configure trust on the Azure side: - -**Step 1 — Create an App Registration** - -``` -Azure Portal → Azure Active Directory → App registrations → New registration -Name: github-actions-talent-dev -``` - -This gives you the `AZURE_CLIENT_ID` (Application ID) and `AZURE_TENANT_ID` (shown on the overview page). - -**Step 2 — Add a Federated Identity Credential** - -``` -App Registration → Certificates & secrets → Federated credentials → Add credential - -Scenario: GitHub Actions deploying Azure resources -Organization: workcontrolgit -Repository: AngularNetTutorial -Entity type: Branch -Branch: main -``` - -This tells Azure: "trust JWT tokens issued by GitHub Actions for this specific repo and branch." No password is created. - -**Step 3 — Grant the App Registration a Role on the Resource Group** - -``` -Azure Portal → Resource Groups → rg-talent-dev → Access control (IAM) -→ Add role assignment -Role: Contributor -Member: github-actions-talent-dev (the App Registration from Step 1) -``` - -> **Use Resource Group scope, not Subscription scope.** `Contributor` at the Subscription level gives access to all resources in your entire Azure subscription. Scoping to the resource group limits the blast radius — if the credential is ever misused, it can only affect `rg-talent-dev`. - -**Step 4 — Add the Three Values as GitHub Secrets** - -``` -GitHub → Repository → Settings → Secrets and variables → Actions → New repository secret -``` - -| Secret Name | Value | How to Find It | -|---|---|---| -| `AZURE_CLIENT_ID` | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | App Registration → Overview → Application (client) ID | -| `AZURE_TENANT_ID` | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | App Registration → Overview → Directory (tenant) ID | -| `AZURE_SUBSCRIPTION_ID` | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | Azure Portal → Subscriptions → Subscription ID | - -#### Automate the Setup with a Script - -All four steps can be run with a single script instead of clicking through the portal. The script lives at `infra/scripts/setup-oidc.sh` in the repository. - -**What the script does:** - -* Creates the App Registration (`az ad app create`) -* Creates the Service Principal (`az ad sp create`) -* Adds the Federated Identity Credential for the `main` branch (`az ad app federated-credential create`) -* Creates the Resource Group if it does not yet exist (`az group create`) -* Grants `Contributor` on the Resource Group only (`az role assignment create`) -* Saves `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID` directly to GitHub Secrets (`gh secret set`) - -**Prerequisites before running:** - -```bash -# Log in to Azure CLI and select the correct subscription -az login -az account set --subscription "Your Subscription Name" - -# Log in to GitHub CLI (needed to write GitHub Secrets) -gh auth login -``` - -**Run the script once:** - -```bash -# From the root of the repository -chmod +x infra/scripts/setup-oidc.sh -./infra/scripts/setup-oidc.sh -``` - -**Edit the configuration block at the top of the script** before running: - -```bash -APP_NAME="github-actions-talent-dev" # App Registration display name -RESOURCE_GROUP="rg-talent-dev" # Resource group to grant access to -LOCATION="eastus" # Azure region -GITHUB_ORG="workcontrolgit" # GitHub organisation -GITHUB_REPO="AngularNetTutorial" # Repository name -BRANCH="main" # Branch that triggers deployments -``` - -**After the script finishes:** - -The script prints a summary and reminds you to add the one secret it cannot generate — the SQL admin password: - -```bash -gh secret set SQL_ADMIN_PASSWORD --repo workcontrolgit/AngularNetTutorial -``` - -Then verify all four secrets are present at: -`https://github.com/workcontrolgit/AngularNetTutorial/settings/secrets/actions` - -> **Run once per environment.** For a `prod` environment, copy the script, set `APP_NAME="github-actions-talent-prod"` and `RESOURCE_GROUP="rg-talent-prod"`, and run again. This creates a separate App Registration with a separate Federated Credential scoped to the `prod` resource group. - ---- - -#### Principle of Least Privilege - -| Concern | Recommendation | -|---|---| -| Role scope | `Contributor` on the **resource group only** — not the subscription | -| Number of credentials | One App Registration per environment (`dev`, `prod`) — never share credentials across environments | -| Federated credential scope | Lock to a specific branch (`main`) — not `*` for all branches | -| `sqlAdminPassword` | Store as a GitHub secret, pass as a `--parameters` argument to Bicep — never hardcode in `.bicepparam` | - -#### `sqlAdminPassword` — Handling Bicep Secure Parameters - -The SQL admin password is a `@secure()` Bicep parameter. Never put it in `dev.bicepparam`. Instead, pass it at deploy time from a GitHub secret: - -```yaml -- name: Deploy Bicep infrastructure - uses: azure/arm-deploy@v2 - with: - resourceGroupName: rg-talent-dev - template: infra/main.bicep - parameters: > - infra/parameters/dev.bicepparam - sqlAdminPassword=${{ secrets.SQL_ADMIN_PASSWORD }} -``` - -Add `SQL_ADMIN_PASSWORD` as a fourth GitHub repository secret containing the chosen password. - -#### Required GitHub Secrets (Complete List) - -| Secret | Purpose | -|---|---| -| `AZURE_CLIENT_ID` | Identifies the App Registration for OIDC login | -| `AZURE_TENANT_ID` | Identifies the Azure AD tenant | -| `AZURE_SUBSCRIPTION_ID` | Identifies the target Azure subscription | -| `SQL_ADMIN_PASSWORD` | Passed as a secure Bicep parameter — never in source files | - -Set them at: **Repository → Settings → Secrets and variables → Actions → New repository secret**. - -### Triggering the Workflow - -After pushing the workflow file and setting the secrets: - -```bash -# Option 1: Push a change to any file in infra/ — workflow triggers automatically -git add infra/main.bicep -git commit -m "Add initial Bicep infrastructure" -git push - -# Option 2: Trigger manually from GitHub UI -# Go to: Actions → Deploy Infrastructure (Bicep) → Run workflow -``` - -## Required Resources - -### App Service Plan - -- SKU: `B1` -- OS: Linux preferred if both applications support it cleanly -- shared by both Web Apps - -### Web App: API - -- runtime configured for the target .NET version -- app settings for database connection and IdentityServer integration -- HTTPS only enabled - -### Web App: IdentityServer - -- runtime configured for the target .NET version -- app settings for IdentityServer database and external URLs -- HTTPS only enabled - -### Azure SQL logical server - -- administrator login configured as a deployment parameter -- firewall/network access kept minimal - -### SQL databases - -- one database for API -- one database for IdentityServer -- lowest practical starting SKU - -## Bicep Parameters - -Parameter names use **camelCase**. Resource name values follow the [Azure Cloud Adoption Framework (CAF) abbreviation convention](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations): `{type}-{workload}-{qualifier}-{env}`. - -### Infrastructure Parameters - -| Parameter | Type | Example Value | CAF Abbreviation | -|---|---|---|---| -| `location` | string | `eastus` | — | -| `environment` | string | `dev` | suffix for all names | -| `appServicePlanName` | string | `asp-talent-b1-dev` | `asp-` | -| `apiAppName` | string | `app-talent-api-dev` | `app-` | -| `identityAppName` | string | `app-talent-ids-dev` | `app-` | -| `angularStaticWebAppName` | string | `swa-talent-ui-dev` | `swa-` | -| `sqlServerName` | string | `sql-talent-dev` | `sql-` | -| `apiDatabaseName` | string | `sqldb-talent-api-dev` | `sqldb-` | -| `identityDatabaseName` | string | `sqldb-talent-ids-dev` | `sqldb-` | -| `sqlAdminLogin` | string | `sqladmin` | — | -| `sqlAdminPassword` | securestring | *(secret)* | `@secure()` | - -### URL Parameters (used to configure IdentityServer and app settings) - -These parameters are derived from the provisioned resource names and must be passed explicitly or computed in the Bicep template using `reference()`. - -| Parameter | Type | Example Value | Used By | -|---|---|---|---| -| `angularAppUrl` | string | `https://swa-talent-ui-dev.azurestaticapps.net` | IdentityServer redirect URIs, API CORS | -| `apiAppUrl` | string | `https://app-talent-api-dev.azurewebsites.net` | Angular `environment.prod.ts`, IdentityServer | -| `identityAppUrl` | string | `https://app-talent-ids-dev.azurewebsites.net` | Angular `environment.prod.ts`, API `Sts:ServerUrl` | - -> These URL parameters are the critical link between infrastructure provisioning and application configuration. They must be confirmed before configuring any app settings. - -## Security Decisions - -The Bicep implementation should enforce these defaults: - -- `httpsOnly: true` on Web Apps -- no secrets committed into source control -- use parameters for credentials -- avoid unnecessary public exposure -- add managed identity later if required by the application design - -## Configuration Strategy - -The template should create infrastructure only. - -Application secrets and environment-specific values should be applied separately through: - -- Azure App Service configuration -- GitHub Actions deployment configuration -- secure deployment parameters - -Avoid hardcoding live connection strings inside the Bicep files. - -## Deployment Flow - -Recommended flow: - -1. deploy Bicep infrastructure -2. configure application settings -3. run database migrations -4. deploy IdentityServer -5. deploy API -6. validate auth and API connectivity - -## Example Resource Relationships - -- one App Service Plan hosts: - - API Web App - - IdentityServer Web App -- one SQL logical server hosts: - - API database - - IdentityServer database - -## Out of Scope for Initial Template - -These items are intentionally excluded from the first low-cost template unless required later: - -- virtual network integration -- private endpoints -- deployment slots -- premium App Service tiers -- Azure Container Registry -- Container Apps resources - -## Configuration Updates Required for Azure - -Once the Azure resources are provisioned and their URLs are known, the following files must be updated before deploying. - ---- - -### 1. IdentityServer — `identityserverdata.json` - -**File:** `TokenService/Duende-IdentityServer/shared/identityserverdata.json` - -Locate the `TalentManagement` client entry (ClientId: `"TalentManagement"`) and add the Azure SWA URLs alongside the existing localhost entries: - -**`RedirectUris`** — add: -``` -https://{angularAppUrl} -https://{angularAppUrl}/silent-refresh.html -https://{angularAppUrl}/callback -``` - -**`PostLogoutRedirectUris`** — add: -``` -https://{angularAppUrl} -``` - -**`AllowedCorsOrigins`** — add: -``` -https://{angularAppUrl} -``` - -**`ClientUri`** — update to: -``` -https://{angularAppUrl} -``` - -> Keep the `localhost` entries. Removing them will break local development. - ---- - -### 2. Angular — `environment.prod.ts` - -**File:** `Clients/TalentManagement-Angular-Material/talent-management/src/environments/environment.prod.ts` - -Current values that need updating: - -```typescript -// BEFORE -apiUrl: 'https://your-production-api.com/api/v1', -identityServerUrl: 'https://localhost:44310', - -// AFTER -apiUrl: 'https://{apiAppUrl}/api/v1', -identityServerUrl: 'https://{identityAppUrl}', -``` - -> In GitHub Actions, these can be injected at build time using `sed` or Angular's `fileReplacements` with environment variables, so the actual URLs do not need to be hardcoded in source control. - ---- - -### 3. API — App Service Configuration (not `appsettings.json`) - -**Do not** commit Azure connection strings or URLs to `appsettings.json`. Instead, set these in **Azure App Service → Configuration → Application settings**: - -| Setting Name | Value | -|---|---| -| `ConnectionStrings__DefaultConnection` | Azure SQL connection string for `sqldb-talent-api-dev` | -| `Sts__ServerUrl` | `https://{identityAppUrl}` | -| `Sts__ValidIssuer` | `https://{identityAppUrl}` | -| `AllowedHosts` | `app-talent-api-dev.azurewebsites.net` | - -> The current `appsettings.json` has `"AllowedHosts": "*"` and `Sts:ServerUrl: "https://localhost:44310"`. Both must be overridden via App Service configuration in Azure. - ---- - -### 4. API — CORS Configuration - -The API must allow cross-origin requests from the Angular Static Web App domain. - -Check the CORS configuration in `appsettings.json` (or the `AddCorsExtension` call in `Program.cs`) and add: - -``` -https://{angularAppUrl} -``` - -as an allowed origin for the production CORS policy. - ---- - -### 5. IdentityServer — App Service Configuration - -Set in **Azure App Service → Configuration → Application settings** for `app-talent-ids-dev`: - -| Setting Name | Value | -|---|---| -| `ConnectionStrings__ConfigurationDbConnection` | Azure SQL connection string for `sqldb-talent-ids-dev` | -| `ConnectionStrings__PersistedGrantDbConnection` | Azure SQL connection string for `sqldb-talent-ids-dev` | -| `ConnectionStrings__IdentityDbConnection` | Azure SQL connection string for `sqldb-talent-ids-dev` | -| `AdminConfiguration__IdentityAdminBaseUrl` | `https://app-talent-ids-dev.azurewebsites.net` | - ---- - -### Configuration Update Checklist - -- [ ] Add Azure SWA URLs to `RedirectUris` in `identityserverdata.json` -- [ ] Add Azure SWA URL to `PostLogoutRedirectUris` in `identityserverdata.json` -- [ ] Add Azure SWA URL to `AllowedCorsOrigins` in `identityserverdata.json` -- [ ] Update `environment.prod.ts` `apiUrl` to Azure API URL -- [ ] Update `environment.prod.ts` `identityServerUrl` to Azure IdentityServer URL -- [ ] Set `Sts__ServerUrl` and `Sts__ValidIssuer` in API App Service configuration -- [ ] Set `ConnectionStrings__DefaultConnection` in API App Service configuration -- [ ] Set IdentityServer database connection strings in IdentityServer App Service configuration -- [ ] Configure CORS in API to allow Angular Static Web App domain -- [ ] Create `staticwebapp.config.json` in Angular for SPA route fallback - ---- - -## Decision Summary - -The Bicep template should provision the smallest practical Azure footprint for this solution: - -- one shared `B1` App Service Plan -- two Web Apps -- one shared Azure SQL logical server -- two databases - -This keeps the infrastructure aligned with the low-cost hosting decision already documented in `docs/azure-deployment-plan.md`. diff --git a/docs/deployment/azure-deployment-plan.md b/docs/deployment/azure-deployment-plan.md deleted file mode 100644 index 482b1d3..0000000 --- a/docs/deployment/azure-deployment-plan.md +++ /dev/null @@ -1,299 +0,0 @@ -# Azure Deployment Plan - -## Goal - -Deploy the full Talent Management stack — Angular client, .NET API, and IdentityServer — to Azure from GitHub at the lowest practical monthly cost, while keeping the design clean enough to support the existing IdentityServer integration and future growth. - -This plan assumes: - -- Azure budget target is approximately `$50/month` -- The API and IdentityServer will both run in Azure App Service -- The Angular SPA will be deployed to Azure Static Web Apps -- A single Azure SQL logical server will be shared -- The SQL server will host two databases: - - `TalentManagementApiDb` - - `IdentityServerDb` - -## Final Decision - -Use the following Azure footprint: - -1. One Azure Resource Group -2. One Azure App Service Plan on the `Basic B1` tier -3. Two Azure Web Apps on that same App Service Plan - - one for the API - - one for IdentityServer -4. One Azure Static Web App for the Angular client -5. One shared Azure SQL logical server -6. Two separate Azure SQL databases on that server - - one for the API - - one for IdentityServer -7. GitHub Actions for CI/CD using Azure OpenID Connect authentication - -## Why This Decision - -This is the lowest-cost practical option for the current requirement. - -### App Service decision - -Azure App Service was selected instead of Azure Container Apps because: - -- it is simpler to operate for standard ASP.NET Core applications -- it avoids container-specific complexity -- it fits the current repo better for straightforward GitHub Actions deployment -- it is easier to keep cost predictable for a small two-app setup - -The `Basic B1` App Service Plan was selected because: - -- it is the lowest dedicated App Service tier appropriate for a real hosted app -- both the API and IdentityServer can share the same plan -- Azure charges at the App Service Plan level, so placing both apps on one plan is significantly cheaper than separate plans - -### Database decision - -A single Azure SQL logical server with two databases was selected because: - -- the API and IdentityServer remain logically isolated -- administration stays simple -- cost stays lower than using more infrastructure than necessary -- this matches the stated requirement that both apps share the same SQL server - -Each application gets its own database because: - -- it avoids coupling the application data model to the IdentityServer schema -- migrations stay independent -- backup/restore and troubleshooting remain cleaner - -### Angular deployment decision - -Azure Static Web Apps was selected for the Angular client because: - -- it is purpose-built for static SPAs and has a free tier suitable for development and demo -- it provides built-in GitHub Actions CI/CD with automatic preview deployments for pull requests -- it includes a built-in CDN and global edge distribution at no extra cost -- it avoids the overhead of running a web server just to serve static files -- adding a third App Service Web App for a static SPA would waste compute resources and increase cost unnecessarily - -The Angular app will be built via `ng build` and the `dist/talent-management/browser` output folder deployed as static assets. - -### CI/CD decision - -GitHub Actions with Azure OpenID Connect was selected because: - -- it integrates directly with the GitHub repository -- it avoids long-lived deployment credentials where possible -- it is the recommended modern deployment model for Azure from GitHub - -## Target Architecture - -### Azure resources - -- Resource Group: one shared resource group for the environment -- App Service Plan: `Basic B1` -- Web App 1: Talent Management API -- Web App 2: IdentityServer -- Static Web App: Angular client (Free tier) -- Azure SQL logical server: shared -- Azure SQL database 1: API database -- Azure SQL database 2: IdentityServer database - -### Networking and configuration - -- keep all App Service resources in the same Azure region -- store connection strings in App Service configuration, not in source control -- store feature flags and environment-specific settings in App Service configuration -- keep production secrets out of `appsettings.json` -- configure Angular production `environment.ts` with Azure API and IdentityServer URLs at build time via GitHub Actions environment variables -- set CORS on the API Web App to allow requests from the Static Web App domain - -## Cost-First Constraints - -This design is intentionally optimized for cost first, not scale first. - -Expected cost controls: - -- one shared `B1` App Service Plan instead of two plans -- Angular on Azure Static Web Apps Free tier (no compute cost) -- one shared Azure SQL logical server -- smallest practical database tiers at the start -- no extra container registry unless later required -- no premium networking or premium compute features initially - -## Known Tradeoffs - -This design has limits that need to be acknowledged. - -### Angular Static Web Apps Free tier - -The Free tier has limits that may require upgrading to Standard (~$9/month): - -- custom domain with free managed TLS is supported -- no SLA on the Free tier -- limited staging environments on Free (only 3 pre-production environments) -- if a backend API proxy or serverless functions are added later, Standard is required - -For development, demo, and MVP usage the Free tier is appropriate. Upgrade to Standard when an SLA or additional staging environments are needed. - -### Shared App Service Plan - -Both applications will compete for the same compute resources. - -Implications: - -- heavy IdentityServer traffic can affect API responsiveness -- heavy API traffic can affect login/token endpoints -- scaling one app means scaling both apps together while they share the same plan - -This is acceptable for: - -- development -- demo -- MVP -- low-traffic internal usage - -This is not ideal for: - -- medium or high production traffic -- independent scaling needs -- strict performance isolation - -### Low-cost SQL tiers - -Starting with the cheapest SQL option reduces cost, but performance headroom is limited. - -Implications: - -- query throughput is limited -- IdentityServer persisted grant activity may become a bottleneck earlier than expected -- future upgrade to a higher tier may be required - -## Recommended Starting SKUs - -These are the initial SKUs to provision unless testing shows they are too small. - -### Compute - -- Azure App Service Plan: `Basic B1` - -### Databases - -- `TalentManagementApiDb`: low-cost Azure SQL single database tier -- `IdentityServerDb`: low-cost Azure SQL single database tier - -The exact database SKU should be confirmed in the Azure Pricing Calculator at provisioning time because pricing changes over time and may vary by region. - -## Deployment Flow - -### Phase 1: Provision Azure resources - -Create: - -1. Resource Group -2. App Service Plan (`B1`) -3. Web App for the API -4. Web App for IdentityServer -5. Static Web App for the Angular client (Free tier) -6. Shared Azure SQL logical server -7. API database -8. IdentityServer database - -### Phase 2: Configure application settings - -For the API Web App: - -- API database connection string -- IdentityServer authority / STS URL -- JWT and auth-related settings -- feature flags -- environment-specific URLs - -For the IdentityServer Web App: - -- IdentityServer database connection string -- signing and runtime settings -- client/app URLs (including Angular Static Web App URL as allowed redirect/logout URI) - -For the Angular Static Web App: - -- `API_URL` — production API base URL (injected at build time via GitHub Actions) -- `IDENTITY_SERVER_URL` — production IdentityServer URL -- configure `staticwebapp.config.json` for SPA fallback routing (all routes return `index.html`) - -### Phase 3: Configure GitHub deployment - -Set up GitHub Actions authentication with Azure using: - -- `AZURE_CLIENT_ID` -- `AZURE_TENANT_ID` -- `AZURE_SUBSCRIPTION_ID` - -Then create deployment workflows for: - -1. IdentityServer -2. Talent Management API -3. Angular client - -For the .NET workflows (IdentityServer and API), each workflow should: - -1. restore -2. build -3. test -4. publish -5. deploy to the target Web App - -For the Angular workflow: - -1. install (`npm ci`) -2. build production (`ng build --configuration production`) -3. deploy `dist/talent-management/browser` to Azure Static Web Apps using the `Azure/static-web-apps-deploy` GitHub Action - -Note: Azure Static Web Apps GitHub Actions integration can be bootstrapped automatically by the Azure portal — it commits a workflow file directly to the repository. This is the easiest starting point. - -### Phase 4: Database migration strategy - -Run migrations separately for each application: - -- API migrations against `TalentManagementApiDb` -- IdentityServer migrations against `IdentityServerDb` - -Migration execution should be explicit and environment-aware. Do not assume both apps can safely auto-migrate on startup in production. - -### Phase 5: Validation - -Validate: - -- Angular app loads at the Static Web App URL -- login redirects to IdentityServer and returns to Angular correctly -- API health and Swagger endpoint -- IdentityServer discovery endpoint -- database connectivity -- token issuance -- API token validation against IdentityServer -- Angular API calls return data with Bearer token (check Network tab) - -## Environment Naming Proposal - -Suggested dev naming: - -- Resource Group: `rg-talent-dev` -- App Service Plan: `asp-talent-b1-dev` -- API Web App: `app-talent-api-dev` -- Identity Web App: `app-talent-ids-dev` -- Angular Static Web App: `swa-talent-ui-dev` -- SQL Server: `sql-talent-dev` -- API DB: `sqldb-talent-api-dev` -- Identity DB: `sqldb-talent-ids-dev` - -## Decision Summary - -The chosen Azure deployment design is: - -- Azure App Service, not Container Apps -- one shared `Basic B1` App Service Plan -- two Web Apps on that shared plan (API + IdentityServer) -- Azure Static Web Apps Free tier for the Angular client -- one shared Azure SQL logical server -- two separate Azure SQL databases -- GitHub Actions with Azure OpenID Connect - -This is the best fit for the current requirement because it keeps monthly cost low, uses the right hosting model for each component (static hosting for the SPA, managed web servers for .NET), respects the shared SQL server constraint, and remains simple to deploy and operate from GitHub. diff --git a/docs/deployment/github-actions-api-deployment.md b/docs/deployment/github-actions-api-deployment.md deleted file mode 100644 index 755d4f9..0000000 --- a/docs/deployment/github-actions-api-deployment.md +++ /dev/null @@ -1,141 +0,0 @@ -# GitHub Actions Deployment Plan for Talent Management API - -## Purpose - -This document defines the GitHub Actions deployment approach for the `TalentManagementAPI.WebApi` project when deploying to Azure App Service. - -The target Azure resource is a dedicated Web App hosted on a shared `Basic B1` App Service Plan. - -## Deployment Target - -- Azure App Service -- separate Web App for the API -- shared App Service Plan with IdentityServer - -## Authentication Model - -Use Azure OpenID Connect from GitHub Actions. - -Required GitHub secrets: - -- `AZURE_CLIENT_ID` -- `AZURE_TENANT_ID` -- `AZURE_SUBSCRIPTION_ID` - -This avoids storing long-lived publish-profile credentials in GitHub. - -## Workflow Responsibilities - -The API workflow should: - -1. trigger on pushes to the deployment branch -2. restore NuGet packages -3. build the solution -4. run tests -5. publish the API project -6. deploy the published output to the API Web App - -## Suggested Workflow File - -Store the workflow as: - -- `.github/workflows/deploy-api.yml` - -## Recommended Trigger - -Start with: - -- push to `main` - -Optional later refinements: - -- path filtering for API-related files only -- manual dispatch for controlled releases -- environment approvals for production - -## Recommended Build Scope - -Restore and build from the solution root: - -- `TalentManagementAPI.slnx` - -Publish only: - -- `TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj` - -## Suggested Workflow Structure - -```yaml -name: Deploy Talent Management API - -on: - push: - branches: [ main ] - -permissions: - id-token: write - contents: read - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Restore - run: dotnet restore TalentManagementAPI.slnx - - - name: Build - run: dotnet build TalentManagementAPI.slnx -c Release --no-restore - - - name: Test - run: dotnet test TalentManagementAPI.slnx -c Release --no-build - - - name: Publish API - run: dotnet publish TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj -c Release -o ./publish/api - - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Deploy API Web App - uses: azure/webapps-deploy@v3 - with: - app-name: app-talent-api-dev - package: ./publish/api -``` - -## App Settings Required in Azure - -These settings should be configured in the Azure Web App, not committed to source control: - -- API database connection string -- `Sts:ServerUrl` -- `Sts:ValidIssuer` -- `Sts:Audience` -- feature flags such as `FeatureManagement__AiEnabled` -- any production-specific cache and mail settings - -## Notes About Database Migration - -Do not couple deployment to automatic production migration unless the migration strategy has been reviewed. - -Preferred options: - -1. separate migration step in the release process -2. controlled manual migration -3. explicit pipeline step added later after validation - -## Decision Summary - -The API deployment workflow should be a standard GitHub Actions build/test/publish/deploy pipeline targeting Azure App Service through OpenID Connect authentication. diff --git a/docs/deployment/github-actions-identityserver-deployment.md b/docs/deployment/github-actions-identityserver-deployment.md deleted file mode 100644 index 34f0775..0000000 --- a/docs/deployment/github-actions-identityserver-deployment.md +++ /dev/null @@ -1,120 +0,0 @@ -# GitHub Actions Deployment Plan for IdentityServer - -## Purpose - -This document defines the GitHub Actions deployment approach for the IdentityServer application that supports the Talent Management API. - -IdentityServer is deployed separately from the API, but both applications share the same Azure App Service Plan. - -## Deployment Target - -- Azure App Service -- separate Web App for IdentityServer -- shared `Basic B1` App Service Plan with the API - -## Authentication Model - -Use Azure OpenID Connect from GitHub Actions. - -Required GitHub secrets: - -- `AZURE_CLIENT_ID` -- `AZURE_TENANT_ID` -- `AZURE_SUBSCRIPTION_ID` - -If the API and IdentityServer are deployed from separate repositories, each repository must hold its own GitHub secrets or use GitHub environments with shared policy. - -## Workflow Responsibilities - -The IdentityServer workflow should: - -1. trigger on pushes to the deployment branch -2. restore packages -3. build the IdentityServer solution or project -4. run tests -5. publish the IdentityServer host project -6. deploy the published output to the IdentityServer Web App - -## Suggested Workflow File - -Store the workflow as: - -- `.github/workflows/deploy-identityserver.yml` - -## Suggested Workflow Structure - -The exact project path depends on the IdentityServer repository layout, but the structure should mirror the API deployment pipeline: - -```yaml -name: Deploy IdentityServer - -on: - push: - branches: [ main ] - -permissions: - id-token: write - contents: read - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build -c Release --no-restore - - - name: Test - run: dotnet test -c Release --no-build - - - name: Publish IdentityServer - run: dotnet publish .csproj -c Release -o ./publish/identity - - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Deploy Identity Web App - uses: azure/webapps-deploy@v3 - with: - app-name: app-talent-ids-dev - package: ./publish/identity -``` - -## IdentityServer Settings Required in Azure - -These settings should be stored in Azure App Service configuration: - -- IdentityServer database connection string -- issuer URL / public origin settings -- client URLs -- signing and certificate-related settings -- external provider settings if used - -## Operational Notes - -IdentityServer is a dependency for the API authentication flow. - -That means: - -- IdentityServer should generally be deployed before the API when auth-related changes are involved -- the API configuration must point to the correct Azure IdentityServer URL -- post-deployment validation must include the discovery document and token issuance flow - -## Decision Summary - -IdentityServer should have its own GitHub Actions deployment workflow and its own Web App, even though it shares the same App Service Plan and Azure SQL logical server with the API. diff --git a/docs/labs/LAB-01-verify-setup.md b/docs/labs/LAB-01-verify-setup.md deleted file mode 100644 index d5d2509..0000000 --- a/docs/labs/LAB-01-verify-setup.md +++ /dev/null @@ -1,639 +0,0 @@ -# LAB-01: Verify Setup & First Code Change - -## 🎯 Objective - -Confirm your development environment is correctly configured and make your first code change to build confidence navigating the project. - -**What you'll accomplish:** -- ✅ Clone repository and initialize Git submodules correctly -- ✅ Verify all three services (IdentityServer, API, Angular) are running -- ✅ Test authentication flow end-to-end -- ✅ Make a simple UI change and see hot-reload -- ✅ Inspect HTTP requests with browser DevTools -- ✅ Navigate the project structure confidently -- ✅ Understand how Git submodules work in the CAT pattern - ---- - -## 📋 Prerequisites - -**Before Starting This Lab:** - -- ✅ Read [Part 1: Foundation](../01-foundation.md) tutorial (understand CAT pattern) -- ✅ .NET SDK 10.0+ installed ([Download](https://dotnet.microsoft.com/download)) -- ✅ Node.js 20.x LTS installed ([Download](https://nodejs.org/)) -- ✅ Git installed ([Download](https://git-scm.com/)) -- ✅ Code editor installed (VS Code recommended: [Download](https://code.visualstudio.com/)) -- ✅ Browser with DevTools (Chrome or Edge recommended) - -**⚠️ Important:** This project uses **Git submodules**. Don't skip Step 0! - ---- - -## ⏱️ Duration - -**15-20 minutes** - ---- - -## 🚀 Steps - -### Step 0: Clone Repository and Initialize Submodules - -**⚠️ IMPORTANT:** This tutorial uses Git submodules. Each component (Angular, API, IdentityServer) is in its own repository. You must initialize submodules before starting. - -#### Option A: Clone with Submodules (Recommended for New Setup) - -If you haven't cloned the repository yet: - -```bash -# Clone the main repository with all submodules -git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git - -# Navigate into the repository -cd AngularNetTutorial -``` - -**Expected output:** -``` -Cloning into 'AngularNetTutorial'... -Submodule 'ApiResources/TalentManagement-API' (https://github.com/workcontrolgit/TalentManagement-API.git) registered for path 'ApiResources/TalentManagement-API' -Submodule 'Clients/TalentManagement-Angular-Material' (https://github.com/workcontrolgit/TalentManagement-Angular-Material.git) registered for path 'Clients/TalentManagement-Angular-Material' -Submodule 'TokenService/Duende-IdentityServer' (https://github.com/workcontrolgit/Duende-IdentityServer.git) registered for path 'TokenService/Duende-IdentityServer' -``` - -#### Option B: Initialize Submodules (If Already Cloned) - -If you already cloned the repository but submodule folders are empty: - -```bash -# Navigate to the repository root -cd AngularNetTutorial - -# Initialize and clone all submodules -git submodule update --init --recursive -``` - -**Expected output:** -``` -Submodule path 'ApiResources/TalentManagement-API': checked out '...' -Submodule path 'Clients/TalentManagement-Angular-Material': checked out '...' -Submodule path 'TokenService/Duende-IdentityServer': checked out '...' -``` - -#### Verify Submodules Are Initialized - -Check that submodule folders contain files (not empty): - -```bash -# Check submodule status -git submodule status -``` - -**Expected output (with commit hashes):** -``` - 7a3b2c1d... Clients/TalentManagement-Angular-Material (heads/develop) - 8e4f9a2b... ApiResources/TalentManagement-API (heads/master) - 9c5d6e3f... TokenService/Duende-IdentityServer (heads/master) -``` - -**Verify folders are not empty:** -```bash -# Windows -dir Clients\TalentManagement-Angular-Material -dir ApiResources\TalentManagement-API -dir TokenService\Duende-IdentityServer - -# Mac/Linux -ls -la Clients/TalentManagement-Angular-Material -ls -la ApiResources/TalentManagement-API -ls -la TokenService/Duende-IdentityServer -``` - -Each folder should contain files (not be empty). - -#### Troubleshooting Submodules - -**Problem: Submodule folders are empty** - -**Solution 1:** Initialize submodules -```bash -git submodule update --init --recursive -``` - -**Solution 2:** If still empty, force update -```bash -git submodule sync --recursive -git submodule update --init --force --recursive -``` - -**Problem: "fatal: not a git repository"** - -**Solution:** Ensure you're in the repository root folder -```bash -# Check current directory -pwd # Mac/Linux -cd # Windows - -# Should be: /path/to/AngularNetTutorial -``` - -**Problem: Submodules show "detached HEAD" state** - -**Solution:** This is normal for submodules. Each submodule points to a specific commit. -```bash -# To work on a submodule, navigate to it and checkout a branch -cd Clients/TalentManagement-Angular-Material -git checkout develop -cd ../.. -``` - -**Problem: "Permission denied (publickey)"** - -**Solution:** Configure Git credentials -```bash -# Use HTTPS instead of SSH -git config --global url."https://github.com/".insteadOf git@github.com: -git submodule update --init --recursive -``` - ---- - -### Step 1: Start All Three Services - -Now that submodules are initialized, let's start all three services. - -Open **three separate terminal windows**. - -#### Terminal 1: Start IdentityServer (Token Service) - -```bash -cd TokenService/Duende-IdentityServer/src/Duende.STS.Identity -dotnet restore -dotnet run -``` - -**Wait for:** -``` -Now listening on: https://localhost:44310 -``` - -**Verify:** -Open browser to `https://localhost:44310` → Should see IdentityServer login page - -**Troubleshooting:** -- **Error: "Port already in use"** → Kill existing process: `netstat -ano | findstr :44310` then `taskkill /PID /F` -- **Error: "Unable to configure HTTPS"** → Run `dotnet dev-certs https --trust` - ---- - -#### Terminal 2: Start API Resource (Backend) - -```bash -cd ApiResources/TalentManagement-API -dotnet restore -dotnet run -``` - -**Wait for:** -``` -Now listening on: https://localhost:44378 -``` - -**Verify:** -Open browser to `https://localhost:44378/swagger` → Should see Swagger UI with API endpoints - -**Troubleshooting:** -- **Error: "Database connection failed"** → Check `appsettings.json` connection string -- **Error: "IdentityServer authority unreachable"** → Ensure IdentityServer (Terminal 1) is running - ---- - -#### Terminal 3: Start Angular Client (Frontend) - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management -npm install # Only needed first time -npm start -``` - -**Wait for:** -``` -✔ Browser application bundle generation complete. -** Angular Live Development Server is listening on localhost:4200 -``` - -**Verify:** -Open browser to `http://localhost:4200` → Should see Angular dashboard - -**Troubleshooting:** -- **Error: "npm not found"** → Install Node.js from https://nodejs.org -- **Error: "Module not found"** → Delete `node_modules` and run `npm install` again - ---- - -### Step 2: Test Authentication Flow - -#### 2.1 Open DevTools Before Login - -1. Open `http://localhost:4200` in browser -2. Press **F12** to open DevTools -3. Go to **Network** tab -4. Ensure **Preserve log** is checked - -#### 2.2 Perform Login - -1. Click **Sign In** (top-right user menu) -2. You'll be redirected to IdentityServer (`https://localhost:44310`) -3. Enter credentials: - - **Username:** `ashtyn1` - - **Password:** `Pa$$word123` -4. Click **Login** -5. You'll be redirected back to Angular (`http://localhost:4200`) - -#### 2.3 Observe Network Activity - -In DevTools Network tab, you should see: - -1. **authorize** request to IdentityServer → Returns authorization code -2. **token** request to IdentityServer → Exchanges code for access token -3. **Employees** request to API → Uses Bearer token in Authorization header - -#### 2.4 Inspect an API Request - -1. In Network tab, find request to `https://localhost:44378/api/v1/Employees` -2. Click on it -3. Go to **Headers** tab -4. Find **Request Headers** section -5. Look for: `Authorization: Bearer eyJhbGc...` - -**This is the JWT access token!** We'll decode this in LAB-02. - ---- - -### Step 3: Make Your First Code Change - -Now let's modify the Angular dashboard title to verify hot-reload works. - -#### 3.1 Open Dashboard Component - -**File:** `Clients/TalentManagement-Angular-Material/talent-management/src/app/routes/dashboard/dashboard.component.ts` - -**Navigate to:** -- If using VS Code: Press `Ctrl+P` → Type `dashboard.component.ts` → Press Enter -- Or browse folders: `Clients` → `TalentManagement-Angular-Material` → `talent-management` → `src` → `app` → `routes` → `dashboard` - -#### 3.2 Modify the Page Title - -**Find this line (around line 23):** -```typescript -pageTitle = 'Dashboard'; -``` - -**Change it to:** -```typescript -pageTitle = 'Dashboard - Welcome to CAT Pattern!'; -``` - -**Save the file** (`Ctrl+S`) - -#### 3.3 Observe Hot-Reload - -Watch your browser at `http://localhost:4200`: - -- Angular automatically detects the change -- Page reloads within 2-3 seconds -- Dashboard title now shows: **"Dashboard - Welcome to CAT Pattern!"** - -**Success!** You've verified hot-reload works. - ---- - -### Step 4: Navigate to Employee List - -#### 4.1 Open Employees Page - -In Angular app, click: -**Employees** (in left sidebar) → You should see employee list with table - -#### 4.2 Inspect Table Data - -In DevTools Network tab: -1. Find request to `https://localhost:44378/api/v1/Employees` -2. Click on it -3. Go to **Response** tab -4. See JSON data with employee records - -**Example response:** -```json -{ - "succeeded": true, - "message": null, - "errors": null, - "value": [ - { - "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "firstName": "John", - "lastName": "Doe", - "email": "john.doe@example.com", - ... - } - ], - "pageNumber": 1, - "pageSize": 10, - "totalRecords": 50 -} -``` - ---- - -### Step 5: Explore Project Structure - -#### 5.1 Angular Client Structure - -``` -Clients/TalentManagement-Angular-Material/talent-management/ -├── src/ -│ ├── app/ -│ │ ├── core/ # Core services (auth, interceptors) -│ │ │ ├── authentication/ # OIDC auth service, guards -│ │ │ └── interceptors/ # HTTP interceptors (add Bearer token) -│ │ ├── routes/ # Feature routes (dashboard, employees) -│ │ ├── shared/ # Shared components, pipes -│ │ ├── services/ # API services (employee, department) -│ │ ├── models/ # TypeScript interfaces -│ │ └── config/ # Auth configuration -│ └── environments/ # Environment configs (URLs, clientId) -``` - -#### 5.2 API Resource Structure - -``` -ApiResources/TalentManagement-API/ -├── TalentManagementAPI.Domain/ # Entities, enums, value objects -├── TalentManagementAPI.Application/ # Commands, queries, DTOs -├── TalentManagementAPI.Infrastructure.Persistence/ # EF Core, repositories -└── TalentManagementAPI.WebApi/ # Controllers, startup -``` - -#### 5.3 Token Service Structure - -``` -TokenService/Duende-IdentityServer/ -└── src/ - ├── Duende.STS.Identity/ # Login UI, OIDC endpoints - ├── Duende.Admin/ # Admin UI (https://localhost:44303) - └── Duende.Admin.Api/ # Admin API -``` - ---- - -## ✅ Verification Checklist - -Use this checklist to confirm you completed all steps: - -### Repository & Submodules -- [ ] Repository cloned successfully -- [ ] All submodule folders contain files (not empty) -- [ ] `git submodule status` shows commits for all three submodules -- [ ] Can navigate into each submodule folder - -### Services Running -- [ ] IdentityServer running at `https://localhost:44310` -- [ ] API running at `https://localhost:44378` -- [ ] Swagger UI accessible at `https://localhost:44378/swagger` -- [ ] Angular running at `http://localhost:4200` - -### Authentication -- [ ] Can click "Sign In" in Angular -- [ ] Redirected to IdentityServer login page -- [ ] Can login with `ashtyn1` / `Pa$$word123` -- [ ] Redirected back to Angular dashboard -- [ ] Can see employee list (authenticated) - -### DevTools Inspection -- [ ] Opened DevTools Network tab -- [ ] Observed `authorize` request to IdentityServer -- [ ] Observed `token` request to IdentityServer -- [ ] Found `Authorization: Bearer eyJ...` header in API requests - -### Code Change -- [ ] Modified dashboard title in `dashboard.component.ts` -- [ ] Saved file and observed hot-reload -- [ ] New title displayed in browser - -### Navigation -- [ ] Can navigate to Employees page -- [ ] Employee table loads with data -- [ ] Can see API response in DevTools - ---- - -## 🐛 Troubleshooting - -### Issue: IdentityServer won't start - -**Symptoms:** -- Error: "Failed to bind to address https://localhost:44310" -- Terminal shows port conflict - -**Solutions:** -1. Check if port already in use: - ```bash - # Windows - netstat -ano | findstr :44310 - - # Mac/Linux - lsof -i :44310 - ``` -2. Kill existing process or change port in `Properties/launchSettings.json` - ---- - -### Issue: Angular shows blank page - -**Symptoms:** -- Browser shows white screen -- Console shows errors about missing modules - -**Solutions:** -1. Check browser console (F12) for errors -2. Verify `npm install` completed successfully -3. Delete `node_modules` folder and run `npm install` again -4. Check that API and IdentityServer are running - ---- - -### Issue: "401 Unauthorized" when accessing API - -**Symptoms:** -- Employee list doesn't load -- Network tab shows 401 status code -- Console shows CORS errors - -**Solutions:** -1. Verify IdentityServer is running (Terminal 1) -2. Check `environment.ts` has correct `identityServerUrl`: `https://localhost:44310` -3. Check `appsettings.json` in API has correct Authority: `https://localhost:44310` -4. Clear browser cache and try logging in again - ---- - -### Issue: Hot-reload not working - -**Symptoms:** -- Changed code but browser doesn't update -- Have to manually refresh browser - -**Solutions:** -1. Check that Angular dev server is still running (Terminal 3) -2. Look for compilation errors in terminal -3. Try saving file again (`Ctrl+S`) -4. If still not working, stop Angular (`Ctrl+C`) and restart with `npm start` - ---- - -## 🎉 Success Criteria - -You've successfully completed LAB-01 if: - -✅ All three services start without errors -✅ You can login and see authenticated content -✅ You can inspect HTTP requests and see Bearer tokens -✅ You made a code change and saw hot-reload in action -✅ You can navigate the project folders confidently - ---- - -## 🚀 Next Steps - -### Immediate Next Lab - -**[LAB-02: Inspect JWT Tokens with DevTools](LAB-02-inspect-tokens.md)** - -In the next lab, you'll: -- Decode the JWT access token you saw in this lab -- Understand token structure and claims -- Modify token lifetime and test expiration -- Debug authentication issues like a pro - -### Alternative Path - -If you want to dive deeper into architecture before more labs: -- Review [Part 2: Token Service Deep Dive](../02-token-service-deep-dive.md) -- Review [Part 3: API Resource Deep Dive](../03-api-resource-deep-dive.md) - ---- - -## 💡 What You Learned - -### Technical Skills -- **How Git submodules work** - Each component in its own repository -- **How to initialize submodules** - Using `git submodule update --init --recursive` -- How to start multiple services for full-stack development -- How to use browser DevTools Network tab effectively -- How hot-reload works in Angular -- How JWT Bearer tokens are sent in HTTP headers -- How to navigate a multi-repository project structure - -### CAT Pattern Concepts -- **Client** (Angular) makes HTTP requests with Bearer token -- **API Resource** (Web API) validates token and returns data -- **Token Service** (IdentityServer) issues tokens after authentication -- All three components must be running for the app to work -- **Git submodules** keep each tier in separate repositories (independent development) - -### Development Workflow -- **Initialize submodules first** - Required before any development work -- Always start services in order: IdentityServer → API → Angular -- Use DevTools to debug HTTP requests and responses -- Hot-reload speeds up development (no manual refresh needed) -- Each submodule can be developed independently by different teams - ---- - -## 🆘 Need Help? - -### If You're Stuck -1. Check the [Troubleshooting section](#-troubleshooting) above -2. Review [Part 1: Foundation](../01-foundation.md) tutorial -3. Check [Solution Document](solutions/lab-01-solution.md) for complete answers -4. Post in GitHub Discussions with your specific error message - -### Common Questions - -**Q: Do I need to restart services when switching labs?** -A: No, keep them running. Only restart if you change configuration files. - -**Q: Can I use different ports?** -A: Yes, but update `environment.ts`, `appsettings.json`, and `identityserverdata.json` consistently. - -**Q: Where are user credentials stored?** -A: In IdentityServer database (seeded from `identitydata.json`). More in LAB-02. - ---- - -## 🎯 Bonus Challenges - -Want to go further? Try these optional tasks: - -### Challenge 1: Change the Welcome Message -**Difficulty:** Easy -- Find the dashboard component HTML template -- Change "Welcome back!" to a custom message -- Observe hot-reload - -**Hint:** Look for `dashboard.component.html` - -### Challenge 2: Add a Console Log -**Difficulty:** Easy -- Add `console.log('Dashboard loaded!')` in `ngOnInit()` of dashboard component -- Save and check browser console (F12) - -**Hint:** Find the `ngOnInit()` lifecycle hook - -### Challenge 3: Inspect ID Token -**Difficulty:** Medium -- In DevTools, find the token response from IdentityServer -- Look for `id_token` in response (separate from `access_token`) -- Copy it to https://jwt.io and decode -- What claims do you see? - -**Hint:** ID token contains user identity info (name, email, roles) - -### Challenge 4: Explore IdentityServer Admin -**Difficulty:** Medium -- Navigate to `https://localhost:44303` -- Login with `admin` / `Pa$$word123` -- Explore Clients, API Resources, Users -- Find the `TalentManagement` client configuration - -**Hint:** This is covered in [Part 2](../02-token-service-deep-dive.md) - -### Challenge 5: Understand Submodule Structure -**Difficulty:** Medium -- Run `git remote -v` in the main repository -- Navigate into `Clients/TalentManagement-Angular-Material` -- Run `git remote -v` inside the submodule -- Notice it points to a different repository! -- Check current branch: `git branch` -- Try: `git log --oneline -5` to see submodule's commit history - -**What you'll discover:** -- Each submodule is a complete Git repository -- Submodules point to specific commits (detached HEAD) -- Main repository only stores references to submodule commits -- Teams can work on submodules independently - -**Hint:** See [Part 5: Advanced Topics](../05-advanced-topics.md) for Git submodules workflow - ---- - -**Congratulations!** 🎉 You've completed LAB-01 and verified your development environment works correctly. - -**Ready for the next challenge?** Continue to **[LAB-02: Inspect JWT Tokens](LAB-02-inspect-tokens.md)** - ---- - -*Part of the [CAT Pattern Tutorial Series](../TUTORIAL.md)* -*Last Updated: February 2026* diff --git a/docs/labs/LAB-02-inspect-tokens.md b/docs/labs/LAB-02-inspect-tokens.md deleted file mode 100644 index 8b89562..0000000 --- a/docs/labs/LAB-02-inspect-tokens.md +++ /dev/null @@ -1,603 +0,0 @@ -# LAB-02: Inspect JWT Tokens with DevTools - -## 🎯 Objective - -Understand OAuth 2.0 and OIDC authentication by inspecting actual JWT tokens in your browser, learning how tokens are structured and used for API authorization. - -**What you'll accomplish:** -- ✅ Capture and inspect access tokens from HTTP requests -- ✅ Decode JWT tokens to see their contents -- ✅ Understand token structure (header, payload, signature) -- ✅ Modify token lifetime and test expiration behavior -- ✅ Debug authentication issues using DevTools -- ✅ Learn the difference between access tokens and ID tokens - ---- - -## 📋 Prerequisites - -**Before Starting This Lab:** - -- ✅ Completed [LAB-01: Verify Setup](LAB-01-verify-setup.md) -- ✅ All three services running (IdentityServer, API, Angular) -- ✅ Successfully logged in at least once -- ✅ Familiar with browser DevTools (F12) -- ✅ Read [Part 2: Token Service Deep Dive](../02-token-service-deep-dive.md) (recommended) - ---- - -## ⏱️ Duration - -**20-25 minutes** - ---- - -## 🚀 Steps - -### Step 1: Capture an Access Token - -#### 1.1 Prepare DevTools - -1. Open browser to `http://localhost:4200` -2. Press **F12** to open DevTools -3. Go to **Network** tab -4. Check **Preserve log** checkbox -5. Clear existing network logs (trash icon) - -#### 1.2 Login to Application - -1. If already logged in, logout first (user menu → Logout) -2. Click **Sign In** (top-right user menu) -3. Login with credentials: - - **Username:** `ashtyn1` - - **Password:** `Pa$$word123` -4. After redirect back to Angular, observe Network tab - -#### 1.3 Find Token Exchange Request - -In Network tab, look for these requests in order: - -**Request 1: Authorization Request** -- URL: `https://localhost:44310/connect/authorize` -- Method: GET -- Purpose: Initiates login, returns authorization code - -**Request 2: Token Exchange** (This is what we want!) -- URL: `https://localhost:44310/connect/token` -- Method: POST -- Purpose: Exchanges authorization code for tokens - -**Click on the `token` request** → Go to **Response** tab - -#### 1.4 Examine Token Response - -You should see JSON response containing: - -```json -{ - "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1...", - "expires_in": 3600, - "token_type": "Bearer", - "scope": "openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write", - "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1..." -} -``` - -**Key fields:** -- `access_token` - Used to access protected APIs -- `id_token` - Contains user identity information -- `expires_in` - Token lifetime in seconds (default: 3600 = 1 hour) -- `scope` - Permissions granted by this token - ---- - -### Step 2: Decode the Access Token - -#### 2.1 Copy Access Token - -1. In the token response, find the `access_token` value -2. **Right-click** on the token string → **Copy value** -3. The token should start with `eyJ...` (this is Base64 encoded) - -**Example token (shortened):** -``` -eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMxMCIsIm5iZiI6MTcwNzMyNzYwMCwiZXhwIjoxNzA3MzMxMjAwLCJhdWQiOiJhcHAuYXBpLnRhbGVudG1hbmFnZW1lbnQiLCJjbGllbnRfaWQiOiJUYWxlbnRNYW5hZ2VtZW50Iiwic3ViIjoiMjQ4Mjg5NzYxMDAxIiwic2NvcGUiOlsiYXBwLmFwaS50YWxlbnRtYW5hZ2VtZW50LnJlYWQiLCJhcHAuYXBpLnRhbGVudG1hbmFnZW1lbnQud3JpdGUiXSwicm9sZSI6WyJIUkFkbWluIiwiTWFuYWdlciJdLCJuYW1lIjoiQXNodHluIERvZSIsImVtYWlsIjoiYXNodHluMUBleGFtcGxlLmNvbSJ9.signature_data_here -``` - -#### 2.2 Visit JWT.io Decoder - -1. Open new browser tab -2. Go to **https://jwt.io** -3. Paste your access token into the **"Encoded"** section (left side) -4. The **"Decoded"** section (right side) automatically shows the token contents - -#### 2.3 Examine Token Structure - -A JWT has three parts separated by dots (`.`): - -``` -[HEADER].[PAYLOAD].[SIGNATURE] -``` - -**HEADER (Algorithm & Key ID):** -```json -{ - "alg": "RS256", - "kid": "1234567890", - "typ": "at+jwt" -} -``` -- `alg` - Algorithm used to sign token (RSA with SHA-256) -- `kid` - Key ID used to verify signature -- `typ` - Token type (at+jwt = Access Token as JWT) - -**PAYLOAD (Claims):** -```json -{ - "iss": "https://localhost:44310", - "nbf": 1707327600, - "exp": 1707331200, - "aud": "app.api.talentmanagement", - "client_id": "TalentManagement", - "sub": "248289761001", - "scope": [ - "app.api.talentmanagement.read", - "app.api.talentmanagement.write", - "openid", - "profile", - "email", - "roles" - ], - "role": ["HRAdmin", "Manager"], - "name": "Ashtyn Doe", - "email": "ashtyn1@example.com" -} -``` - -**Key Claims:** -- `iss` (issuer) - Who issued this token (IdentityServer URL) -- `aud` (audience) - Who this token is for (API resource name) -- `sub` (subject) - Unique user ID -- `exp` (expiration) - When token expires (Unix timestamp) -- `nbf` (not before) - When token becomes valid -- `scope` - What permissions this token grants -- `role` - User's roles -- `name`, `email` - User profile information - -**SIGNATURE:** -- Cryptographic signature that proves token wasn't tampered with -- Generated using IdentityServer's private key -- Verified by API using IdentityServer's public key - ---- - -### Step 3: Inspect API Requests with Bearer Token - -#### 3.1 Navigate to Employees Page - -In Angular app (keep DevTools open): -1. Click **Employees** in left sidebar -2. Watch Network tab for new requests - -#### 3.2 Find API Request - -Look for request to: -- URL: `https://localhost:44378/api/v1/Employees` -- Method: GET -- Status: 200 OK - -#### 3.3 Examine Authorization Header - -Click on the request → **Headers** tab → **Request Headers** section - -Find: -``` -Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI... -``` - -**This is how APIs receive the access token!** - -The Angular HTTP interceptor automatically adds this header to every API request. - -**File Reference:** See how this works in: -`Clients/TalentManagement-Angular-Material/talent-management/src/app/core/interceptors/auth.interceptor.ts` - -#### 3.4 Compare Token - -Copy the token after "Bearer " and paste into jwt.io. - -**Verify:** -- It's the same access token from Step 2 -- It has the required scopes: `app.api.talentmanagement.read` -- It hasn't expired yet (check `exp` claim) - ---- - -### Step 4: Decode the ID Token - -#### 4.1 Find ID Token in Token Response - -Go back to the `/connect/token` request response. - -Find the `id_token` field (separate from `access_token`). - -#### 4.2 Decode ID Token - -1. Copy the `id_token` value -2. Paste into https://jwt.io -3. Examine the payload - -**ID Token Payload Example:** -```json -{ - "iss": "https://localhost:44310", - "aud": "TalentManagement", - "sub": "248289761001", - "name": "Ashtyn Doe", - "given_name": "Ashtyn", - "family_name": "Doe", - "email": "ashtyn1@example.com", - "email_verified": true, - "role": ["HRAdmin", "Manager"], - "iat": 1707327600, - "exp": 1707327900, - "auth_time": 1707327600, - "amr": ["pwd"] -} -``` - -#### 4.3 Compare Access Token vs ID Token - -| Aspect | Access Token | ID Token | -|--------|--------------|----------| -| **Purpose** | Grant API access | Prove user identity | -| **Audience** | API (`app.api.talentmanagement`) | Client (`TalentManagement`) | -| **Contains** | Scopes, permissions | User identity claims | -| **Lifetime** | Longer (1 hour default) | Shorter (5 min default) | -| **Used By** | API validates it | Client validates it | - ---- - -### Step 5: Test Token Expiration - -Now let's see what happens when a token expires. - -#### 5.1 Modify Token Lifetime - -1. Open `TokenService/Duende-IdentityServer/src/Duende.Admin/identityserverdata.json` -2. Find the `TalentManagement` client configuration -3. Find `AccessTokenLifetime: 3600` (1 hour in seconds) -4. Change to: `AccessTokenLifetime: 60` (1 minute) -5. Save the file - -**File location:** `TokenService/Duende-IdentityServer/src/Duende.Admin/identityserverdata.json` (around line 460) - -#### 5.2 Restart IdentityServer - -1. Go to Terminal 1 (IdentityServer) -2. Press **Ctrl+C** to stop -3. Run `dotnet run` to restart -4. Wait for: `Now listening on: https://localhost:44310` - -#### 5.3 Login with New Token - -1. In Angular app, logout (if logged in) -2. Login again with `ashtyn1` / `Pa$$word123` -3. Check DevTools Network → `/connect/token` response -4. Verify: `"expires_in": 60` (not 3600) - -#### 5.4 Wait for Token to Expire - -1. Stay on Employees page (or any page) -2. **Wait 1 minute and 10 seconds** (to ensure token expired) -3. Click to a different page or refresh the list -4. Watch Network tab - -**What happens:** -- API request returns **401 Unauthorized** -- Angular shows error or redirects to login -- This is because the access token expired! - -#### 5.5 Restore Original Token Lifetime - -1. Open `identityserverdata.json` again -2. Change `AccessTokenLifetime` back to: `3600` -3. Save file -4. Restart IdentityServer (Ctrl+C, then `dotnet run`) - ---- - -### Step 6: Understand Token Validation - -#### 6.1 How API Validates Tokens - -When the API receives a request with `Authorization: Bearer `, it: - -1. **Extracts token** from Authorization header -2. **Fetches public key** from IdentityServer (`/.well-known/openid-configuration`) -3. **Verifies signature** using public key (ensures token wasn't tampered) -4. **Checks claims:** - - `iss` matches IdentityServer URL - - `aud` matches API resource name - - `exp` hasn't passed (token not expired) - - `nbf` has passed (token is valid now) -5. **Checks scopes** match what endpoint requires -6. **Returns 200 OK** if valid, or **401 Unauthorized** if invalid - -**File Reference:** See JWT validation configuration in: -`ApiResources/TalentManagement-API/Program.cs` (around line 60-80) - -#### 6.2 Test IdentityServer Discovery Endpoint - -Open browser to: -``` -https://localhost:44310/.well-known/openid-configuration -``` - -This shows IdentityServer's metadata that APIs use for token validation: -- `issuer` - IdentityServer URL -- `jwks_uri` - Where to get public keys for signature verification -- `token_endpoint` - Where to exchange authorization code for token -- `userinfo_endpoint` - Where to get user information -- Supported scopes, grant types, algorithms - ---- - -## ✅ Verification Checklist - -Use this checklist to confirm you completed all steps: - -### Token Capture -- [ ] Opened DevTools Network tab -- [ ] Logged in and observed `/connect/authorize` request -- [ ] Found `/connect/token` request with token response -- [ ] Identified `access_token` and `id_token` in response - -### Token Decoding -- [ ] Copied access token to jwt.io -- [ ] Examined token header (algorithm, key ID) -- [ ] Examined token payload (issuer, audience, subject, scopes, roles) -- [ ] Understood token structure (header.payload.signature) - -### Token Usage -- [ ] Found API request with `Authorization: Bearer` header -- [ ] Confirmed same access token is used for API calls -- [ ] Decoded ID token and compared with access token - -### Token Expiration -- [ ] Modified `AccessTokenLifetime` to 60 seconds -- [ ] Restarted IdentityServer -- [ ] Observed token expiration after 1 minute -- [ ] Saw 401 Unauthorized error -- [ ] Restored original token lifetime (3600 seconds) - -### Token Validation -- [ ] Accessed `/.well-known/openid-configuration` endpoint -- [ ] Understood how API validates tokens - ---- - -## 🐛 Troubleshooting - -### Issue: Can't find `/connect/token` request - -**Symptoms:** -- Network tab doesn't show token request -- Only see authorize request - -**Solutions:** -1. Ensure **Preserve log** is checked in DevTools -2. Clear network logs and login again -3. Look for XHR or Fetch requests (filter in Network tab) - ---- - -### Issue: jwt.io shows "Invalid Signature" - -**Symptoms:** -- jwt.io shows signature verification failed (red) - -**Solution:** -This is **normal and expected**! jwt.io doesn't have IdentityServer's public key, so it can't verify the signature. The signature is still valid - only IdentityServer and your API can verify it. - -**What the red warning means:** -- jwt.io is warning you it couldn't verify -- Your API **can** verify because it has the public key -- The token is still valid and secure - ---- - -### Issue: Token has already expired - -**Symptoms:** -- Decoded token shows `exp` timestamp in the past -- Can't test with the token - -**Solution:** -1. Logout and login again to get a fresh token -2. Work quickly after login (tokens expire in 1 hour) -3. Or increase token lifetime temporarily for testing - ---- - -### Issue: 401 Unauthorized immediately after login - -**Symptoms:** -- Login succeeds but API calls fail -- Token looks valid - -**Solutions:** -1. **Check API is running** - Verify Terminal 2 shows API on port 44378 -2. **Check IdentityServer is running** - Verify Terminal 1 shows port 44310 -3. **Check scopes match** - Token must have required scope for API -4. **Check audience** - Token `aud` must match API resource name -5. **Clear browser cache** - Old tokens might be cached - ---- - -### Issue: IdentityServer won't restart after config change - -**Symptoms:** -- Error when restarting IdentityServer -- Configuration invalid - -**Solution:** -1. Check JSON syntax in `identityserverdata.json` -2. Ensure no trailing commas -3. Verify `AccessTokenLifetime` is a number (not string) -4. Restore from backup if needed - ---- - -## 💡 What You Learned - -### OAuth 2.0 & OIDC Concepts -- **Authorization Code Flow with PKCE** - How SPAs get tokens securely -- **Token Exchange** - Trading authorization code for access token -- **Access Token vs ID Token** - Different purposes and audiences -- **JWT Structure** - Header, payload, signature -- **Token Claims** - Standard claims (iss, aud, sub, exp) and custom claims (roles, name) - -### Debugging Skills -- **How to capture tokens** in DevTools Network tab -- **How to decode JWT tokens** using jwt.io -- **How to inspect HTTP headers** for Authorization -- **How to test token expiration** by modifying lifetime -- **How to troubleshoot authentication** using browser tools - -### Security Concepts -- **Token Signing** - How signatures prevent tampering -- **Token Validation** - What APIs check when verifying tokens -- **Token Expiration** - Why short lifetimes improve security -- **Scopes** - How permissions are encoded in tokens -- **Discovery Endpoint** - How clients and APIs find IdentityServer metadata - -### Development Workflow -- Tokens are issued by IdentityServer during login -- Angular stores tokens in memory (not localStorage) -- HTTP interceptor adds Bearer token to API requests automatically -- APIs validate tokens on every request -- Expired tokens return 401 Unauthorized - ---- - -## 🚀 Next Steps - -### Immediate Next Lab - -**[LAB-03: Extend API with New Property](LAB-03-extend-api.md)** - -In the next lab, you'll: -- Add a new "Notes" field to the Employee entity -- Create and apply Entity Framework migrations -- Update commands and handlers -- Test your changes using Swagger and tokens! - -### Deepen Your Understanding - -- Read [Part 2: Token Service Deep Dive](../02-token-service-deep-dive.md) for complete OAuth 2.0 theory -- Read [Part 3: API Resource Deep Dive](../03-api-resource-deep-dive.md) to understand JWT validation in the API - ---- - -## 🎯 Bonus Challenges - -### Challenge 1: Inspect Refresh Token Flow -**Difficulty:** Medium - -Angular uses silent refresh to get new access tokens without re-login. - -**Tasks:** -1. Stay logged in for more than 1 hour -2. Watch DevTools Network for `/connect/token` requests -3. Look for `grant_type: refresh_token` in request body -4. Observe new access token issued without redirect to login page - -**What you'll learn:** How refresh tokens enable long sessions without constant re-authentication - ---- - -### Challenge 2: Test Invalid Token -**Difficulty:** Medium - -See what happens when you send a tampered token. - -**Tasks:** -1. Copy a valid access token from DevTools -2. Paste into jwt.io -3. Change a claim (e.g., change role from "HRAdmin" to "SuperAdmin") -4. Copy the modified token from jwt.io -5. Use DevTools → Network tab → Right-click API request → Edit and Resend -6. Replace Authorization header with modified token -7. Observe API response - -**Expected:** 401 Unauthorized (signature validation fails) - -**What you'll learn:** Why token signatures are critical for security - ---- - -### Challenge 3: Compare User Claims -**Difficulty:** Easy - -**Tasks:** -1. Login as `ashtyn1` -2. Decode access token and note the roles -3. Logout -4. Try logging in as different user (if you create one in IdentityServer Admin) -5. Compare tokens - different `sub`, different roles? - -**What you'll learn:** How tokens differ per user - ---- - -### Challenge 4: Explore Scope-Based Authorization -**Difficulty:** Advanced - -**Tasks:** -1. Open `identityserverdata.json` -2. Remove `app.api.talentmanagement.write` from `AllowedScopes` -3. Restart IdentityServer -4. Login and get new token -5. Decode token - verify write scope is missing -6. Try to POST/PUT/DELETE in Swagger -7. Observe 403 Forbidden (even though authenticated) - -**Restore:** Add write scope back and restart - -**What you'll learn:** Difference between authentication (401) and authorization (403) - ---- - -## 🆘 Need Help? - -### If You're Stuck -1. Check [Troubleshooting section](#-troubleshooting) above -2. Review [Part 2: Token Service Deep Dive](../02-token-service-deep-dive.md) -3. Check [Solution Document](solutions/lab-02-solution.md) for hints -4. Post in GitHub Discussions with your specific issue - -### Common Questions - -**Q: Why does jwt.io show "Invalid Signature" in red?** -A: jwt.io doesn't have IdentityServer's public key. This is normal - your API can verify it. - -**Q: Can I decode tokens in production?** -A: You can decode the structure, but never log or expose token values in production. They're secrets! - -**Q: Why are token lifetimes so short?** -A: Security! If a token is stolen, it only works for 1 hour max. Refresh tokens allow longer sessions securely. - -**Q: Where are tokens stored in Angular?** -A: In memory (using `angular-oauth2-oidc` library). Not in localStorage for security. - ---- - -**Congratulations!** 🎉 You now understand JWT tokens and OAuth 2.0 authentication at the HTTP level! - -**Ready to build API features?** Continue to **[LAB-03: Extend API](LAB-03-extend-api.md)** - ---- - -*Part of the [CAT Pattern Tutorial Series](../TUTORIAL.md)* -*Last Updated: February 2026* diff --git a/docs/labs/LAB-03-extend-api.md b/docs/labs/LAB-03-extend-api.md deleted file mode 100644 index b761ab9..0000000 --- a/docs/labs/LAB-03-extend-api.md +++ /dev/null @@ -1,82 +0,0 @@ -# LAB-03: Extend API with New Property - -## 🎯 Objective - -Learn the **full vertical slice** of Clean Architecture by adding a new "Notes" field to the Employee entity. This lab demonstrates the complete workflow: **Domain → Migration → Application → Testing**. - ---- - -## 📋 Prerequisites - -**Completed:** -- ✅ [LAB-01: Verify Setup](LAB-01-verify-setup.md) -- ✅ [LAB-02: Inspect JWT Tokens](LAB-02-inspect-tokens.md) - -**Time Required:** 30-40 minutes - ---- - -## 🚀 Steps - -### Step 1: Add Property to Domain Entity - -File: `TalentManagementAPI.Domain/Entities/Employee.cs` - -Add after the `Phone` property: - -```csharp -/// -/// Internal notes about the employee (max 500 characters). -/// -public string Notes { get; set; } -``` - -### Step 2: Create Migration - -```bash -cd ApiResources/TalentManagement-API -dotnet ef migrations add AddNotesToEmployee --project TalentManagementAPI.Infrastructure.Persistence --startup-project TalentManagementAPI.WebApi -``` - -### Step 3: Apply Migration - -```bash -dotnet ef database update --project TalentManagementAPI.Infrastructure.Persistence --startup-project TalentManagementAPI.WebApi -``` - -### Step 4: Update Commands - -Add to `CreateEmployeeCommand.cs` and `UpdateEmployeeCommand.cs`: - -```csharp -public string Notes { get; set; } -``` - -### Step 5: Add Validation - -In `CreateEmployeeCommandValidator.cs`: - -```csharp -RuleFor(v => v.Notes) - .MaximumLength(500).WithMessage("Notes must not exceed 500 characters."); -``` - -### Step 6: Test in Swagger - -1. Rebuild: `dotnet build` -2. Run: `dotnet run --project TalentManagementAPI.WebApi` -3. Test at: `https://localhost:44378/swagger` - ---- - -## ✅ Verification - -- [ ] Migration created and applied -- [ ] API builds without errors -- [ ] Swagger shows `notes` field -- [ ] Can create employee with notes -- [ ] Validation works (501+ chars rejected) - ---- - -*[LAB-04: Build Angular Component →](LAB-04-build-component.md)* diff --git a/docs/labs/LAB-04-build-component.md b/docs/labs/LAB-04-build-component.md deleted file mode 100644 index abe6aeb..0000000 --- a/docs/labs/LAB-04-build-component.md +++ /dev/null @@ -1,258 +0,0 @@ -# LAB-04: Build Angular Search Component - -## 🎯 Objective - -Learn how to create a **reusable search component** in Angular 20 with Material Design that integrates with the TalentManagement API. - ---- - -## 📋 Prerequisites - -**Completed:** -- ✅ [LAB-01: Verify Setup](LAB-01-verify-setup.md) -- ✅ [LAB-02: Inspect JWT Tokens](LAB-02-inspect-tokens.md) -- ✅ [LAB-03: Extend API](LAB-03-extend-api.md) - -**Time Required:** 45-60 minutes - ---- - -## 🚀 Steps - -### Step 1: Generate Component - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management -ng generate component features/employees/employee-search --standalone -``` - -### Step 2: Component TypeScript - -File: `employee-search.component.ts` - -```typescript -import { Component, OnInit, inject, output } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; -import { MatButtonModule } from '@angular/material/button'; -import { MatTableModule } from '@angular/material/table'; -import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; -import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; - -import { EmployeeService } from '../../../services/api/employee.service'; -import { Employee } from '../../../models'; - -@Component({ - selector: 'app-employee-search', - standalone: true, - imports: [ - CommonModule, - ReactiveFormsModule, - MatFormFieldModule, - MatInputModule, - MatSelectModule, - MatButtonModule, - MatTableModule, - MatPaginatorModule - ], - templateUrl: './employee-search.component.html', - styleUrls: ['./employee-search.component.scss'] -}) -export class EmployeeSearchComponent implements OnInit { - private fb = inject(FormBuilder); - private employeeService = inject(EmployeeService); - - employeeSelected = output(); - - searchForm!: FormGroup; - employees: Employee[] = []; - - displayedColumns: string[] = ['employeeNumber', 'fullName', 'email', 'departmentName']; - - totalRecords = 0; - pageSize = 10; - pageNumber = 1; - isLoading = false; - - ngOnInit(): void { - this.searchForm = this.fb.group({ - searchTerm: [''] - }); - - this.setupSearchSubscription(); - this.searchEmployees(); - } - - private setupSearchSubscription(): void { - this.searchForm.get('searchTerm')!.valueChanges.pipe( - debounceTime(300), - distinctUntilChanged() - ).subscribe(() => { - this.pageNumber = 1; - this.searchEmployees(); - }); - } - - searchEmployees(): void { - this.isLoading = true; - - const queryParams: any = { - pageNumber: this.pageNumber, - pageSize: this.pageSize - }; - - const searchTerm = this.searchForm.get('searchTerm')!.value; - if (searchTerm) { - queryParams.lastName = searchTerm; - } - - this.employeeService.getAll(queryParams).subscribe({ - next: (response) => { - this.isLoading = false; - if (response.succeeded) { - this.employees = response.value; - this.totalRecords = response.totalRecords; - } - }, - error: () => { - this.isLoading = false; - } - }); - } - - onPageChange(event: PageEvent): void { - this.pageNumber = event.pageIndex + 1; - this.pageSize = event.pageSize; - this.searchEmployees(); - } - - onEmployeeClick(employee: Employee): void { - this.employeeSelected.emit(employee); - } - - getFullName(employee: Employee): string { - return `${employee.firstName} ${employee.lastName}`; - } -} -``` - -### Step 3: Component Template - -File: `employee-search.component.html` - -```html -
-
- - Search by name - - -
- -
Loading...
- - - - - - - - - - - - - - - - - - - - - - - - -
Employee #{{ employee.employeeNumber }}Name{{ getFullName(employee) }}Email{{ employee.email }}Department{{ employee.departmentName }}
- - - -
-``` - -### Step 4: Component Styles - -File: `employee-search.component.scss` - -```scss -.search-container { - padding: 24px; -} - -.clickable-row { - cursor: pointer; - &:hover { - background-color: rgba(0, 0, 0, 0.04); - } -} - -mat-form-field { - width: 100%; - max-width: 400px; -} -``` - -### Step 5: Add Route - -In `src/app/routes/routes.ts`: - -```typescript -{ - path: 'employees', - component: EmployeesPageComponent, - canActivate: [authGuard] -} -``` - -### Step 6: Test - -1. Start all services -2. Navigate to: `http://localhost:4200/employees` -3. Test search (type in search box) -4. Test pagination -5. Test row click - ---- - -## ✅ Verification - -- [ ] Component generated successfully -- [ ] Search input with debouncing works -- [ ] Table displays employee data -- [ ] Pagination works -- [ ] Row click emits event -- [ ] Loading state shows - ---- - -## 🎓 Key Concepts - -- **Standalone components** - No NgModule needed -- **inject()** - Modern dependency injection -- **output()** - Signal-based component outputs -- **debounceTime()** - Wait before API call -- **Material Design** - Professional UI components - ---- - -*[LAB-05: Write Unit Tests →](LAB-05-write-tests.md)* diff --git a/docs/labs/LAB-05-write-tests.md b/docs/labs/LAB-05-write-tests.md deleted file mode 100644 index 523f332..0000000 --- a/docs/labs/LAB-05-write-tests.md +++ /dev/null @@ -1,929 +0,0 @@ -# LAB-05: Write Unit Tests - -## 🎯 Objective - -Learn **test-driven development (TDD)** practices by writing unit tests for your .NET API validators, Angular services, and Angular components. This lab demonstrates the **testing pyramid** approach with practical examples. - ---- - -## 📋 Prerequisites - -**Completed:** -- ✅ [LAB-01: Verify Setup](LAB-01-verify-setup.md) -- ✅ [LAB-02: Inspect JWT Tokens](LAB-02-inspect-tokens.md) -- ✅ [LAB-03: Extend API](LAB-03-extend-api.md) -- ✅ [LAB-04: Build Angular Component](LAB-04-build-component.md) - -**Time Required:** 45-60 minutes - ---- - -## 🧪 Testing Pyramid Overview - -**Unit Tests (70%)** — Fast, isolated, tests single components -- **Tools:** xUnit, FluentAssertions, Jasmine -- **Focus:** Business logic, validators, services - -**Integration Tests (20%)** — Test component interaction -- **Tools:** WebApplicationFactory, TestBed -- **Focus:** API endpoints, component + service - -**E2E Tests (10%)** — Full user flows -- **Tools:** Playwright, Cypress -- **Focus:** Login → Navigate → CRUD operations - -**This lab focuses on Unit Tests (70% of your testing strategy).** - ---- - -## 🚀 Part 1: .NET API Unit Tests - -### Step 1: Create Test Project (if not exists) - -```bash -cd ApiResources/TalentManagement-API - -# Create xUnit test project -dotnet new xunit -n TalentManagementAPI.Application.Tests - -# Add project reference -dotnet add TalentManagementAPI.Application.Tests/TalentManagementAPI.Application.Tests.csproj reference TalentManagementAPI.Application/TalentManagementAPI.Application.csproj - -# Add to solution -dotnet sln add TalentManagementAPI.Application.Tests/TalentManagementAPI.Application.Tests.csproj -``` - -### Step 2: Install Test Dependencies - -```bash -cd TalentManagementAPI.Application.Tests - -dotnet add package FluentAssertions -dotnet add package Moq -dotnet add package xunit -dotnet add package xunit.runner.visualstudio -``` - -### Step 3: Write Validator Tests - -Create file: `TalentManagementAPI.Application.Tests/Employees/CreateEmployeeCommandValidatorTests.cs` - -```csharp -using FluentAssertions; -using TalentManagementAPI.Application.Features.Employees.Commands.CreateEmployee; -using Xunit; - -namespace TalentManagementAPI.Application.Tests.Employees -{ - public class CreateEmployeeCommandValidatorTests - { - private readonly CreateEmployeeCommandValidator _validator; - - public CreateEmployeeCommandValidatorTests() - { - _validator = new CreateEmployeeCommandValidator(); - } - - [Fact] - public void Should_Have_Error_When_FirstName_Is_Empty() - { - // Arrange - var command = new CreateEmployeeCommand - { - FirstName = "", - LastName = "Doe", - Email = "john@example.com", - DateOfBirth = DateTime.Now.AddYears(-25), - HireDate = DateTime.Now - }; - - // Act - var result = _validator.Validate(command); - - // Assert - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == "FirstName"); - } - - [Fact] - public void Should_Have_Error_When_LastName_Is_Empty() - { - // Arrange - var command = new CreateEmployeeCommand - { - FirstName = "John", - LastName = "", - Email = "john@example.com", - DateOfBirth = DateTime.Now.AddYears(-25), - HireDate = DateTime.Now - }; - - // Act - var result = _validator.Validate(command); - - // Assert - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == "LastName"); - } - - [Theory] - [InlineData("test@example.com", true)] - [InlineData("invalid-email", false)] - [InlineData("", false)] - [InlineData("test@", false)] - [InlineData("@example.com", false)] - public void Should_Validate_Email_Format(string email, bool expectedValid) - { - // Arrange - var command = new CreateEmployeeCommand - { - FirstName = "John", - LastName = "Doe", - Email = email, - DateOfBirth = DateTime.Now.AddYears(-25), - HireDate = DateTime.Now - }; - - // Act - var result = _validator.Validate(command); - - // Assert - result.IsValid.Should().Be(expectedValid); - if (!expectedValid) - { - result.Errors.Should().Contain(e => e.PropertyName == "Email"); - } - } - - [Fact] - public void Should_Have_Error_When_FirstName_Exceeds_50_Characters() - { - // Arrange - var command = new CreateEmployeeCommand - { - FirstName = new string('A', 51), - LastName = "Doe", - Email = "john@example.com", - DateOfBirth = DateTime.Now.AddYears(-25), - HireDate = DateTime.Now - }; - - // Act - var result = _validator.Validate(command); - - // Assert - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => - e.PropertyName == "FirstName" && - e.ErrorMessage.Contains("50 characters")); - } - - [Fact] - public void Should_Pass_When_All_Required_Fields_Valid() - { - // Arrange - var command = new CreateEmployeeCommand - { - FirstName = "John", - LastName = "Doe", - Email = "john.doe@example.com", - DateOfBirth = DateTime.Now.AddYears(-30), - HireDate = DateTime.Now.AddDays(-100) - }; - - // Act - var result = _validator.Validate(command); - - // Assert - result.IsValid.Should().BeTrue(); - result.Errors.Should().BeEmpty(); - } - - [Fact] - public void Should_Validate_Notes_MaxLength_500() - { - // Arrange - var command = new CreateEmployeeCommand - { - FirstName = "John", - LastName = "Doe", - Email = "john@example.com", - DateOfBirth = DateTime.Now.AddYears(-25), - HireDate = DateTime.Now, - Notes = new string('A', 501) - }; - - // Act - var result = _validator.Validate(command); - - // Assert - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => - e.PropertyName == "Notes" && - e.ErrorMessage.Contains("500 characters")); - } - } -} -``` - -### Step 4: Run .NET Tests - -```bash -cd ApiResources/TalentManagement-API - -# Run all tests -dotnet test - -# Run with detailed output -dotnet test --verbosity normal - -# Run specific test class -dotnet test --filter "FullyQualifiedName~CreateEmployeeCommandValidatorTests" -``` - -**Expected Output:** -``` -Starting test execution, please wait... -A total of 1 test files matched the specified pattern. - -Passed! - Failed: 0, Passed: 7, Skipped: 0, Total: 7, Duration: 245 ms -``` - ---- - -## 🚀 Part 2: Angular Service Tests - -### Step 5: Create Service Test File - -Create file: `Clients/TalentManagement-Angular-Material/talent-management/src/app/core/services/employee.service.spec.ts` - -```typescript -import { TestBed } from '@angular/core/testing'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { provideHttpClient } from '@angular/common/http'; -import { EmployeeService } from './employee.service'; -import { Employee } from '../models/employee.model'; -import { PagedResponse } from '../models/paged-response.model'; - -describe('EmployeeService', () => { - let service: EmployeeService; - let httpMock: HttpTestingController; - const apiUrl = 'https://localhost:44378/api/v1/employees'; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - EmployeeService, - provideHttpClient(), - provideHttpClientTesting() - ] - }); - - service = TestBed.inject(EmployeeService); - httpMock = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - // Verify no outstanding HTTP requests - httpMock.verify(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - describe('getEmployees', () => { - it('should return paged employees', () => { - // Arrange - const mockResponse: PagedResponse = { - data: [ - { - id: 1, - employeeNumber: 'EMP001', - firstName: 'John', - lastName: 'Doe', - email: 'john@example.com', - phone: '555-1234', - hireDate: new Date('2020-01-01'), - departmentId: 1, - departmentName: 'IT', - positionId: 1, - positionName: 'Developer' - } - ], - pageNumber: 1, - pageSize: 10, - totalPages: 1, - totalCount: 1 - }; - - // Act - service.getEmployees(1, 10, '').subscribe(response => { - // Assert - expect(response).toEqual(mockResponse); - expect(response.data.length).toBe(1); - expect(response.data[0].firstName).toBe('John'); - }); - - // Assert HTTP request - const req = httpMock.expectOne(request => - request.url.includes(apiUrl) && - request.params.get('pageNumber') === '1' && - request.params.get('pageSize') === '10' - ); - expect(req.request.method).toBe('GET'); - - // Respond with mock data - req.flush(mockResponse); - }); - - it('should include search term in query params', () => { - // Arrange - const searchTerm = 'John'; - - // Act - service.getEmployees(1, 10, searchTerm).subscribe(); - - // Assert - const req = httpMock.expectOne(request => - request.url.includes(apiUrl) && - request.params.get('searchTerm') === searchTerm - ); - expect(req.request.method).toBe('GET'); - - req.flush({ data: [], pageNumber: 1, pageSize: 10, totalPages: 0, totalCount: 0 }); - }); - }); - - describe('getEmployee', () => { - it('should return employee by id', () => { - // Arrange - const mockEmployee: Employee = { - id: 1, - employeeNumber: 'EMP001', - firstName: 'John', - lastName: 'Doe', - email: 'john@example.com', - phone: '555-1234', - hireDate: new Date('2020-01-01'), - departmentId: 1, - departmentName: 'IT', - positionId: 1, - positionName: 'Developer' - }; - - // Act - service.getEmployee(1).subscribe(employee => { - // Assert - expect(employee).toEqual(mockEmployee); - expect(employee.id).toBe(1); - }); - - // Assert HTTP request - const req = httpMock.expectOne(`${apiUrl}/1`); - expect(req.request.method).toBe('GET'); - - req.flush(mockEmployee); - }); - }); - - describe('createEmployee', () => { - it('should create new employee', () => { - // Arrange - const newEmployee = { - firstName: 'Jane', - lastName: 'Smith', - email: 'jane@example.com', - phone: '555-5678', - hireDate: new Date('2024-01-01'), - departmentId: 2, - positionId: 2 - }; - const createdId = 123; - - // Act - service.createEmployee(newEmployee).subscribe(id => { - // Assert - expect(id).toBe(createdId); - }); - - // Assert HTTP request - const req = httpMock.expectOne(apiUrl); - expect(req.request.method).toBe('POST'); - expect(req.request.body).toEqual(newEmployee); - - req.flush(createdId); - }); - }); - - describe('updateEmployee', () => { - it('should update existing employee', () => { - // Arrange - const updatedEmployee = { - id: 1, - firstName: 'John', - lastName: 'Doe Updated', - email: 'john.updated@example.com', - phone: '555-9999', - hireDate: new Date('2020-01-01'), - departmentId: 1, - positionId: 1 - }; - - // Act - service.updateEmployee(1, updatedEmployee).subscribe(); - - // Assert HTTP request - const req = httpMock.expectOne(`${apiUrl}/1`); - expect(req.request.method).toBe('PUT'); - expect(req.request.body).toEqual(updatedEmployee); - - req.flush(null); - }); - }); - - describe('deleteEmployee', () => { - it('should delete employee by id', () => { - // Arrange - const employeeId = 1; - - // Act - service.deleteEmployee(employeeId).subscribe(); - - // Assert HTTP request - const req = httpMock.expectOne(`${apiUrl}/${employeeId}`); - expect(req.request.method).toBe('DELETE'); - - req.flush(null); - }); - }); - - describe('error handling', () => { - it('should handle HTTP error', () => { - // Arrange - const errorMessage = 'Server error'; - - // Act - service.getEmployee(999).subscribe( - () => fail('should have failed with 404 error'), - (error) => { - // Assert - expect(error.status).toBe(404); - } - ); - - // Assert HTTP request - const req = httpMock.expectOne(`${apiUrl}/999`); - req.flush(errorMessage, { status: 404, statusText: 'Not Found' }); - }); - }); -}); -``` - -### Step 6: Run Angular Service Tests - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management - -# Run all tests -npm test - -# Run tests in headless mode (CI) -npm test -- --watch=false --browsers=ChromeHeadless - -# Run specific test file -npm test -- --include='**/employee.service.spec.ts' -``` - ---- - -## 🚀 Part 3: Angular Component Tests - -### Step 7: Create Component Test File - -Create file: `Clients/TalentManagement-Angular-Material/talent-management/src/app/features/employees/employee-form/employee-form.component.spec.ts` - -```typescript -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { provideHttpClient } from '@angular/common/http'; -import { provideRouter } from '@angular/router'; -import { of, throwError } from 'rxjs'; -import { EmployeeFormComponent } from './employee-form.component'; -import { EmployeeService } from '../../../core/services/employee.service'; -import { DepartmentService } from '../../../core/services/department.service'; -import { PositionService } from '../../../core/services/position.service'; - -describe('EmployeeFormComponent', () => { - let component: EmployeeFormComponent; - let fixture: ComponentFixture; - let employeeService: jasmine.SpyObj; - let departmentService: jasmine.SpyObj; - let positionService: jasmine.SpyObj; - - beforeEach(async () => { - // Create spy objects - const employeeSpy = jasmine.createSpyObj('EmployeeService', [ - 'createEmployee', - 'updateEmployee', - 'getEmployee' - ]); - const departmentSpy = jasmine.createSpyObj('DepartmentService', ['getDepartments']); - const positionSpy = jasmine.createSpyObj('PositionService', ['getPositions']); - - await TestBed.configureTestingModule({ - imports: [EmployeeFormComponent, ReactiveFormsModule], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - provideRouter([]), - { provide: EmployeeService, useValue: employeeSpy }, - { provide: DepartmentService, useValue: departmentSpy }, - { provide: PositionService, useValue: positionSpy } - ] - }).compileComponents(); - - employeeService = TestBed.inject(EmployeeService) as jasmine.SpyObj; - departmentService = TestBed.inject(DepartmentService) as jasmine.SpyObj; - positionService = TestBed.inject(PositionService) as jasmine.SpyObj; - - // Setup default spy returns - departmentService.getDepartments.and.returnValue(of([])); - positionService.getPositions.and.returnValue(of([])); - - fixture = TestBed.createComponent(EmployeeFormComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize form with empty values', () => { - fixture.detectChanges(); - - expect(component.form.get('firstName')?.value).toBe(''); - expect(component.form.get('lastName')?.value).toBe(''); - expect(component.form.get('email')?.value).toBe(''); - }); - - it('should mark form as invalid when required fields are empty', () => { - fixture.detectChanges(); - - expect(component.form.valid).toBeFalse(); - expect(component.form.get('firstName')?.errors?.['required']).toBeTruthy(); - expect(component.form.get('lastName')?.errors?.['required']).toBeTruthy(); - expect(component.form.get('email')?.errors?.['required']).toBeTruthy(); - }); - - it('should validate email format', () => { - fixture.detectChanges(); - - const emailControl = component.form.get('email'); - - emailControl?.setValue('invalid-email'); - expect(emailControl?.errors?.['email']).toBeTruthy(); - - emailControl?.setValue('valid@example.com'); - expect(emailControl?.errors).toBeNull(); - }); - - it('should validate first name max length (50)', () => { - fixture.detectChanges(); - - const firstNameControl = component.form.get('firstName'); - - firstNameControl?.setValue('A'.repeat(51)); - expect(firstNameControl?.errors?.['maxlength']).toBeTruthy(); - - firstNameControl?.setValue('A'.repeat(50)); - expect(firstNameControl?.errors).toBeNull(); - }); - - it('should call createEmployee on submit when creating new employee', () => { - fixture.detectChanges(); - - component.form.patchValue({ - firstName: 'John', - lastName: 'Doe', - email: 'john@example.com', - phone: '555-1234', - dateOfBirth: new Date('1990-01-01'), - hireDate: new Date('2020-01-01'), - departmentId: 1, - positionId: 1 - }); - - employeeService.createEmployee.and.returnValue(of(123)); - - component.onSubmit(); - - expect(employeeService.createEmployee).toHaveBeenCalledWith( - jasmine.objectContaining({ - firstName: 'John', - lastName: 'Doe', - email: 'john@example.com' - }) - ); - }); - - it('should not submit when form is invalid', () => { - fixture.detectChanges(); - - component.form.patchValue({ - firstName: '', - lastName: '', - email: 'invalid' - }); - - component.onSubmit(); - - expect(employeeService.createEmployee).not.toHaveBeenCalled(); - expect(employeeService.updateEmployee).not.toHaveBeenCalled(); - }); - - it('should handle create employee error', () => { - fixture.detectChanges(); - - component.form.patchValue({ - firstName: 'John', - lastName: 'Doe', - email: 'john@example.com', - phone: '555-1234', - dateOfBirth: new Date('1990-01-01'), - hireDate: new Date('2020-01-01'), - departmentId: 1, - positionId: 1 - }); - - const errorResponse = { status: 400, statusText: 'Bad Request' }; - employeeService.createEmployee.and.returnValue(throwError(() => errorResponse)); - - component.onSubmit(); - - // Verify error was handled (component should still be valid for retry) - expect(component.form.enabled).toBeTrue(); - }); - - it('should load departments on init', () => { - const mockDepartments = [ - { id: 1, name: 'IT', description: 'Information Technology' }, - { id: 2, name: 'HR', description: 'Human Resources' } - ]; - - departmentService.getDepartments.and.returnValue(of(mockDepartments)); - - fixture.detectChanges(); - - expect(departmentService.getDepartments).toHaveBeenCalled(); - }); - - it('should load positions on init', () => { - const mockPositions = [ - { id: 1, name: 'Developer', description: 'Software Developer' }, - { id: 2, name: 'Manager', description: 'Team Manager' } - ]; - - positionService.getPositions.and.returnValue(of(mockPositions)); - - fixture.detectChanges(); - - expect(positionService.getPositions).toHaveBeenCalled(); - }); -}); -``` - -### Step 8: Run Angular Component Tests - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management - -# Run all tests -npm test - -# Run specific component test -npm test -- --include='**/employee-form.component.spec.ts' - -# Run with code coverage -npm test -- --code-coverage -``` - -**Coverage report will be generated in:** `coverage/index.html` - ---- - -## ✅ Verification Checklist - -### .NET API Tests -- [ ] All validator tests pass -- [ ] Tests verify required fields -- [ ] Tests verify max length constraints -- [ ] Tests verify email format validation -- [ ] Tests verify Notes field (from LAB-03) -- [ ] `dotnet test` shows all green - -### Angular Service Tests -- [ ] Service is created successfully -- [ ] GET requests return paged data -- [ ] POST requests create employees -- [ ] PUT requests update employees -- [ ] DELETE requests remove employees -- [ ] HTTP errors are handled -- [ ] `npm test` shows all green - -### Angular Component Tests -- [ ] Component is created successfully -- [ ] Form initializes with empty values -- [ ] Required field validation works -- [ ] Email format validation works -- [ ] Max length validation works -- [ ] Submit calls service methods -- [ ] Invalid forms don't submit -- [ ] Dropdowns load departments and positions - ---- - -## 🎓 Key Concepts Learned - -### Testing Best Practices - -**✅ AAA Pattern (Arrange-Act-Assert)** -- **Arrange:** Set up test data and mocks -- **Act:** Execute the code being tested -- **Assert:** Verify the expected outcome - -**✅ Test Naming Convention** -- `Should_ExpectedBehavior_When_Condition` -- Example: `Should_Have_Error_When_FirstName_Is_Empty` - -**✅ One Assertion Per Test (Ideal)** -- Each test should verify one specific behavior -- Makes failures easier to diagnose - -**✅ Test Independence** -- Each test should run independently -- No shared state between tests -- Use `beforeEach` for setup - -### .NET Testing Tools - -**xUnit** — Testing framework (alternative: NUnit, MSTest) -**FluentAssertions** — Readable assertions -**Moq** — Mocking framework for dependencies -**Theory/InlineData** — Data-driven tests - -### Angular Testing Tools - -**TestBed** — Angular testing utility -**HttpTestingController** — Mock HTTP requests -**Jasmine** — Testing framework and assertion library -**Spy Objects** — Mock services and verify calls - ---- - -## 🔍 Common Testing Scenarios - -### Test Data Validation -```csharp -[Fact] -public void Should_Validate_Field() -{ - var command = new CreateEmployeeCommand { Field = InvalidValue }; - var result = _validator.Validate(command); - result.IsValid.Should().BeFalse(); -} -``` - -### Test HTTP Service Call -```typescript -it('should make GET request', () => { - service.getData().subscribe(); - const req = httpMock.expectOne(url); - expect(req.request.method).toBe('GET'); - req.flush(mockData); -}); -``` - -### Test Form Validation -```typescript -it('should validate form', () => { - component.form.patchValue({ field: '' }); - expect(component.form.valid).toBeFalse(); -}); -``` - -### Test Service Method Call -```typescript -it('should call service method', () => { - serviceSpy.method.and.returnValue(of(result)); - component.action(); - expect(serviceSpy.method).toHaveBeenCalled(); -}); -``` - ---- - -## 💡 Tips and Best Practices - -### Writing Effective Tests - -1. **Test behavior, not implementation** - - Focus on what the code does, not how it does it - - Don't test private methods directly - -2. **Use meaningful test names** - - Test names should describe the expected behavior - - Anyone should understand what's being tested - -3. **Keep tests simple and focused** - - One test = one behavior - - Avoid complex logic in tests - -4. **Use test data builders** - - Create helper methods for test data - - Reduces duplication - -5. **Mock external dependencies** - - HTTP calls, database access, file system - - Tests should be fast and isolated - -### Running Tests in CI/CD - -```bash -# .NET API tests -dotnet test --configuration Release --no-build --verbosity normal - -# Angular tests (headless) -npm run test -- --watch=false --browsers=ChromeHeadless --code-coverage -``` - ---- - -## 🚨 Troubleshooting - -### .NET Tests - -**Issue:** Tests not discovered -```bash -# Rebuild solution -dotnet clean -dotnet build -dotnet test -``` - -**Issue:** FluentAssertions not found -```bash -dotnet add package FluentAssertions -``` - -### Angular Tests - -**Issue:** "NullInjectorError: No provider for HttpClient" -```typescript -// Add to TestBed providers: -providers: [ - provideHttpClient(), - provideHttpClientTesting() -] -``` - -**Issue:** "Can't resolve all parameters for Router" -```typescript -// Add to imports: -provideRouter([]) -``` - -**Issue:** Tests timeout -```typescript -// Increase timeout in jasmine -beforeEach(async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; -}); -``` - ---- - -## 📖 Next Steps - -Now that you've learned unit testing: - -1. **Add more test coverage** - - Aim for 70-80% code coverage - - Focus on critical business logic - -2. **Write integration tests** - - Test API endpoints with WebApplicationFactory - - Test component + service integration - -3. **Add E2E tests** - - Use Playwright for critical user flows - - Test login → CRUD operations - -4. **Set up CI/CD** - - Run tests automatically on every commit - - Block merges if tests fail - ---- - -*[LAB-06: Docker Containerization →](LAB-06-docker-deployment.md)* diff --git a/docs/labs/LAB-06-docker-deployment.md b/docs/labs/LAB-06-docker-deployment.md deleted file mode 100644 index 7e17113..0000000 --- a/docs/labs/LAB-06-docker-deployment.md +++ /dev/null @@ -1,858 +0,0 @@ -# LAB-06: Docker Containerization - -## 🎯 Objective - -Learn how to **containerize all three CAT Pattern components** (Angular Client, .NET API, IdentityServer) using Docker and Docker Compose. This lab demonstrates production-ready containerization with multi-stage builds, networking, and orchestration. - ---- - -## 📋 Prerequisites - -**Completed:** -- ✅ [LAB-01: Verify Setup](LAB-01-verify-setup.md) -- ✅ [LAB-02: Inspect JWT Tokens](LAB-02-inspect-tokens.md) -- ✅ [LAB-03: Extend API](LAB-03-extend-api.md) -- ✅ [LAB-04: Build Angular Component](LAB-04-build-component.md) -- ✅ [LAB-05: Write Unit Tests](LAB-05-write-tests.md) - -**Required Tools:** -- **Docker Desktop** — [Download](https://www.docker.com/products/docker-desktop/) -- **Docker Compose** — Included with Docker Desktop - -**Time Required:** 60-75 minutes - ---- - -## 🐳 Why Docker? - -**Benefits:** -✅ **Consistent Environments** — Dev, staging, production use identical containers -✅ **Easy Deployment** — Single command deploys entire stack -✅ **Isolation** — Each service runs in its own container -✅ **Scalability** — Scale services independently -✅ **Version Control** — Infrastructure as code with Dockerfiles - ---- - -## 🚀 Part 1: Containerize Angular Client - -### Step 1: Create Angular Dockerfile - -Create file: `Clients/TalentManagement-Angular-Material/talent-management/Dockerfile` - -```dockerfile -# Stage 1: Build Angular application -FROM node:20-alpine AS build - -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install dependencies (npm ci for clean install) -RUN npm ci - -# Copy source code -COPY . . - -# Build application for production -RUN npm run build -- --configuration production - -# Stage 2: Serve with Nginx -FROM nginx:alpine - -# Copy built application to nginx -COPY --from=build /app/dist/talent-management /usr/share/nginx/html - -# Copy nginx configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf - -# Expose port 80 -EXPOSE 80 - -# Start nginx -CMD ["nginx", "-g", "daemon off;"] -``` - -### Step 2: Create Nginx Configuration - -Create file: `Clients/TalentManagement-Angular-Material/talent-management/nginx.conf` - -```nginx -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - # Angular routing - serve index.html for all routes - location / { - try_files $uri $uri/ /index.html; - } - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; - - # Cache static assets (js, css, images) - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Don't cache index.html - location = /index.html { - add_header Cache-Control "no-cache, no-store, must-revalidate"; - expires 0; - } - - # Gzip compression - gzip on; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; -} -``` - -### Step 3: Create .dockerignore - -Create file: `Clients/TalentManagement-Angular-Material/talent-management/.dockerignore` - -``` -node_modules -dist -.angular -.vscode -.git -*.md -.editorconfig -.gitignore -``` - -### Step 4: Build and Test Angular Image - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management - -# Build Docker image -docker build -t talentmanagement-angular:latest . - -# Run container -docker run -d -p 4200:80 --name angular-client talentmanagement-angular:latest - -# Verify running -docker ps - -# Test in browser -# Open: http://localhost:4200 -``` - -### Step 5: Stop and Remove Test Container - -```bash -docker stop angular-client -docker rm angular-client -``` - ---- - -## 🚀 Part 2: Containerize .NET API - -### Step 6: Create API Dockerfile - -Create file: `ApiResources/TalentManagement-API/Dockerfile` - -```dockerfile -# Stage 1: Build -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build - -WORKDIR /src - -# Copy solution file if exists -COPY ["TalentManagement.sln", "./"] - -# Copy project files for restore -COPY ["TalentManagementAPI.Domain/TalentManagementAPI.Domain.csproj", "TalentManagementAPI.Domain/"] -COPY ["TalentManagementAPI.Application/TalentManagementAPI.Application.csproj", "TalentManagementAPI.Application/"] -COPY ["TalentManagementAPI.Infrastructure.Persistence/TalentManagementAPI.Infrastructure.Persistence.csproj", "TalentManagementAPI.Infrastructure.Persistence/"] -COPY ["TalentManagementAPI.Infrastructure.Shared/TalentManagementAPI.Infrastructure.Shared.csproj", "TalentManagementAPI.Infrastructure.Shared/"] -COPY ["TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj", "TalentManagementAPI.WebApi/"] - -# Restore dependencies -RUN dotnet restore "TalentManagementAPI.WebApi/TalentManagementAPI.WebApi.csproj" - -# Copy all source code -COPY . . - -# Build -WORKDIR "/src/TalentManagementAPI.WebApi" -RUN dotnet build "TalentManagementAPI.WebApi.csproj" -c Release -o /app/build - -# Stage 2: Publish -FROM build AS publish -RUN dotnet publish "TalentManagementAPI.WebApi.csproj" -c Release -o /app/publish /p:UseAppHost=false - -# Stage 3: Runtime -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final - -WORKDIR /app - -# Copy published files -COPY --from=publish /app/publish . - -# Expose ports -EXPOSE 80 -EXPOSE 443 - -# Set environment -ENV ASPNETCORE_URLS=http://+:80 - -# Start application -ENTRYPOINT ["dotnet", "TalentManagementAPI.WebApi.dll"] -``` - -### Step 7: Create API .dockerignore - -Create file: `ApiResources/TalentManagement-API/.dockerignore` - -``` -**/bin -**/obj -**/.vs -**/.vscode -**/*.user -**/.git -**/node_modules -**/appsettings.Development.json -**/Logs -``` - -### Step 8: Build and Test API Image - -```bash -cd ApiResources/TalentManagement-API - -# Build Docker image -docker build -t talentmanagement-api:latest . - -# Run container (without database for now) -docker run -d -p 5001:80 --name api-test talentmanagement-api:latest - -# Check logs -docker logs api-test - -# Stop and remove -docker stop api-test -docker rm api-test -``` - ---- - -## 🚀 Part 3: Containerize IdentityServer - -### Step 9: Create IdentityServer Dockerfile - -Create file: `TokenService/Duende-IdentityServer/Dockerfile` - -```dockerfile -# Stage 1: Build -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build - -WORKDIR /src - -# Copy project files -COPY ["src/Duende.STS.Identity/Duende.STS.Identity.csproj", "src/Duende.STS.Identity/"] -COPY ["src/Duende.Admin/Duende.Admin.csproj", "src/Duende.Admin/"] -COPY ["src/Duende.Admin.Api/Duende.Admin.Api.csproj", "src/Duende.Admin.Api/"] - -# Restore dependencies -RUN dotnet restore "src/Duende.STS.Identity/Duende.STS.Identity.csproj" - -# Copy all source -COPY . . - -# Build -WORKDIR "/src/src/Duende.STS.Identity" -RUN dotnet build "Duende.STS.Identity.csproj" -c Release -o /app/build - -# Stage 2: Publish -FROM build AS publish -RUN dotnet publish "Duende.STS.Identity.csproj" -c Release -o /app/publish /p:UseAppHost=false - -# Stage 3: Runtime -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final - -WORKDIR /app - -# Copy published files -COPY --from=publish /app/publish . - -# Expose ports -EXPOSE 80 -EXPOSE 443 - -# Set environment -ENV ASPNETCORE_URLS=http://+:80 - -# Start application -ENTRYPOINT ["dotnet", "Duende.STS.Identity.dll"] -``` - -### Step 10: Create IdentityServer .dockerignore - -Create file: `TokenService/Duende-IdentityServer/.dockerignore` - -``` -**/bin -**/obj -**/.vs -**/.vscode -**/*.user -**/.git -**/Logs -``` - ---- - -## 🚀 Part 4: Orchestrate with Docker Compose - -### Step 11: Create Docker Compose File - -Create file in root: `docker-compose.yml` - -```yaml -version: '3.8' - -services: - # SQL Server Database - sqlserver: - image: mcr.microsoft.com/mssql/server:2022-latest - container_name: talentmanagement-sqlserver - environment: - - ACCEPT_EULA=Y - - SA_PASSWORD=YourStrong@Password123 - - MSSQL_PID=Developer - ports: - - "1433:1433" - volumes: - - sqlserver-data:/var/opt/mssql - networks: - - talentmanagement-network - healthcheck: - test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "YourStrong@Password123" -Q "SELECT 1" - interval: 10s - timeout: 3s - retries: 10 - start_period: 40s - - # IdentityServer - identityserver: - build: - context: ./TokenService/Duende-IdentityServer - dockerfile: Dockerfile - container_name: talentmanagement-identityserver - environment: - - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://+:80 - - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=IdentityServerDb;User Id=sa;Password=YourStrong@Password123;TrustServerCertificate=True;Encrypt=False - ports: - - "44310:80" - depends_on: - sqlserver: - condition: service_healthy - networks: - - talentmanagement-network - restart: unless-stopped - - # Web API - api: - build: - context: ./ApiResources/TalentManagement-API - dockerfile: Dockerfile - container_name: talentmanagement-api - environment: - - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://+:80 - - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=TalentManagementDb;User Id=sa;Password=YourStrong@Password123;TrustServerCertificate=True;Encrypt=False - - IdentityServer__Authority=http://identityserver:80 - - IdentityServer__RequireHttpsMetadata=false - ports: - - "44378:80" - depends_on: - - sqlserver - - identityserver - networks: - - talentmanagement-network - restart: unless-stopped - - # Angular Client - angular: - build: - context: ./Clients/TalentManagement-Angular-Material/talent-management - dockerfile: Dockerfile - container_name: talentmanagement-angular - ports: - - "4200:80" - depends_on: - - api - - identityserver - networks: - - talentmanagement-network - restart: unless-stopped - -networks: - talentmanagement-network: - driver: bridge - -volumes: - sqlserver-data: -``` - -### Step 12: Update Angular Environment for Docker - -Update: `Clients/TalentManagement-Angular-Material/talent-management/src/environments/environment.prod.ts` - -```typescript -export const environment = { - production: true, - apiUrl: 'http://localhost:44378/api/v1', - identityServerUrl: 'http://localhost:44310', - clientId: 'TalentManagement', - scope: 'openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write', - responseType: 'code', - silentRenew: true, - useRefreshToken: true, - logLevel: 0 -}; -``` - -### Step 13: Start All Services with Docker Compose - -```bash -# Build and start all services -docker-compose up --build - -# Or run in detached mode (background) -docker-compose up -d --build -``` - -**Expected Output:** -``` -[+] Building 245.3s (52/52) FINISHED -[+] Running 5/5 - ✔ Network talentmanagement-network Created - ✔ Container talentmanagement-sqlserver Started - ✔ Container talentmanagement-identityserver Started - ✔ Container talentmanagement-api Started - ✔ Container talentmanagement-angular Started -``` - -### Step 14: Verify All Services Running - -```bash -# Check running containers -docker-compose ps - -# View logs for all services -docker-compose logs - -# View logs for specific service -docker-compose logs angular -docker-compose logs api -docker-compose logs identityserver - -# Follow logs in real-time -docker-compose logs -f -``` - -### Step 15: Test Containerized Application - -**URLs to Test:** -- **Angular:** http://localhost:4200 -- **API Swagger:** http://localhost:44378/swagger -- **IdentityServer:** http://localhost:44310 - -**Test Flow:** -1. Open http://localhost:4200 -2. Click "Login" -3. Login with: `ashtyn1` / `Pa$$word123` -4. Verify dashboard loads -5. Test CRUD operations - ---- - -## 🔍 Docker Commands Reference - -### Managing Containers - -```bash -# Start services -docker-compose up - -# Start in background -docker-compose up -d - -# Stop services -docker-compose down - -# Stop and remove volumes (clean slate) -docker-compose down -v - -# Restart specific service -docker-compose restart api - -# Rebuild specific service -docker-compose up -d --build api -``` - -### Viewing Logs - -```bash -# All service logs -docker-compose logs - -# Specific service -docker-compose logs api - -# Follow logs (real-time) -docker-compose logs -f - -# Last 100 lines -docker-compose logs --tail=100 - -# With timestamps -docker-compose logs -t -``` - -### Executing Commands in Containers - -```bash -# Execute shell in running container -docker exec -it talentmanagement-api /bin/bash - -# Run EF migrations in API container -docker exec talentmanagement-api dotnet ef database update - -# Check API health -docker exec talentmanagement-api curl http://localhost:80/health -``` - -### Inspecting Containers - -```bash -# View container details -docker inspect talentmanagement-api - -# View container stats (CPU, memory) -docker stats - -# View specific stats -docker stats talentmanagement-api -``` - -### Cleaning Up - -```bash -# Stop all containers -docker-compose down - -# Remove all containers and volumes -docker-compose down -v - -# Remove unused images -docker image prune -a - -# Remove all stopped containers -docker container prune - -# Full system cleanup -docker system prune -a --volumes -``` - ---- - -## ✅ Verification Checklist - -### Docker Images Built -- [ ] `talentmanagement-angular:latest` exists -- [ ] `talentmanagement-api:latest` exists -- [ ] `talentmanagement-identityserver:latest` exists -- [ ] SQL Server image pulled - -### Containers Running -- [ ] `docker-compose ps` shows all services "Up" -- [ ] No restart loops (check with `docker-compose ps`) -- [ ] SQL Server health check passes - -### Services Accessible -- [ ] Angular loads at http://localhost:4200 -- [ ] API Swagger loads at http://localhost:44378/swagger -- [ ] IdentityServer loads at http://localhost:44310 - -### Functionality Works -- [ ] Can login via IdentityServer -- [ ] Dashboard loads after login -- [ ] API calls return data -- [ ] CRUD operations work - -### Logs Clean -- [ ] No critical errors in `docker-compose logs` -- [ ] SQL Server connection successful -- [ ] IdentityServer database migrations applied -- [ ] API database migrations applied - ---- - -## 🚨 Troubleshooting - -### Issue: SQL Server Container Won't Start - -**Symptoms:** Container exits immediately - -**Solutions:** -```bash -# Check logs -docker-compose logs sqlserver - -# Ensure password meets requirements -# Must contain: uppercase, lowercase, number, special char -# Minimum 8 characters - -# Increase memory for Docker Desktop -# Docker Desktop → Settings → Resources → Memory (min 4GB) -``` - -### Issue: API Can't Connect to SQL Server - -**Symptoms:** "Cannot open database" error - -**Solutions:** -```bash -# Check SQL Server is healthy -docker-compose ps - -# Wait for health check to pass (up to 40s) - -# Check connection string -docker exec talentmanagement-api printenv ConnectionStrings__DefaultConnection - -# Manually run migrations -docker exec talentmanagement-api dotnet ef database update -``` - -### Issue: IdentityServer Discovery Fails - -**Symptoms:** Angular shows "invalid_issuer" error - -**Solutions:** -1. Ensure IdentityServer is running: - ```bash - docker-compose logs identityserver - ``` - -2. Check IdentityServer URL from Angular container: - ```bash - docker exec talentmanagement-angular wget -O- http://identityserver:80/.well-known/openid-configuration - ``` - -3. Verify `environment.prod.ts` uses correct URL - -### Issue: Angular Can't Reach API - -**Symptoms:** API calls fail with CORS errors - -**Solutions:** -1. Check API is running: - ```bash - docker-compose ps api - ``` - -2. Test API from Angular container: - ```bash - docker exec talentmanagement-angular wget -O- http://api:80/api/v1/employees - ``` - -3. Verify CORS configuration in API - -### Issue: Port Already in Use - -**Symptoms:** "Bind for 0.0.0.0:4200 failed: port is already allocated" - -**Solutions:** -```bash -# Find process using port -# Windows: -netstat -ano | findstr :4200 - -# Linux/Mac: -lsof -i :4200 - -# Kill process or change port in docker-compose.yml -ports: - - "4201:80" # Use different host port -``` - -### Issue: Container Builds Slowly - -**Solutions:** -```bash -# Use BuildKit for faster builds -export DOCKER_BUILDKIT=1 - -# Build with no cache (if stale cache) -docker-compose build --no-cache - -# Optimize .dockerignore files -# Exclude node_modules, bin, obj folders -``` - ---- - -## 🎓 Key Concepts Learned - -### Multi-Stage Builds - -**Why?** Smaller final images, faster deploys - -```dockerfile -# Build stage (large, has SDKs) -FROM node:20-alpine AS build -RUN npm run build - -# Runtime stage (small, only runtime) -FROM nginx:alpine -COPY --from=build /app/dist /usr/share/nginx/html -``` - -### Docker Networking - -**Bridge Network:** Containers can communicate by service name -- `identityserver` resolves to IdentityServer container IP -- `sqlserver` resolves to SQL Server container IP - -### Health Checks - -**Why?** Ensure dependencies are ready before starting dependent services - -```yaml -healthcheck: - test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "password" -Q "SELECT 1" - interval: 10s - retries: 10 -``` - -### Docker Volumes - -**Persistent Data:** SQL Server data survives container restarts - -```yaml -volumes: - - sqlserver-data:/var/opt/mssql -``` - -### Container Dependencies - -**Ordering:** Wait for SQL Server before starting API - -```yaml -depends_on: - sqlserver: - condition: service_healthy -``` - ---- - -## 💡 Production Best Practices - -### Security - -1. **Don't hardcode secrets** in docker-compose.yml - ```yaml - environment: - - SA_PASSWORD=${SQL_SA_PASSWORD} - ``` - -2. **Use secrets management** (Docker Secrets, Azure Key Vault) - -3. **Run as non-root user** in Dockerfile - ```dockerfile - USER appuser - ``` - -4. **Scan images for vulnerabilities** - ```bash - docker scan talentmanagement-api:latest - ``` - -### Performance - -1. **Layer caching:** Copy package files before source code - ```dockerfile - COPY package*.json ./ - RUN npm ci - COPY . . - ``` - -2. **Use alpine images** when possible (smaller, faster) - ```dockerfile - FROM node:20-alpine - ``` - -3. **Multi-stage builds:** Separate build and runtime - -### Monitoring - -1. **Container logs:** `docker-compose logs -f` -2. **Health endpoints:** Implement `/health` in all services -3. **Resource limits:** - ```yaml - deploy: - resources: - limits: - cpus: '0.5' - memory: 512M - ``` - ---- - -## 📖 Next Steps - -Now that you've containerized the application: - -1. **Push to Docker Hub** - ```bash - docker tag talentmanagement-api:latest yourusername/talentmanagement-api:latest - docker push yourusername/talentmanagement-api:latest - ``` - -2. **Deploy to cloud** (Azure Container Instances, AWS ECS, Google Cloud Run) - -3. **Set up CI/CD** to automatically build and push images - -4. **Implement Kubernetes** for production orchestration - -5. **Add monitoring** with Prometheus + Grafana - ---- - -## 🎉 Congratulations! - -You've successfully containerized the entire CAT Pattern application! - -**What you've accomplished:** -✅ Created Dockerfiles for all three tiers -✅ Built Docker images -✅ Orchestrated services with Docker Compose -✅ Set up container networking -✅ Configured SQL Server in a container -✅ Verified end-to-end functionality - -**You're now ready for:** -- Cloud deployment -- CI/CD pipelines -- Production orchestration -- Scaling and high availability - ---- - -*[← LAB-05: Write Unit Tests](LAB-05-write-tests.md) | [Labs Home](README.md)* diff --git a/docs/labs/README.md b/docs/labs/README.md deleted file mode 100644 index 92ef453..0000000 --- a/docs/labs/README.md +++ /dev/null @@ -1,280 +0,0 @@ -# Hands-On Labs: CAT Pattern Tutorial Series - -## 🎯 Overview - -These hands-on labs complement the main tutorial series by providing practical, step-by-step exercises using the **actual TalentManagement source code**. Each lab builds your skills progressively, from setup verification to advanced deployment scenarios. - ---- - -## 📋 Lab Structure - -Each lab follows this format: -- **Objective:** What you'll learn -- **Prerequisites:** What you need before starting -- **Duration:** Expected time to complete -- **Steps:** Detailed instructions with file paths -- **Verification Checklist:** How to confirm success -- **Troubleshooting:** Common issues and solutions -- **Bonus Challenges:** Optional advanced tasks - ---- - -## 🗂️ Lab Series - -### Foundation Labs - -#### [LAB-01: Verify Setup & First Code Change](LAB-01-verify-setup.md) ⭐ **START HERE** -**Duration:** 15 minutes -**Difficulty:** Beginner -**Goal:** Confirm your development environment works and make your first code change - -**You'll learn:** -- How to verify all three services are running correctly -- How to make a simple UI change and see hot-reload in action -- How to inspect network requests in browser DevTools -- How to navigate the project structure - -**Prerequisites:** -- Completed Part 1: Foundation tutorial -- All three services running (IdentityServer, API, Angular) - ---- - -### Authentication & Security Labs - -#### [LAB-02: Inspect JWT Tokens with DevTools](LAB-02-inspect-tokens.md) ⭐ **ESSENTIAL** -**Duration:** 20 minutes -**Difficulty:** Beginner -**Goal:** Understand OAuth 2.0/OIDC authentication by inspecting actual tokens - -**You'll learn:** -- How to capture access tokens from HTTP requests -- How to decode and inspect JWT token contents -- How to modify token lifetime and test expiration -- How to debug authentication issues using browser DevTools - -**Prerequisites:** -- Completed Part 2: Token Service Deep Dive tutorial -- LAB-01 completed -- Familiarity with browser DevTools - ---- - -### Backend Development Labs - -#### [LAB-03: Extend API with New Property](LAB-03-extend-api.md) ⭐ **ESSENTIAL** -**Duration:** 30 minutes -**Difficulty:** Intermediate -**Goal:** Learn the full vertical slice by adding a new "Notes" field to Employee - -**You'll learn:** -- How to extend domain entities -- How to create and apply Entity Framework migrations -- How to update commands, validators, and handlers -- How to test API changes using Swagger -- How Clean Architecture layers interact - -**Prerequisites:** -- Completed Part 3: API Resource Deep Dive tutorial -- LAB-01 and LAB-02 completed -- Basic C# and EF Core knowledge - ---- - -### Frontend Development Labs - -#### [LAB-04: Build Angular Search Component](LAB-04-build-component.md) ⭐ **ESSENTIAL** -**Duration:** 45 minutes -**Difficulty:** Intermediate -**Goal:** Create a reusable search component with Material Design - -**You'll learn:** -- How to generate standalone Angular components -- How to use Material Design form components -- How to implement component communication with @Output -- How to integrate with API services using RxJS -- How to handle query parameters and pagination - -**Prerequisites:** -- Completed Part 4: Angular Client Deep Dive tutorial -- LAB-03 completed (API must be working) -- Basic Angular and TypeScript knowledge - ---- - -### Testing & Quality Labs - -#### [LAB-05: Write Unit Tests](LAB-05-write-tests.md) 📝 **IMPORTANT** -**Duration:** 30 minutes -**Difficulty:** Intermediate -**Goal:** Implement test-driven development practices - -**You'll learn:** -- How to write FluentValidation tests for commands -- How to write Angular service tests with mocks -- How to write component tests using TestBed -- How to run tests and interpret results - -**Prerequisites:** -- Completed Part 3 and Part 4 tutorials -- LAB-03 and LAB-04 completed -- Understanding of unit testing concepts - ---- - -### DevOps & Deployment Labs - -#### [LAB-06: Docker Containerization](LAB-06-docker-deploy.md) 🐳 **VALUABLE** -**Duration:** 40 minutes -**Difficulty:** Advanced -**Goal:** Containerize the application using Docker - -**You'll learn:** -- How to build Docker images for each service -- How to create docker-compose configuration -- How to run the full stack in containers -- How to debug containerization issues -- How to prepare for production deployment - -**Prerequisites:** -- Completed Part 5: Advanced Topics tutorial -- Docker Desktop installed and running -- All previous labs completed - ---- - -## 🎓 Learning Paths - -### Path 1: Quick Start (Minimum Viable Understanding) -**Time:** 1-2 hours -**Labs:** LAB-01 → LAB-02 -**Outcome:** Environment working, basic understanding of authentication - -### Path 2: Full Stack Developer (Comprehensive) -**Time:** 3-4 hours -**Labs:** LAB-01 → LAB-02 → LAB-03 → LAB-04 -**Outcome:** Can build features across all three tiers - -### Path 3: Production Ready (Complete Mastery) -**Time:** 5-6 hours -**Labs:** All labs (LAB-01 through LAB-06) -**Outcome:** Ready to deploy and maintain the application - ---- - -## 📁 Folder Structure - -``` -docs/labs/ -├── README.md (this file) -├── LAB-01-verify-setup.md (Foundation) -├── LAB-02-inspect-tokens.md (Authentication) -├── LAB-03-extend-api.md (Backend) -├── LAB-04-build-component.md (Frontend) -├── LAB-05-write-tests.md (Testing) -├── LAB-06-docker-deploy.md (DevOps) -│ -└── solutions/ (Lab answers & hints) - ├── lab-01-solution.md - ├── lab-02-solution.md - ├── lab-03-solution.md - ├── lab-04-solution.md - ├── lab-05-solution.md - └── lab-06-solution.md -``` - ---- - -## 🆘 Getting Help - -### During Labs - -Each lab includes: -- **Verification Checklist:** Confirm you completed each step correctly -- **Troubleshooting Section:** Common errors and solutions -- **File Path References:** Exact locations in the codebase - -### If You Get Stuck - -1. **Check the Solutions folder:** `labs/solutions/lab-XX-solution.md` -2. **Review the main tutorial:** Ensure you understand the concepts -3. **Check GitHub Issues:** Someone may have encountered the same problem -4. **Ask in Discussions:** Share your issue with the community - ---- - -## 📊 Progress Tracking - -Use this checklist to track your lab completion: - -- [ ] **LAB-01:** Verify Setup & First Code Change -- [ ] **LAB-02:** Inspect JWT Tokens with DevTools -- [ ] **LAB-03:** Extend API with New Property -- [ ] **LAB-04:** Build Angular Search Component -- [ ] **LAB-05:** Write Unit Tests -- [ ] **LAB-06:** Docker Containerization - -**Completion Goal:** 🎉 All 6 labs = You're ready to build production CAT pattern applications! - ---- - -## 🔗 Related Resources - -### Tutorial Series -- [Part 1: Foundation](../01-foundation.md) -- [Part 2: Token Service Deep Dive](../02-token-service-deep-dive.md) -- [Part 3: API Resource Deep Dive](../03-api-resource-deep-dive.md) -- [Part 4: Angular Client Deep Dive](../04-angular-client-deep-dive.md) -- [Part 5: Advanced Topics](../05-advanced-topics.md) -- [Part 6: Real-World Features](../06-real-world-features.md) - -### Navigation Guides -- [Code Map: Quick Navigation](../CODE-MAP.md) - Find files quickly -- [Tutorial Home](../TUTORIAL.md) - Overview of entire series - -### Repository Resources -- [Main README](../../README.md) - Project overview -- [Setup Guide](../../SETUP-SUBMODULES.md) - Git submodules -- [CLAUDE.md](../../CLAUDE.md) - Developer guidance - ---- - -## 💡 Lab Development Philosophy - -### Why Hands-On Labs? - -**Reading teaches knowledge.** -**Building teaches skill.** - -These labs bridge the gap between understanding architecture and actually building features. Each lab: - -1. **Uses real source code** - No toy examples, work with the actual project -2. **Provides clear verification** - You know when you've succeeded -3. **Includes troubleshooting** - Common issues addressed proactively -4. **Builds progressively** - Each lab prepares you for the next -5. **Offers bonus challenges** - Extend your learning if you want to go deeper - -### Target Outcomes - -After completing these labs, you should be able to: -- ✅ Run and debug the application confidently -- ✅ Add new features to any of the three tiers -- ✅ Understand the flow of data through the system -- ✅ Test your changes effectively -- ✅ Deploy the application to production -- ✅ Explain the CAT pattern to your team - ---- - -## 🚀 Ready to Start? - -Begin with **[LAB-01: Verify Setup & First Code Change](LAB-01-verify-setup.md)** - -This lab ensures your environment is working correctly and gives you confidence that you're ready to proceed. - -**Good luck, and happy building!** 🎉 - ---- - -*Last Updated: February 2026* -*Part of the [CAT Pattern Tutorial Series](../TUTORIAL.md)* diff --git a/docs/labs/solutions/LAB-03-solution.md b/docs/labs/solutions/LAB-03-solution.md deleted file mode 100644 index 288aded..0000000 --- a/docs/labs/solutions/LAB-03-solution.md +++ /dev/null @@ -1,572 +0,0 @@ -# LAB-03 Solution: Extend API with New Property - -## Complete Solution Code - -This document provides the complete solution for [LAB-03: Extend API with New Property](../LAB-03-extend-api.md). - ---- - -## Step 1: Add Property to Domain Entity - -**File:** `ApiResources/TalentManagement-API/TalentManagementAPI.Domain/Entities/Employee.cs` - -**Add after the Phone property:** - -```csharp -/// -/// Internal notes about the employee (max 500 characters). -/// -public string Notes { get; set; } -``` - -**Complete Employee.cs (relevant section):** - -```csharp -namespace TalentManagementAPI.Domain.Entities -{ - public class Employee : AuditableBaseEntity - { - public string EmployeeNumber { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string Email { get; set; } - public string Phone { get; set; } - - /// - /// Internal notes about the employee (max 500 characters). - /// - public string Notes { get; set; } - - public DateTime DateOfBirth { get; set; } - public DateTime HireDate { get; set; } - public int DepartmentId { get; set; } - public Department Department { get; set; } - public int PositionId { get; set; } - public Position Position { get; set; } - } -} -``` - ---- - -## Step 2: Create Migration - -**Command:** - -```bash -cd ApiResources/TalentManagement-API -dotnet ef migrations add AddNotesToEmployee --project TalentManagementAPI.Infrastructure.Persistence --startup-project TalentManagementAPI.WebApi -``` - -**Expected Output:** - -``` -Build started... -Build succeeded. -Done. To undo this action, use 'ef migrations remove' -``` - -**Generated Migration File:** - -`TalentManagementAPI.Infrastructure.Persistence/Migrations/[Timestamp]_AddNotesToEmployee.cs` - -```csharp -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TalentManagementAPI.Infrastructure.Persistence.Migrations -{ - /// - public partial class AddNotesToEmployee : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Notes", - table: "Employees", - type: "nvarchar(500)", - maxLength: 500, - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Notes", - table: "Employees"); - } - } -} -``` - ---- - -## Step 3: Apply Migration - -**Command:** - -```bash -dotnet ef database update --project TalentManagementAPI.Infrastructure.Persistence --startup-project TalentManagementAPI.WebApi -``` - -**Expected Output:** - -``` -Build started... -Build succeeded. -Applying migration '20240208123456_AddNotesToEmployee'. -Done. -``` - -**Verify in Database:** - -```sql --- Check that Notes column was added -SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH -FROM INFORMATION_SCHEMA.COLUMNS -WHERE TABLE_NAME = 'Employees' AND COLUMN_NAME = 'Notes'; -``` - ---- - -## Step 4: Update Commands - -### CreateEmployeeCommand.cs - -**File:** `ApiResources/TalentManagement-API/TalentManagementAPI.Application/Features/Employees/Commands/CreateEmployee/CreateEmployeeCommand.cs` - -**Add property:** - -```csharp -public string Notes { get; set; } -``` - -**Complete CreateEmployeeCommand.cs:** - -```csharp -using MediatR; -using System; - -namespace TalentManagementAPI.Application.Features.Employees.Commands.CreateEmployee -{ - public class CreateEmployeeCommand : IRequest - { - public string EmployeeNumber { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string Email { get; set; } - public string Phone { get; set; } - public string Notes { get; set; } - public DateTime DateOfBirth { get; set; } - public DateTime HireDate { get; set; } - public int DepartmentId { get; set; } - public int PositionId { get; set; } - } -} -``` - -### UpdateEmployeeCommand.cs - -**File:** `ApiResources/TalentManagement-API/TalentManagementAPI.Application/Features/Employees/Commands/UpdateEmployee/UpdateEmployeeCommand.cs` - -**Add property:** - -```csharp -public string Notes { get; set; } -``` - -**Complete UpdateEmployeeCommand.cs:** - -```csharp -using MediatR; -using System; - -namespace TalentManagementAPI.Application.Features.Employees.Commands.UpdateEmployee -{ - public class UpdateEmployeeCommand : IRequest - { - public int Id { get; set; } - public string EmployeeNumber { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string Email { get; set; } - public string Phone { get; set; } - public string Notes { get; set; } - public DateTime DateOfBirth { get; set; } - public DateTime HireDate { get; set; } - public int DepartmentId { get; set; } - public int PositionId { get; set; } - } -} -``` - ---- - -## Step 5: Add Validation - -### CreateEmployeeCommandValidator.cs - -**File:** `ApiResources/TalentManagement-API/TalentManagementAPI.Application/Features/Employees/Commands/CreateEmployee/CreateEmployeeCommandValidator.cs` - -**Add validation rule:** - -```csharp -RuleFor(v => v.Notes) - .MaximumLength(500).WithMessage("Notes must not exceed 500 characters."); -``` - -**Complete CreateEmployeeCommandValidator.cs:** - -```csharp -using FluentValidation; - -namespace TalentManagementAPI.Application.Features.Employees.Commands.CreateEmployee -{ - public class CreateEmployeeCommandValidator : AbstractValidator - { - public CreateEmployeeCommandValidator() - { - RuleFor(v => v.EmployeeNumber) - .NotEmpty().WithMessage("Employee number is required.") - .MaximumLength(50).WithMessage("Employee number must not exceed 50 characters."); - - RuleFor(v => v.FirstName) - .NotEmpty().WithMessage("First name is required.") - .MaximumLength(50).WithMessage("First name must not exceed 50 characters."); - - RuleFor(v => v.LastName) - .NotEmpty().WithMessage("Last name is required.") - .MaximumLength(50).WithMessage("Last name must not exceed 50 characters."); - - RuleFor(v => v.Email) - .NotEmpty().WithMessage("Email is required.") - .EmailAddress().WithMessage("A valid email is required.") - .MaximumLength(100).WithMessage("Email must not exceed 100 characters."); - - RuleFor(v => v.Phone) - .MaximumLength(20).WithMessage("Phone must not exceed 20 characters."); - - RuleFor(v => v.Notes) - .MaximumLength(500).WithMessage("Notes must not exceed 500 characters."); - - RuleFor(v => v.DateOfBirth) - .NotEmpty().WithMessage("Date of birth is required.") - .Must(BeAValidAge).WithMessage("Employee must be at least 18 years old."); - - RuleFor(v => v.HireDate) - .NotEmpty().WithMessage("Hire date is required."); - - RuleFor(v => v.DepartmentId) - .GreaterThan(0).WithMessage("Department is required."); - - RuleFor(v => v.PositionId) - .GreaterThan(0).WithMessage("Position is required."); - } - - private bool BeAValidAge(DateTime dateOfBirth) - { - var age = DateTime.Today.Year - dateOfBirth.Year; - if (dateOfBirth.Date > DateTime.Today.AddYears(-age)) age--; - return age >= 18; - } - } -} -``` - -### UpdateEmployeeCommandValidator.cs - -**File:** `ApiResources/TalentManagement-API/TalentManagementAPI.Application/Features/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandValidator.cs` - -**Add the same validation rule:** - -```csharp -RuleFor(v => v.Notes) - .MaximumLength(500).WithMessage("Notes must not exceed 500 characters."); -``` - ---- - -## Step 6: Update Command Handlers - -The handlers automatically map properties, but verify they include Notes: - -### CreateEmployeeCommandHandler.cs - -**File:** `ApiResources/TalentManagement-API/TalentManagementAPI.Application/Features/Employees/Commands/CreateEmployee/CreateEmployeeCommandHandler.cs` - -**Relevant section (Handle method):** - -```csharp -var employee = new Employee -{ - EmployeeNumber = request.EmployeeNumber, - FirstName = request.FirstName, - LastName = request.LastName, - Email = request.Email, - Phone = request.Phone, - Notes = request.Notes, // ← Add this line - DateOfBirth = request.DateOfBirth, - HireDate = request.HireDate, - DepartmentId = request.DepartmentId, - PositionId = request.PositionId -}; -``` - -### UpdateEmployeeCommandHandler.cs - -**File:** `ApiResources/TalentManagement-API/TalentManagementAPI.Application/Features/Employees/Commands/UpdateEmployee/UpdateEmployeeCommandHandler.cs` - -**Relevant section (Handle method):** - -```csharp -employee.EmployeeNumber = request.EmployeeNumber; -employee.FirstName = request.FirstName; -employee.LastName = request.LastName; -employee.Email = request.Email; -employee.Phone = request.Phone; -employee.Notes = request.Notes; // ← Add this line -employee.DateOfBirth = request.DateOfBirth; -employee.HireDate = request.HireDate; -employee.DepartmentId = request.DepartmentId; -employee.PositionId = request.PositionId; -``` - ---- - -## Step 7: Update Query Response DTOs (if needed) - -### EmployeeDto.cs - -**File:** `ApiResources/TalentManagement-API/TalentManagementAPI.Application/Features/Employees/Queries/GetEmployees/EmployeeDto.cs` - -**Add property:** - -```csharp -public string Notes { get; set; } -``` - -**Complete EmployeeDto.cs:** - -```csharp -namespace TalentManagementAPI.Application.Features.Employees.Queries.GetEmployees -{ - public class EmployeeDto - { - public int Id { get; set; } - public string EmployeeNumber { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string FullName => $"{FirstName} {LastName}"; - public string Email { get; set; } - public string Phone { get; set; } - public string Notes { get; set; } - public DateTime DateOfBirth { get; set; } - public DateTime HireDate { get; set; } - public int DepartmentId { get; set; } - public string DepartmentName { get; set; } - public int PositionId { get; set; } - public string PositionName { get; set; } - } -} -``` - ---- - -## Step 8: Build and Test - -### Build the Solution - -```bash -cd ApiResources/TalentManagement-API -dotnet build -``` - -**Expected Output:** - -``` -Build succeeded. - 0 Warning(s) - 0 Error(s) -``` - -### Run the API - -```bash -dotnet run --project TalentManagementAPI.WebApi -``` - -**Wait for:** - -``` -Now listening on: https://localhost:44378 -Application started. Press Ctrl+C to shut down. -``` - ---- - -## Step 9: Test in Swagger - -### 1. Open Swagger UI - -Navigate to: `https://localhost:44378/swagger` - -### 2. Authenticate - -1. Click **Authorize** button (top right) -2. Login with credentials: `ashtyn1` / `Pa$$word123` -3. Allow scopes - -### 3. Test POST /api/v1/employees - -**Expand POST endpoint → Try it out** - -**Request body:** - -```json -{ - "employeeNumber": "EMP999", - "firstName": "Test", - "lastName": "Employee", - "email": "test@example.com", - "phone": "555-1234", - "notes": "This is a test employee with notes field", - "dateOfBirth": "1990-01-01", - "hireDate": "2024-01-01", - "departmentId": 1, - "positionId": 1 -} -``` - -**Expected Response:** `201 Created` with new employee ID - -### 4. Test Validation - -**Test with 501 characters in Notes:** - -```json -{ - "employeeNumber": "EMP998", - "firstName": "Test", - "lastName": "Validation", - "email": "validation@example.com", - "phone": "555-5678", - "notes": "AAAAAAAAAA...AAA", // 501 A's - "dateOfBirth": "1990-01-01", - "hireDate": "2024-01-01", - "departmentId": 1, - "positionId": 1 -} -``` - -**Expected Response:** `400 Bad Request` - -```json -{ - "errors": { - "Notes": [ - "Notes must not exceed 500 characters." - ] - }, - "title": "One or more validation errors occurred." -} -``` - -### 5. Test GET /api/v1/employees - -**Verify Notes field appears in response:** - -```json -{ - "data": [ - { - "id": 999, - "employeeNumber": "EMP999", - "firstName": "Test", - "lastName": "Employee", - "fullName": "Test Employee", - "email": "test@example.com", - "phone": "555-1234", - "notes": "This is a test employee with notes field", - "dateOfBirth": "1990-01-01T00:00:00", - "hireDate": "2024-01-01T00:00:00", - "departmentId": 1, - "departmentName": "IT", - "positionId": 1, - "positionName": "Developer" - } - ] -} -``` - ---- - -## Verification Checklist - -- [x] Notes property added to Employee.cs -- [x] Migration created successfully -- [x] Migration applied to database -- [x] CreateEmployeeCommand includes Notes -- [x] UpdateEmployeeCommand includes Notes -- [x] Validators include MaxLength(500) rule -- [x] Command handlers map Notes property -- [x] EmployeeDto includes Notes property -- [x] API builds without errors -- [x] Swagger shows notes field -- [x] Can create employee with notes -- [x] Validation rejects 501+ characters -- [x] GET returns notes field - ---- - -## Common Issues and Solutions - -### Issue: Migration not creating Notes column - -**Solution:** Verify Entity Framework configuration - -```csharp -// Check: TalentManagementAPI.Infrastructure.Persistence/Configurations/EmployeeConfiguration.cs -public void Configure(EntityTypeBuilder builder) -{ - builder.Property(e => e.Notes) - .HasMaxLength(500) - .IsRequired(false); -} -``` - -### Issue: Validation not working - -**Solution:** Ensure FluentValidation is registered in Program.cs - -```csharp -builder.Services.AddFluentValidation(fv => - fv.RegisterValidatorsFromAssemblyContaining()); -``` - -### Issue: Notes not appearing in Swagger - -**Solution:** Rebuild and restart API - -```bash -dotnet clean -dotnet build -dotnet run --project TalentManagementAPI.WebApi -``` - ---- - -## What You Learned - -✅ **Domain-First Development** — Start with domain entity -✅ **EF Core Migrations** — Create and apply database changes -✅ **CQRS Pattern** — Update commands and queries separately -✅ **FluentValidation** — Add declarative validation rules -✅ **Vertical Slice** — Complete flow from domain to API -✅ **Testing in Swagger** — Verify API changes immediately - ---- - -*[← Back to LAB-03](../LAB-03-extend-api.md) | [Labs Home](../README.md)* diff --git a/docs/labs/solutions/LAB-04-solution.md b/docs/labs/solutions/LAB-04-solution.md deleted file mode 100644 index b8a4633..0000000 --- a/docs/labs/solutions/LAB-04-solution.md +++ /dev/null @@ -1,535 +0,0 @@ -# LAB-04 Solution: Build Angular Search Component - -## Complete Solution Code - -This document provides the complete solution for [LAB-04: Build Angular Search Component](../LAB-04-build-component.md). - ---- - -## Complete Component Files - -### employee-search.component.ts - -**File:** `Clients/TalentManagement-Angular-Material/talent-management/src/app/features/employees/employee-search/employee-search.component.ts` - -```typescript -import { Component, OnInit, inject, output } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatTableModule } from '@angular/material/table'; -import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { debounceTime, distinctUntilChanged, catchError, finalize } from 'rxjs/operators'; -import { of } from 'rxjs'; -import { EmployeeService } from '../../../core/services/employee.service'; -import { Employee } from '../../../core/models/employee.model'; - -@Component({ - selector: 'app-employee-search', - standalone: true, - imports: [ - CommonModule, - ReactiveFormsModule, - MatFormFieldModule, - MatInputModule, - MatTableModule, - MatPaginatorModule, - MatButtonModule, - MatIconModule, - MatProgressSpinnerModule - ], - templateUrl: './employee-search.component.html', - styleUrls: ['./employee-search.component.scss'] -}) -export class EmployeeSearchComponent implements OnInit { - private fb = inject(FormBuilder); - private employeeService = inject(EmployeeService); - - // Output event when employee is selected - employeeSelected = output(); - - searchForm!: FormGroup; - employees: Employee[] = []; - displayedColumns: string[] = ['employeeNumber', 'fullName', 'email', 'departmentName']; - - // Pagination properties - pageNumber = 1; - pageSize = 10; - totalCount = 0; - - // Loading state - isLoading = false; - - ngOnInit(): void { - this.initializeForm(); - this.setupSearchSubscription(); - this.searchEmployees(); - } - - private initializeForm(): void { - this.searchForm = this.fb.group({ - searchTerm: [''] - }); - } - - private setupSearchSubscription(): void { - this.searchForm.get('searchTerm')!.valueChanges.pipe( - debounceTime(300), - distinctUntilChanged() - ).subscribe(() => { - this.pageNumber = 1; // Reset to first page on search - this.searchEmployees(); - }); - } - - searchEmployees(): void { - this.isLoading = true; - const searchTerm = this.searchForm.get('searchTerm')?.value || ''; - - this.employeeService.getEmployees(this.pageNumber, this.pageSize, searchTerm) - .pipe( - catchError(error => { - console.error('Error loading employees:', error); - return of({ data: [], pageNumber: 1, pageSize: 10, totalPages: 0, totalCount: 0 }); - }), - finalize(() => this.isLoading = false) - ) - .subscribe(response => { - this.employees = response.data; - this.totalCount = response.totalCount; - }); - } - - onPageChange(event: PageEvent): void { - this.pageNumber = event.pageIndex + 1; - this.pageSize = event.pageSize; - this.searchEmployees(); - } - - onRowClick(employee: Employee): void { - this.employeeSelected.emit(employee); - } - - clearSearch(): void { - this.searchForm.reset(); - } -} -``` - ---- - -### employee-search.component.html - -**File:** `Clients/TalentManagement-Angular-Material/talent-management/src/app/features/employees/employee-search/employee-search.component.html` - -```html -
- -
-

Search Employees

-
- - -
- - Search by name, email, or employee number - - search - @if (searchForm.get('searchTerm')?.value) { - - } - -
- - - @if (isLoading) { -
- -
- } - - - @if (!isLoading) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Employee #{{ employee.employeeNumber }}Name{{ employee.fullName }}Email{{ employee.email }}Department{{ employee.departmentName }}
- @if (searchForm.get('searchTerm')?.value) { - No employees found matching "{{ searchForm.get('searchTerm')?.value }}" - } @else { - No employees found - } -
- - - @if (totalCount > 0) { - - - } -
- } -
-``` - ---- - -### employee-search.component.scss - -**File:** `Clients/TalentManagement-Angular-Material/talent-management/src/app/features/employees/employee-search/employee-search.component.scss` - -```scss -.employee-search-container { - padding: 24px; - max-width: 1200px; - margin: 0 auto; - - .search-header { - margin-bottom: 24px; - - h2 { - font-size: 24px; - font-weight: 500; - color: rgba(0, 0, 0, 0.87); - margin: 0; - } - } - - .search-form { - margin-bottom: 24px; - - .search-field { - width: 100%; - font-size: 16px; - } - } - - .loading-container { - display: flex; - justify-content: center; - align-items: center; - padding: 48px 0; - } - - .results-container { - background: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - overflow: hidden; - - .employee-table { - width: 100%; - - th { - background-color: #f5f5f5; - font-weight: 600; - font-size: 14px; - color: rgba(0, 0, 0, 0.87); - } - - td { - font-size: 14px; - color: rgba(0, 0, 0, 0.87); - } - - .clickable-row { - cursor: pointer; - transition: background-color 0.2s ease; - - &:hover { - background-color: #f5f5f5; - } - - &:active { - background-color: #e0e0e0; - } - } - - .no-data { - text-align: center; - padding: 48px 24px; - color: rgba(0, 0, 0, 0.54); - font-style: italic; - } - } - } -} - -// Responsive design -@media (max-width: 768px) { - .employee-search-container { - padding: 16px; - - .search-header h2 { - font-size: 20px; - } - - .employee-table { - font-size: 12px; - - th, td { - padding: 8px 4px; - } - } - } -} -``` - ---- - -## Integration Steps - -### Step 1: Create the Component - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management -ng generate component features/employees/employee-search --standalone -``` - -### Step 2: Add Route - -**File:** `src/app/app.routes.ts` - -```typescript -import { EmployeeSearchComponent } from './features/employees/employee-search/employee-search.component'; - -export const routes: Routes = [ - // ... other routes - { - path: 'employees/search', - component: EmployeeSearchComponent, - canActivate: [AuthGuard] - } -]; -``` - -### Step 3: Add Navigation Menu Item - -**File:** `src/app/shared/components/sidebar/sidebar.component.html` - -```html - - - - - search - Search Employees - - -``` - -### Step 4: Use Component with Event Handling - -**Example parent component:** - -```typescript -import { EmployeeSearchComponent } from './employee-search/employee-search.component'; - -@Component({ - selector: 'app-employee-list', - template: ` - - - `, - imports: [EmployeeSearchComponent] -}) -export class EmployeeListComponent { - onEmployeeSelected(employee: Employee): void { - console.log('Selected employee:', employee); - // Navigate to detail view or open edit dialog - this.router.navigate(['/employees', employee.id]); - } -} -``` - ---- - -## Testing the Component - -### Manual Testing Steps - -1. **Start the application:** - ```bash - npm start - ``` - -2. **Navigate to search page:** - - Open http://localhost:4200 - - Login with `ashtyn1` / `Pa$$word123` - - Click "Search Employees" in sidebar - -3. **Test search functionality:** - - Type in search box - - Verify 300ms debounce (results update after typing stops) - - Clear search with X button - -4. **Test pagination:** - - Change page size (5, 10, 25, 50) - - Navigate through pages - - Verify page numbers update correctly - -5. **Test row selection:** - - Click on table row - - Verify `employeeSelected` event fires - - Check console for selected employee - -### Unit Testing - -**File:** `employee-search.component.spec.ts` - -```typescript -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { provideHttpClient } from '@angular/common/http'; -import { of } from 'rxjs'; -import { EmployeeSearchComponent } from './employee-search.component'; -import { EmployeeService } from '../../../core/services/employee.service'; - -describe('EmployeeSearchComponent', () => { - let component: EmployeeSearchComponent; - let fixture: ComponentFixture; - let employeeService: jasmine.SpyObj; - - beforeEach(async () => { - const spy = jasmine.createSpyObj('EmployeeService', ['getEmployees']); - - await TestBed.configureTestingModule({ - imports: [EmployeeSearchComponent], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - { provide: EmployeeService, useValue: spy } - ] - }).compileComponents(); - - employeeService = TestBed.inject(EmployeeService) as jasmine.SpyObj; - employeeService.getEmployees.and.returnValue(of({ - data: [], - pageNumber: 1, - pageSize: 10, - totalPages: 0, - totalCount: 0 - })); - - fixture = TestBed.createComponent(EmployeeSearchComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should call getEmployees on init', () => { - fixture.detectChanges(); - expect(employeeService.getEmployees).toHaveBeenCalledWith(1, 10, ''); - }); - - it('should emit selected employee on row click', () => { - spyOn(component.employeeSelected, 'emit'); - const mockEmployee = { - id: 1, - employeeNumber: 'EMP001', - fullName: 'John Doe', - email: 'john@example.com' - } as any; - - component.onRowClick(mockEmployee); - - expect(component.employeeSelected.emit).toHaveBeenCalledWith(mockEmployee); - }); -}); -``` - ---- - -## Verification Checklist - -- [x] Component created with standalone: true -- [x] ReactiveFormsModule imported -- [x] Material Design modules imported -- [x] inject() function used for DI -- [x] output() function used for event emission -- [x] debounceTime(300) applied to search -- [x] distinctUntilChanged() prevents duplicate calls -- [x] catchError() handles API errors -- [x] Pagination works correctly -- [x] Row click emits employee -- [x] Loading spinner displays -- [x] No data message shows -- [x] Responsive styles applied -- [x] Unit tests pass - ---- - -## Key Concepts Demonstrated - -✅ **Standalone Components** — No NgModule required -✅ **Modern DI** — Using inject() function -✅ **Signal-based Events** — Using output() for events -✅ **Reactive Forms** — FormBuilder and FormGroup -✅ **RxJS Operators** — debounceTime, distinctUntilChanged, catchError, finalize -✅ **Material Design** — Table, paginator, form field -✅ **Server-side Pagination** — PagedResponse pattern -✅ **Error Handling** — Graceful degradation -✅ **Loading States** — User feedback -✅ **Responsive Design** — Mobile-friendly styles - ---- - -*[← Back to LAB-04](../LAB-04-build-component.md) | [Labs Home](../README.md)* diff --git a/docs/labs/solutions/LAB-05-solution.md b/docs/labs/solutions/LAB-05-solution.md deleted file mode 100644 index 02831f6..0000000 --- a/docs/labs/solutions/LAB-05-solution.md +++ /dev/null @@ -1,590 +0,0 @@ -# LAB-05 Solution: Write Unit Tests - -## Complete Solution Code - -This document provides the complete solution for [LAB-05: Write Unit Tests](../LAB-05-write-tests.md). - ---- - -## Part 1: .NET API Unit Tests - Complete Solutions - -### CreateEmployeeCommandValidatorTests.cs (Complete) - -**File:** `TalentManagementAPI.Application.Tests/Employees/CreateEmployeeCommandValidatorTests.cs` - -**Complete test class with all test methods shown in the lab.** - -See [LAB-05: Step 3](../LAB-05-write-tests.md#step-3-write-validator-tests) for the full implementation. - -**Key Test Patterns:** - -```csharp -[Fact] -public void Should_Have_Error_When_FirstName_Is_Empty() -{ - // Arrange - Create command with empty FirstName - var command = new CreateEmployeeCommand { FirstName = "" /* ... */ }; - - // Act - Validate command - var result = _validator.Validate(command); - - // Assert - Expect validation error - result.IsValid.Should().BeFalse(); - result.Errors.Should().Contain(e => e.PropertyName == "FirstName"); -} -``` - -**Theory Tests with InlineData:** - -```csharp -[Theory] -[InlineData("test@example.com", true)] -[InlineData("invalid-email", false)] -[InlineData("", false)] -public void Should_Validate_Email_Format(string email, bool expectedValid) -{ - // Test multiple scenarios with one test method -} -``` - ---- - -## Part 2: Angular Service Tests - Complete Solutions - -### EmployeeService.spec.ts (Complete) - -**File:** `src/app/core/services/employee.service.spec.ts` - -**See [LAB-05: Step 5](../LAB-05-write-tests.md#step-5-create-service-test-file) for full implementation.** - -**Key Testing Patterns:** - -### 1. Setup TestBed with HTTP Testing - -```typescript -beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - EmployeeService, - provideHttpClient(), - provideHttpClientTesting() - ] - }); - - service = TestBed.inject(EmployeeService); - httpMock = TestBed.inject(HttpTestingController); -}); -``` - -### 2. Verify HTTP Requests - -```typescript -it('should return paged employees', () => { - const mockResponse: PagedResponse = { /* ... */ }; - - service.getEmployees(1, 10, '').subscribe(response => { - expect(response).toEqual(mockResponse); - }); - - const req = httpMock.expectOne(request => - request.url.includes(apiUrl) && - request.params.get('pageNumber') === '1' - ); - expect(req.request.method).toBe('GET'); - req.flush(mockResponse); -}); -``` - -### 3. Test Error Handling - -```typescript -it('should handle HTTP error', () => { - service.getEmployee(999).subscribe( - () => fail('should have failed with 404 error'), - (error) => { - expect(error.status).toBe(404); - } - ); - - const req = httpMock.expectOne(`${apiUrl}/999`); - req.flush('Not Found', { status: 404, statusText: 'Not Found' }); -}); -``` - -### 4. Cleanup After Each Test - -```typescript -afterEach(() => { - httpMock.verify(); // Ensures no outstanding HTTP requests -}); -``` - ---- - -## Part 3: Angular Component Tests - Complete Solutions - -### EmployeeFormComponent.spec.ts (Complete) - -**File:** `src/app/features/employees/employee-form/employee-form.component.spec.ts` - -**See [LAB-05: Step 7](../LAB-05-write-tests.md#step-7-create-component-test-file) for full implementation.** - -**Key Testing Patterns:** - -### 1. Setup with Spy Objects - -```typescript -beforeEach(async () => { - const employeeSpy = jasmine.createSpyObj('EmployeeService', [ - 'createEmployee', - 'updateEmployee', - 'getEmployee' - ]); - - await TestBed.configureTestingModule({ - imports: [EmployeeFormComponent], - providers: [ - { provide: EmployeeService, useValue: employeeSpy } - ] - }).compileComponents(); - - employeeService = TestBed.inject(EmployeeService) as jasmine.SpyObj; -}); -``` - -### 2. Test Form Initialization - -```typescript -it('should initialize form with empty values', () => { - fixture.detectChanges(); - - expect(component.form.get('firstName')?.value).toBe(''); - expect(component.form.get('email')?.value).toBe(''); -}); -``` - -### 3. Test Form Validation - -```typescript -it('should validate email format', () => { - fixture.detectChanges(); - - const emailControl = component.form.get('email'); - - emailControl?.setValue('invalid-email'); - expect(emailControl?.errors?.['email']).toBeTruthy(); - - emailControl?.setValue('valid@example.com'); - expect(emailControl?.errors).toBeNull(); -}); -``` - -### 4. Test Service Method Calls - -```typescript -it('should call createEmployee on submit', () => { - fixture.detectChanges(); - - component.form.patchValue({ - firstName: 'John', - lastName: 'Doe', - email: 'john@example.com' - }); - - employeeService.createEmployee.and.returnValue(of(123)); - - component.onSubmit(); - - expect(employeeService.createEmployee).toHaveBeenCalledWith( - jasmine.objectContaining({ - firstName: 'John', - lastName: 'Doe' - }) - ); -}); -``` - -### 5. Test Error Scenarios - -```typescript -it('should handle create employee error', () => { - fixture.detectChanges(); - - component.form.patchValue({ /* valid data */ }); - - const errorResponse = { status: 400, statusText: 'Bad Request' }; - employeeService.createEmployee.and.returnValue(throwError(() => errorResponse)); - - component.onSubmit(); - - expect(component.form.enabled).toBeTrue(); -}); -``` - ---- - -## Running Tests - Complete Commands - -### .NET Tests - -```bash -cd ApiResources/TalentManagement-API - -# Run all tests -dotnet test - -# Run with detailed output -dotnet test --verbosity detailed - -# Run specific test class -dotnet test --filter "FullyQualifiedName~CreateEmployeeCommandValidatorTests" - -# Run specific test method -dotnet test --filter "FullyQualifiedName~Should_Have_Error_When_FirstName_Is_Empty" - -# Run with code coverage -dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover -``` - -**Expected Output:** - -``` -Test run for TalentManagementAPI.Application.Tests.dll (.NET 10.0) -Microsoft (R) Test Execution Command Line Tool Version 17.9.0 - -Starting test execution, please wait... -A total of 1 test files matched the specified pattern. - -Passed! - Failed: 0, Passed: 7, Skipped: 0, Total: 7, Duration: 245 ms -``` - -### Angular Tests - -```bash -cd Clients/TalentManagement-Angular-Material/talent-management - -# Run all tests (watch mode) -npm test - -# Run tests once (CI mode) -npm test -- --watch=false - -# Run tests in headless browser -npm test -- --watch=false --browsers=ChromeHeadless - -# Run specific test file -npm test -- --include='**/employee.service.spec.ts' - -# Run with code coverage -npm test -- --code-coverage --watch=false - -# Run and generate coverage report -npm test -- --code-coverage --watch=false --browsers=ChromeHeadless -``` - -**Expected Output:** - -``` -✔ Browser application bundle generation complete. -08 02 2026 10:30:15.123:INFO [karma-server]: Karma v6.4.2 server started -08 02 2026 10:30:15.456:INFO [Chrome Headless]: Connected on socket -Chrome Headless 120.0.0.0: Executed 15 of 15 SUCCESS (0.789 secs / 0.654 secs) -TOTAL: 15 SUCCESS - -=============================== Coverage summary =============================== -Statements : 85.23% ( 247/290 ) -Branches : 78.12% ( 75/96 ) -Functions : 81.67% ( 49/60 ) -Lines : 84.91% ( 236/278 ) -================================================================================ -``` - ---- - -## Test Coverage Analysis - -### Viewing .NET Coverage Report - -**Install ReportGenerator:** - -```bash -dotnet tool install -g dotnet-reportgenerator-globaltool -``` - -**Generate HTML Report:** - -```bash -dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover -reportgenerator -reports:coverage.opencover.xml -targetdir:coveragereport -reporttypes:Html -``` - -**Open report:** `coveragereport/index.html` - -### Viewing Angular Coverage Report - -**After running tests with coverage:** - -```bash -npm test -- --code-coverage --watch=false -``` - -**Open report:** `coverage/index.html` - -**Coverage report shows:** -- Line coverage percentage -- Branch coverage percentage -- Uncovered lines highlighted in red -- File-by-file breakdown - ---- - -## Additional Test Examples - -### Testing with Mock Data Builders - -**Create test data builder:** - -```typescript -// test-helpers/employee.builder.ts -export class EmployeeBuilder { - private employee: Partial = { - id: 1, - employeeNumber: 'EMP001', - firstName: 'John', - lastName: 'Doe', - email: 'john@example.com' - }; - - withId(id: number): this { - this.employee.id = id; - return this; - } - - withEmail(email: string): this { - this.employee.email = email; - return this; - } - - build(): Employee { - return this.employee as Employee; - } -} - -// Usage in tests -const employee = new EmployeeBuilder() - .withId(123) - .withEmail('test@example.com') - .build(); -``` - -### Testing Async Operations - -```typescript -it('should handle async operation', fakeAsync(() => { - let result: any; - - service.getEmployee(1).subscribe(data => { - result = data; - }); - - tick(); // Simulate passage of time - - expect(result).toBeDefined(); -})); -``` - -### Testing Observables with Marble Testing - -```typescript -import { TestScheduler } from 'rxjs/testing'; - -it('should debounce search input', () => { - const scheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); - - scheduler.run(({ cold, expectObservable }) => { - const input$ = cold('a-b-c---|'); - const expected = '---c----|'; - const result$ = input$.pipe(debounceTime(300)); - - expectObservable(result$).toBe(expected); - }); -}); -``` - ---- - -## Troubleshooting Common Test Issues - -### .NET Tests - -**Issue: "No test is available"** - -```bash -# Solution: Clean and rebuild -dotnet clean -dotnet build -dotnet test -``` - -**Issue: "FluentAssertions not found"** - -```bash -# Solution: Add package -cd TalentManagementAPI.Application.Tests -dotnet add package FluentAssertions -dotnet restore -``` - -**Issue: Tests fail due to database** - -```csharp -// Solution: Use in-memory database for tests -services.AddDbContext(options => - options.UseInMemoryDatabase("TestDatabase")); -``` - -### Angular Tests - -**Issue: "NullInjectorError: No provider for HttpClient"** - -```typescript -// Solution: Add HTTP testing providers -providers: [ - provideHttpClient(), - provideHttpClientTesting() -] -``` - -**Issue: "Can't resolve all parameters for Router"** - -```typescript -// Solution: Add router testing provider -providers: [ - provideRouter([]) -] -``` - -**Issue: "Timeout - Async callback was not invoked"** - -```typescript -// Solution: Increase timeout -beforeEach(async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - // ... -}); -``` - -**Issue: "Expected spy to have been called"** - -```typescript -// Solution: Ensure detectChanges() is called -fixture.detectChanges(); // Triggers ngOnInit -expect(serviceSpy.method).toHaveBeenCalled(); -``` - ---- - -## Best Practices Summary - -### AAA Pattern (Arrange-Act-Assert) - -```typescript -it('should do something', () => { - // Arrange - Set up test data - const input = 'test'; - serviceSpy.method.and.returnValue(of('result')); - - // Act - Execute the code being tested - component.action(input); - - // Assert - Verify the expected outcome - expect(serviceSpy.method).toHaveBeenCalledWith(input); -}); -``` - -### Test Naming Conventions - -✅ **Good:** `Should_Have_Error_When_FirstName_Is_Empty` -✅ **Good:** `should validate email format` -❌ **Bad:** `test1`, `testValidation`, `checkEmail` - -### One Assertion Per Test (Ideal) - -```typescript -// Good - focused test -it('should mark form invalid when email is empty', () => { - component.form.get('email')?.setValue(''); - expect(component.form.valid).toBeFalse(); -}); - -// Also good - related assertions -it('should populate form with employee data', () => { - const employee = { firstName: 'John', lastName: 'Doe' }; - component.loadEmployee(employee); - - expect(component.form.get('firstName')?.value).toBe('John'); - expect(component.form.get('lastName')?.value).toBe('Doe'); -}); -``` - -### Test Independence - -```typescript -// Good - each test is independent -beforeEach(() => { - component = new MyComponent(); - component.ngOnInit(); -}); - -// Bad - tests depend on execution order -let sharedState; -it('test 1', () => { sharedState = 'value'; }); -it('test 2', () => { expect(sharedState).toBe('value'); }); // Fragile -``` - -### Mock External Dependencies - -```typescript -// Good - mock HTTP calls -const httpMock = TestBed.inject(HttpTestingController); - -// Good - mock services -const serviceSpy = jasmine.createSpyObj('MyService', ['method']); - -// Bad - make real HTTP calls in tests -service.getData().subscribe(); // Slow, unreliable -``` - ---- - -## Test Pyramid Reminder - -**70% Unit Tests** — Fast, isolated, test business logic -- Validators, pure functions, services, components - -**20% Integration Tests** — Test component interactions -- API controllers with database, component + service - -**10% E2E Tests** — Full user flows -- Login → Navigate → CRUD operations - ---- - -## What You Learned - -✅ **xUnit Testing** — Facts, Theories, InlineData -✅ **FluentAssertions** — Readable test assertions -✅ **Jasmine/Karma** — Angular testing framework -✅ **TestBed** — Angular testing utility -✅ **HttpTestingController** — Mock HTTP requests -✅ **Spy Objects** — Mock services and verify calls -✅ **Code Coverage** — Measure test effectiveness -✅ **AAA Pattern** — Arrange-Act-Assert structure -✅ **Test Naming** — Clear, descriptive test names -✅ **Test Independence** — Isolated, repeatable tests - ---- - -*[← Back to LAB-05](../LAB-05-write-tests.md) | [Labs Home](../README.md)* diff --git a/docs/screenshots/angular/angular-image-catalog.md b/docs/screenshots/angular/angular-image-catalog.md deleted file mode 100644 index b299bcd..0000000 --- a/docs/screenshots/angular/angular-image-catalog.md +++ /dev/null @@ -1,70 +0,0 @@ -# Angular Image Catalog - -1. `angular-login-page.png` -Figure title: Angular user menu with Login option before authentication - -2. `application-dashboard-anonymous.png` -Figure title: Angular dashboard accessible to anonymous users before login - -3. `profile-overview-page.png` -Figure title: Profile overview page opened from the top-right user menu - -4. `identityserver-login-ashtyn1.png` -Figure title: IdentityServer login page with tutorial user credentials entered - -5. `identityserver-logout-intermediate.png` -Figure title: IdentityServer logout confirmation page with Click Here sign-out link - -6. `user-menu-logout-link.png` -Figure title: Top-right user menu displaying the Logout action - -7. `employee-list-page.png` -Figure title: Employee list page with records table and actions - -8. `employee-search-filtering-ui.png` -Figure title: Employee search and filtering interface - -9. `employee-crud-operations.png` -Figure title: Employee page showing CRUD entry points such as List and Create - -10. `employee-form.png` -Figure title: Employee create form after selecting Create - -11. `department-list-page.png` -Figure title: Department list page with records table and actions - -12. `department-search-filtering-ui.png` -Figure title: Department search and filtering interface - -13. `department-crud-operations.png` -Figure title: Department page showing CRUD entry points such as List and Create - -14. `department-form.png` -Figure title: Department create form after selecting Create - -15. `position-list-page.png` -Figure title: Position list page with records table and actions - -16. `position-search-filtering-ui.png` -Figure title: Position search and filtering interface - -17. `position-crud-operations.png` -Figure title: Position page showing CRUD entry points such as List and Create - -18. `position-form.png` -Figure title: Position create form after selecting Create - -19. `salary-range-list-page.png` -Figure title: Salary Range list page with records table and actions - -20. `salary-range-search-filtering-ui.png` -Figure title: Salary Range search and filtering interface - -21. `salary-range-crud-operations.png` -Figure title: Salary Range page showing CRUD entry points such as List and Create - -22. `salary-range-form.png` -Figure title: Salary Range create form after selecting Create - -23. `search-filtering-ui.png` -Figure title: Legacy search and filtering screenshot from earlier capture flow diff --git a/docs/screenshots/angular/angular-login-page.png b/docs/screenshots/angular/angular-login-page.png deleted file mode 100644 index 8d285c6..0000000 Binary files a/docs/screenshots/angular/angular-login-page.png and /dev/null differ diff --git a/docs/screenshots/angular/application-dashboard-anonymous.png b/docs/screenshots/angular/application-dashboard-anonymous.png deleted file mode 100644 index ac54f77..0000000 Binary files a/docs/screenshots/angular/application-dashboard-anonymous.png and /dev/null differ diff --git a/docs/screenshots/angular/department-crud-operations.png b/docs/screenshots/angular/department-crud-operations.png deleted file mode 100644 index 3d18d7c..0000000 Binary files a/docs/screenshots/angular/department-crud-operations.png and /dev/null differ diff --git a/docs/screenshots/angular/department-form.png b/docs/screenshots/angular/department-form.png deleted file mode 100644 index 73c5b07..0000000 Binary files a/docs/screenshots/angular/department-form.png and /dev/null differ diff --git a/docs/screenshots/angular/department-list-page.png b/docs/screenshots/angular/department-list-page.png deleted file mode 100644 index 25ec502..0000000 Binary files a/docs/screenshots/angular/department-list-page.png and /dev/null differ diff --git a/docs/screenshots/angular/department-search-filtering-ui.png b/docs/screenshots/angular/department-search-filtering-ui.png deleted file mode 100644 index 25ec502..0000000 Binary files a/docs/screenshots/angular/department-search-filtering-ui.png and /dev/null differ diff --git a/docs/screenshots/angular/employee-crud-operations.png b/docs/screenshots/angular/employee-crud-operations.png deleted file mode 100644 index 6e9f09f..0000000 Binary files a/docs/screenshots/angular/employee-crud-operations.png and /dev/null differ diff --git a/docs/screenshots/angular/employee-form.png b/docs/screenshots/angular/employee-form.png deleted file mode 100644 index c8d541e..0000000 Binary files a/docs/screenshots/angular/employee-form.png and /dev/null differ diff --git a/docs/screenshots/angular/employee-list-page.png b/docs/screenshots/angular/employee-list-page.png deleted file mode 100644 index 6e9f09f..0000000 Binary files a/docs/screenshots/angular/employee-list-page.png and /dev/null differ diff --git a/docs/screenshots/angular/employee-search-filtering-ui.png b/docs/screenshots/angular/employee-search-filtering-ui.png deleted file mode 100644 index 6e9f09f..0000000 Binary files a/docs/screenshots/angular/employee-search-filtering-ui.png and /dev/null differ diff --git a/docs/screenshots/angular/identityserver-login-ashtyn1.png b/docs/screenshots/angular/identityserver-login-ashtyn1.png deleted file mode 100644 index 7d50fc3..0000000 Binary files a/docs/screenshots/angular/identityserver-login-ashtyn1.png and /dev/null differ diff --git a/docs/screenshots/angular/identityserver-logout-intermediate.png b/docs/screenshots/angular/identityserver-logout-intermediate.png deleted file mode 100644 index b0ae16c..0000000 Binary files a/docs/screenshots/angular/identityserver-logout-intermediate.png and /dev/null differ diff --git a/docs/screenshots/angular/position-crud-operations.png b/docs/screenshots/angular/position-crud-operations.png deleted file mode 100644 index 1d3a2f3..0000000 Binary files a/docs/screenshots/angular/position-crud-operations.png and /dev/null differ diff --git a/docs/screenshots/angular/position-form.png b/docs/screenshots/angular/position-form.png deleted file mode 100644 index fc0f1e4..0000000 Binary files a/docs/screenshots/angular/position-form.png and /dev/null differ diff --git a/docs/screenshots/angular/position-list-page.png b/docs/screenshots/angular/position-list-page.png deleted file mode 100644 index 1d3a2f3..0000000 Binary files a/docs/screenshots/angular/position-list-page.png and /dev/null differ diff --git a/docs/screenshots/angular/position-search-filtering-ui.png b/docs/screenshots/angular/position-search-filtering-ui.png deleted file mode 100644 index 1d3a2f3..0000000 Binary files a/docs/screenshots/angular/position-search-filtering-ui.png and /dev/null differ diff --git a/docs/screenshots/angular/profile-overview-page.png b/docs/screenshots/angular/profile-overview-page.png deleted file mode 100644 index 4c58e4e..0000000 Binary files a/docs/screenshots/angular/profile-overview-page.png and /dev/null differ diff --git a/docs/screenshots/angular/salary-range-crud-operations.png b/docs/screenshots/angular/salary-range-crud-operations.png deleted file mode 100644 index 525eab7..0000000 Binary files a/docs/screenshots/angular/salary-range-crud-operations.png and /dev/null differ diff --git a/docs/screenshots/angular/salary-range-form.png b/docs/screenshots/angular/salary-range-form.png deleted file mode 100644 index 4fa4f7b..0000000 Binary files a/docs/screenshots/angular/salary-range-form.png and /dev/null differ diff --git a/docs/screenshots/angular/salary-range-list-page.png b/docs/screenshots/angular/salary-range-list-page.png deleted file mode 100644 index 525eab7..0000000 Binary files a/docs/screenshots/angular/salary-range-list-page.png and /dev/null differ diff --git a/docs/screenshots/angular/salary-range-search-filtering-ui.png b/docs/screenshots/angular/salary-range-search-filtering-ui.png deleted file mode 100644 index 525eab7..0000000 Binary files a/docs/screenshots/angular/salary-range-search-filtering-ui.png and /dev/null differ diff --git a/docs/screenshots/angular/search-filtering-ui.png b/docs/screenshots/angular/search-filtering-ui.png deleted file mode 100644 index 8f95851..0000000 Binary files a/docs/screenshots/angular/search-filtering-ui.png and /dev/null differ diff --git a/docs/screenshots/angular/user-menu-logout-link.png b/docs/screenshots/angular/user-menu-logout-link.png deleted file mode 100644 index 84cc479..0000000 Binary files a/docs/screenshots/angular/user-menu-logout-link.png and /dev/null differ diff --git a/docs/screenshots/identityserver/identityserver-admin-dashboard.png b/docs/screenshots/identityserver/identityserver-admin-dashboard.png deleted file mode 100644 index cdb01b1..0000000 Binary files a/docs/screenshots/identityserver/identityserver-admin-dashboard.png and /dev/null differ diff --git a/docs/screenshots/identityserver/identityserver-client-talentmanagement-advanced-grant-types.png b/docs/screenshots/identityserver/identityserver-client-talentmanagement-advanced-grant-types.png deleted file mode 100644 index 9f73505..0000000 Binary files a/docs/screenshots/identityserver/identityserver-client-talentmanagement-advanced-grant-types.png and /dev/null differ diff --git a/docs/screenshots/identityserver/identityserver-client-talentmanagement-advanced-token.png b/docs/screenshots/identityserver/identityserver-client-talentmanagement-advanced-token.png deleted file mode 100644 index ab3158b..0000000 Binary files a/docs/screenshots/identityserver/identityserver-client-talentmanagement-advanced-token.png and /dev/null differ diff --git a/docs/screenshots/identityserver/identityserver-client-talentmanagement-urls-tab.png b/docs/screenshots/identityserver/identityserver-client-talentmanagement-urls-tab.png deleted file mode 100644 index b61b5e1..0000000 Binary files a/docs/screenshots/identityserver/identityserver-client-talentmanagement-urls-tab.png and /dev/null differ diff --git a/docs/screenshots/identityserver/identityserver-clients-list.png b/docs/screenshots/identityserver/identityserver-clients-list.png deleted file mode 100644 index a4d1f32..0000000 Binary files a/docs/screenshots/identityserver/identityserver-clients-list.png and /dev/null differ diff --git a/docs/screenshots/identityserver/identityserver-image-catalog.md b/docs/screenshots/identityserver/identityserver-image-catalog.md deleted file mode 100644 index 869de1e..0000000 --- a/docs/screenshots/identityserver/identityserver-image-catalog.md +++ /dev/null @@ -1,22 +0,0 @@ -# IdentityServer Image Catalog - -1. `identityserver-admin-dashboard.png` -Figure title: IdentityServer Admin dashboard home screen - -2. `identityserver-clients-list.png` -Figure title: IdentityServer Clients configuration list - -3. `identityserver-client-talentmanagement-advanced-grant-types.png` -Figure title: TalentManagement client Advanced tab with Grant Types settings - -4. `identityserver-client-talentmanagement-advanced-token.png` -Figure title: TalentManagement client Advanced tab with Token settings - -5. `identityserver-client-talentmanagement-urls-tab.png` -Figure title: TalentManagement client Urls tab configuration - -6. `identityserver-login-admin.png` -Figure title: IdentityServer login page with admin credentials entered - -7. `identityserver-swagger-ui.png` -Figure title: IdentityServer API Swagger UI page diff --git a/docs/screenshots/identityserver/identityserver-login-admin.png b/docs/screenshots/identityserver/identityserver-login-admin.png deleted file mode 100644 index bed9775..0000000 Binary files a/docs/screenshots/identityserver/identityserver-login-admin.png and /dev/null differ diff --git a/docs/screenshots/identityserver/identityserver-swagger-ui.png b/docs/screenshots/identityserver/identityserver-swagger-ui.png deleted file mode 100644 index bd1802c..0000000 Binary files a/docs/screenshots/identityserver/identityserver-swagger-ui.png and /dev/null differ diff --git a/docs/screenshots/screenshot-catalog.json b/docs/screenshots/screenshot-catalog.json deleted file mode 100644 index a110aeb..0000000 --- a/docs/screenshots/screenshot-catalog.json +++ /dev/null @@ -1,535 +0,0 @@ -{ - "generated": "2026-04-22T03:36:16.505Z", - "screenshots": [ - { - "path": "screenshots-output/series-0-architecture/anonymous-home.png", - "audioPath": "screenshots-output/series-0-architecture/anonymous-home.wav", - "series": "series-0-architecture", - "filename": "anonymous-home.png", - "capturedAt": "2026-04-22T03:36:31.141Z", - "description": "Full TalentManagement app in anonymous Guest state — sidebar with limited menu, header with user icon, empty dashboard placeholder.", - "narration": "This is the TalentManagement application before login. The sidebar shows only public routes, and the header displays a Guest user icon in the top right corner.", - "articles": [ - "0.1", - "0.2", - "1.1" - ], - "tags": [ - "anonymous", - "guest", - "home", - "sidebar", - "app-shell" - ], - "useFor": "Hero image for architecture overview articles; shows the app before login." - }, - { - "path": "screenshots-output/series-0-architecture/sidebar-navigation.png", - "audioPath": "screenshots-output/series-0-architecture/sidebar-navigation.wav", - "series": "series-0-architecture", - "filename": "sidebar-navigation.png", - "capturedAt": "2026-04-22T03:36:42.930Z", - "description": "Left sidebar showing navigation menu items available to an anonymous Guest user — limited to public routes only.", - "narration": "The sidebar navigation shows a limited set of menu items for anonymous users. After login, additional items appear based on the user's assigned role.", - "articles": [ - "0.1", - "1.4" - ], - "tags": [ - "sidebar", - "navigation", - "anonymous", - "menu" - ], - "useFor": "Illustrate sidebar structure before discussing role-based menu visibility." - }, - { - "path": "screenshots-output/series-0-architecture/swagger-ui-overview.png", - "audioPath": "screenshots-output/series-0-architecture/swagger-ui-overview.wav", - "series": "series-0-architecture", - "filename": "swagger-ui-overview.png", - "capturedAt": "2026-04-22T03:37:02.955Z", - "description": "NSwag Swagger UI for the TalentManagement .NET 10 Web API — all versioned controller groups collapsed.", - "narration": "The Swagger UI confirms the .NET 10 Web API is running on port 44378. You can see all the controller groups — Employees, Departments, Positions, and the AI endpoints added in Series 6.", - "articles": [ - "0.1", - "2.1", - "2.4", - "6.1" - ], - "tags": [ - "swagger", - "api", - "dotnet", - "nswag", - "overview" - ], - "useFor": "Show the API is running and document the full endpoint surface." - }, - { - "path": "screenshots-output/series-1-authentication/identityserver-login-form.png", - "audioPath": "screenshots-output/series-1-authentication/identityserver-login-form.wav", - "series": "series-1-authentication", - "filename": "identityserver-login-form.png", - "capturedAt": "2026-04-22T03:37:23.444Z", - "description": "Duende IdentityServer 7.0 login page — Username and Password fields, Login button. Reached after clicking Login in the Angular user menu.", - "narration": "Clicking Login redirects to Duende IdentityServer — the token service in our CAT architecture. Enter your username and password here to receive an ID token and access token via the OAuth 2.0 PKCE flow.", - "articles": [ - "1.1" - ], - "tags": [ - "identityserver", - "login", - "oauth2", - "oidc", - "pkce" - ], - "useFor": "Illustrate the OIDC redirect step in the OAuth 2.0 PKCE flow." - }, - { - "path": "screenshots-output/series-1-authentication/user-menu-anonymous.png", - "audioPath": "screenshots-output/series-1-authentication/user-menu-anonymous.wav", - "series": "series-1-authentication", - "filename": "user-menu-anonymous.png", - "capturedAt": "2026-04-22T03:37:37.810Z", - "description": "Angular app header user menu expanded in anonymous state — shows only the Login option.", - "narration": "Clicking the user icon in the top right opens a dropdown with a single Login option. This is where users start the OAuth 2.0 login flow.", - "articles": [ - "1.1" - ], - "tags": [ - "user-menu", - "anonymous", - "header", - "login-button" - ], - "useFor": "Show where users click to initiate the OIDC login flow." - }, - { - "path": "screenshots-output/series-1-authentication/dashboard-authenticated.png", - "audioPath": "screenshots-output/series-1-authentication/dashboard-authenticated.wav", - "series": "series-1-authentication", - "filename": "dashboard-authenticated.png", - "capturedAt": "2026-04-22T03:37:59.494Z", - "description": "TalentManagement dashboard after successful OAuth 2.0 PKCE login as Manager — metrics cards, sidebar with manager-visible items, authenticated user avatar in header.", - "narration": "After a successful login, IdentityServer redirects back to the Angular app with an access token. The dashboard loads with live workforce metrics, and the sidebar now shows the Manager's available features.", - "articles": [ - "1.1", - "1.2", - "1.4" - ], - "tags": [ - "dashboard", - "authenticated", - "manager", - "post-login", - "oauth2" - ], - "useFor": "Hero image showing the successful result of the OIDC login flow." - }, - { - "path": "screenshots-output/series-1-authentication/user-menu-authenticated.png", - "audioPath": "screenshots-output/series-1-authentication/user-menu-authenticated.wav", - "series": "series-1-authentication", - "filename": "user-menu-authenticated.png", - "capturedAt": "2026-04-22T03:38:21.333Z", - "description": "Angular app header user menu after login — shows Profile, Settings, and Logout options alongside the authenticated username.", - "narration": "Once logged in, the user menu expands to show Profile, Settings, and Logout. The Profile page is particularly useful for inspecting the ID token and access token returned by IdentityServer.", - "articles": [ - "1.1", - "1.2" - ], - "tags": [ - "user-menu", - "authenticated", - "header", - "logout", - "profile" - ], - "useFor": "Show the post-login user menu options including Profile and Logout." - }, - { - "path": "screenshots-output/series-1-authentication/sidebar-hradmin-full-menu.png", - "audioPath": "screenshots-output/series-1-authentication/sidebar-hradmin-full-menu.wav", - "series": "series-1-authentication", - "filename": "sidebar-hradmin-full-menu.png", - "capturedAt": "2026-04-22T03:38:40.992Z", - "description": "Sidebar as seen by HRAdmin role — all menu items visible including Positions, Salary Ranges, and AI Assistant.", - "narration": "Logged in as HRAdmin, the sidebar shows the complete menu including Positions, Salary Ranges, and the AI Assistant — features restricted to the administrator role using ngx-permissions.", - "articles": [ - "1.4" - ], - "tags": [ - "sidebar", - "hradmin", - "role-based-ui", - "menu", - "ngx-permissions" - ], - "useFor": "Contrast with the Manager sidebar to demonstrate role-based UI rendering." - }, - { - "path": "screenshots-output/series-1-authentication/sidebar-manager-limited-menu.png", - "audioPath": "screenshots-output/series-1-authentication/sidebar-manager-limited-menu.wav", - "series": "series-1-authentication", - "filename": "sidebar-manager-limited-menu.png", - "capturedAt": "2026-04-22T03:39:02.444Z", - "description": "Sidebar as seen by Manager role — Positions and Salary Ranges hidden; only Employee and Department management visible.", - "narration": "As a Manager, the sidebar shows only Employee and Department management. Positions and Salary Ranges are hidden — ngx-permissions reads the roles claim from the access token and removes those menu items automatically.", - "articles": [ - "1.4" - ], - "tags": [ - "sidebar", - "manager", - "role-based-ui", - "menu", - "ngx-permissions" - ], - "useFor": "Pair with the HRAdmin sidebar for a before/after role comparison." - }, - { - "path": "screenshots-output/series-1-authentication/identityserver-logout-screen.png", - "audioPath": "screenshots-output/series-1-authentication/identityserver-logout-screen.wav", - "series": "series-1-authentication", - "filename": "identityserver-logout-screen.png", - "capturedAt": "2026-04-22T03:39:26.001Z", - "description": "Duende IdentityServer logout confirmation screen with a \"click here\" link to return to the Angular app.", - "narration": "Logout is handled by IdentityServer, not Angular. This screen confirms the session has been terminated. Clicking the link returns to the Angular app in Guest mode.", - "articles": [ - "1.1" - ], - "tags": [ - "identityserver", - "logout", - "oauth2", - "oidc", - "session" - ], - "useFor": "Illustrate the IdentityServer-managed logout redirect step." - }, - { - "path": "screenshots-output/series-2-dotnet-api/swagger-employees-endpoints.png", - "audioPath": "screenshots-output/series-2-dotnet-api/swagger-employees-endpoints.wav", - "series": "series-2-dotnet-api", - "filename": "swagger-employees-endpoints.png", - "capturedAt": "2026-04-22T03:39:44.273Z", - "description": "Swagger UI Employees controller expanded — GET, POST, PUT, DELETE endpoints with versioning and JWT lock icons.", - "narration": "The Employees controller exposes a full set of versioned REST endpoints. The lock icons indicate which routes require a Bearer token — in this case all of them except the read endpoint for anonymous access.", - "articles": [ - "2.1", - "2.3", - "2.4" - ], - "tags": [ - "swagger", - "employees", - "crud", - "versioning", - "jwt", - "dotnet" - ], - "useFor": "Document the employee CRUD API surface and JWT auth requirement." - }, - { - "path": "screenshots-output/series-2-dotnet-api/swagger-ai-endpoints.png", - "audioPath": "screenshots-output/series-2-dotnet-api/swagger-ai-endpoints.wav", - "series": "series-2-dotnet-api", - "filename": "swagger-ai-endpoints.png", - "capturedAt": "2026-04-22T03:40:08.777Z", - "description": "Swagger UI AI controller expanded — POST /ai/chat, POST /ai/hr-insight, POST /ai/nl-employee-search.", - "narration": "Series 6 adds an AI controller with three endpoints. The chat endpoint accepts any question. The H R insight endpoint grounds the answer in live workforce data. And the natural language search endpoint parses plain English into structured employee filter parameters.", - "articles": [ - "6.1", - "6.2", - "6.5" - ], - "tags": [ - "swagger", - "ai", - "chat", - "hr-insight", - "nl-search", - "ollama" - ], - "useFor": "Show the full AI endpoint surface after enabling the AiEnabled feature flag." - }, - { - "path": "screenshots-output/series-2-dotnet-api/swagger-ai-chat-endpoint.png", - "audioPath": "screenshots-output/series-2-dotnet-api/swagger-ai-chat-endpoint.wav", - "series": "series-2-dotnet-api", - "filename": "swagger-ai-chat-endpoint.png", - "capturedAt": "2026-04-22T03:40:29.555Z", - "description": "Swagger UI POST /api/v1/ai/chat endpoint expanded — shows request body schema with message and systemPrompt fields.", - "narration": "The chat endpoint accepts two fields: message is the question to ask, and systemPrompt is an optional instruction that controls the AI's persona or constraints. Both are plain strings — no special formatting required.", - "articles": [ - "6.1" - ], - "tags": [ - "swagger", - "ai", - "chat", - "request-body", - "system-prompt" - ], - "useFor": "Illustrate how to test the AI chat endpoint from Swagger in Article 6.1." - }, - { - "path": "screenshots-output/series-3-angular-material/dashboard-metrics-charts.png", - "audioPath": "screenshots-output/series-3-angular-material/dashboard-metrics-charts.wav", - "series": "series-3-angular-material", - "filename": "dashboard-metrics-charts.png", - "capturedAt": "2026-04-22T03:40:55.796Z", - "description": "Dashboard showing KPI metric cards (total employees, departments, new hires) and Chart.js bar/doughnut charts for department and gender distribution.", - "narration": "The dashboard displays live workforce metrics as Material Design cards and Chart.js visualisations. The data comes from a single API call to the dashboard metrics endpoint, which aggregates counts across employees, departments, and positions.", - "articles": [ - "3.1", - "3.4", - "6.4" - ], - "tags": [ - "dashboard", - "charts", - "metrics", - "angular-material", - "chartjs", - "kpi" - ], - "useFor": "Hero image for dashboard articles; base screenshot for AI insights overlay comparison in Series 6." - }, - { - "path": "screenshots-output/series-3-angular-material/employee-list-table.png", - "audioPath": "screenshots-output/series-3-angular-material/employee-list-table.wav", - "series": "series-3-angular-material", - "filename": "employee-list-table.png", - "capturedAt": "2026-04-22T03:41:14.961Z", - "description": "Angular Material data table listing employees — sortable columns, pagination controls, and a search/filter bar.", - "narration": "The employee list uses an Angular Material data table with server-side sorting and pagination. The search bar filters results by name or department without reloading the page.", - "articles": [ - "3.1", - "6.5" - ], - "tags": [ - "employee-list", - "data-table", - "pagination", - "sorting", - "angular-material" - ], - "useFor": "Illustrate the Material Design data table component and the employee list feature." - }, - { - "path": "screenshots-output/series-3-angular-material/employee-create-form.png", - "audioPath": "screenshots-output/series-3-angular-material/employee-create-form.wav", - "series": "series-3-angular-material", - "filename": "employee-create-form.png", - "capturedAt": "2026-04-22T03:41:38.684Z", - "description": "Material dialog showing Create Employee reactive form — fields for name, email, department, position, hire date, gender — with inline validation.", - "narration": "Clicking Create opens a Material dialog with a reactive form. All fields use Angular Material form controls with built-in validation. Errors appear inline as you type, following the Material Design specification.", - "articles": [ - "3.2", - "3.3" - ], - "tags": [ - "employee-form", - "reactive-forms", - "mat-dialog", - "validation", - "angular-material" - ], - "useFor": "Illustrate the reactive form inside a Material dialog for the forms and dialogs articles." - }, - { - "path": "screenshots-output/series-3-angular-material/department-list-table.png", - "audioPath": "screenshots-output/series-3-angular-material/department-list-table.wav", - "series": "series-3-angular-material", - "filename": "department-list-table.png", - "capturedAt": "2026-04-22T03:41:59.872Z", - "description": "Department management page — Material data table with department names and edit/delete action buttons.", - "narration": "The department list follows the same Material table pattern as the employee list. Managers can create, edit, and delete departments. The table refreshes automatically after each operation.", - "articles": [ - "3.1" - ], - "tags": [ - "department-list", - "data-table", - "crud", - "angular-material" - ], - "useFor": "Illustrate the department management feature alongside the employee list." - }, - { - "path": "screenshots-output/series-3-angular-material/position-list-table.png", - "audioPath": "screenshots-output/series-3-angular-material/position-list-table.wav", - "series": "series-3-angular-material", - "filename": "position-list-table.png", - "capturedAt": "2026-04-22T03:42:19.272Z", - "description": "Position management page — HRAdmin-only table of job positions with title, department, and salary range columns.", - "narration": "Positions are visible only to the HRAdmin role. The ngx-permissions directive hides this page from Managers and Employees entirely — both in the sidebar and via route guard.", - "articles": [ - "1.4", - "3.1" - ], - "tags": [ - "position-list", - "hradmin", - "role-based-ui", - "data-table", - "ngx-permissions" - ], - "useFor": "Demonstrate HRAdmin-only feature access for role-based UI articles." - }, - { - "path": "screenshots-output/series-3-angular-material/salary-ranges-table.png", - "audioPath": "screenshots-output/series-3-angular-material/salary-ranges-table.wav", - "series": "series-3-angular-material", - "filename": "salary-ranges-table.png", - "capturedAt": "2026-04-22T03:42:38.471Z", - "description": "Salary Range management page restricted to HRAdmin — table with range label, minimum and maximum salary columns.", - "narration": "Salary ranges are an HRAdmin-only feature. They define the pay bands that Positions reference, creating a hierarchy from Salary Range down to Position down to Employee.", - "articles": [ - "1.4", - "3.1" - ], - "tags": [ - "salary-ranges", - "hradmin", - "role-based-ui", - "data-table" - ], - "useFor": "Show the HRAdmin-exclusive salary range management feature." - }, - { - "path": "screenshots-output/series-6-ai-app-features/ai-submenu-sidebar.png", - "audioPath": "screenshots-output/series-6-ai-app-features/ai-submenu-sidebar.wav", - "series": "series-6-ai-app-features", - "filename": "ai-submenu-sidebar.png", - "capturedAt": "2026-04-22T03:42:56.200Z", - "description": "Dashboard with the AI submenu expanded in the sidebar — shows four child items: AI Assistant, HR Insight, NL Search, Vector Search.", - "narration": "The AI section lives in its own collapsible group in the sidebar. Clicking the smart toy icon expands four child pages, each with its own dedicated route.", - "articles": [ - "6.3" - ], - "tags": [ - "ai-submenu", - "sidebar", - "menu-json", - "ng-matero", - "navigation" - ], - "useFor": "Hero image for Article 6.3 showing the submenu structure." - }, - { - "path": "screenshots-output/series-6-ai-app-features/ai-assistant-page-full.png", - "audioPath": "screenshots-output/series-6-ai-app-features/ai-assistant-page-full.wav", - "series": "series-6-ai-app-features", - "filename": "ai-assistant-page-full.png", - "capturedAt": "2026-04-22T03:43:17.592Z", - "description": "Full AI Assistant page at /ai/assistant — chat card with message input when aiEnabled is true, or an info banner when false.", - "narration": "The AI Assistant page is one of four pages in the AI submenu. When AI is enabled it shows a chat card with a message input and send button. When disabled, an info banner explains what to enable.", - "articles": [ - "6.3" - ], - "tags": [ - "ai-assistant", - "chat-ui", - "feature-flag", - "angular-material" - ], - "useFor": "Hero image for Article 6.3 showing the AI Assistant page." - }, - { - "path": "screenshots-output/series-6-ai-app-features/ai-assistant-empty-state.png", - "audioPath": "screenshots-output/series-6-ai-app-features/ai-assistant-empty-state.wav", - "series": "series-6-ai-app-features", - "filename": "ai-assistant-empty-state.png", - "capturedAt": "2026-04-22T03:43:39.229Z", - "description": "AI Assistant page — empty state before any messages, showing the \"Start a conversation\" prompt.", - "narration": "The initial state shows an empty chat card with a prompt to start a conversation. The message input sits at the bottom with a Send button.", - "articles": [ - "6.3" - ], - "tags": [ - "ai-assistant", - "empty-state", - "chat-ui" - ], - "useFor": "Show the initial state at the start of the Article 6.3 demo." - }, - { - "path": "screenshots-output/series-6-ai-app-features/ai-hr-insight-empty.png", - "audioPath": "screenshots-output/series-6-ai-app-features/ai-hr-insight-empty.wav", - "series": "series-6-ai-app-features", - "filename": "ai-hr-insight-empty.png", - "capturedAt": "2026-04-22T03:43:54.497Z", - "description": "HR Insight page at /ai/hr-insight — empty state showing four suggestion buttons and the question input.", - "narration": "The H R Insight page shows suggestion buttons for common workforce questions. Clicking one pre-fills the input field.", - "articles": [ - "6.3" - ], - "tags": [ - "ai-hr-insight", - "suggestion-buttons", - "empty-state" - ], - "useFor": "Show the HR Insight page layout in Article 6.3." - }, - { - "path": "screenshots-output/series-6-ai-app-features/ai-hr-insight-with-answer.png", - "audioPath": "screenshots-output/series-6-ai-app-features/ai-hr-insight-with-answer.wav", - "series": "series-6-ai-app-features", - "filename": "ai-hr-insight-with-answer.png", - "capturedAt": "2026-04-22T03:44:38.430Z", - "description": "HR Insight page showing a data-grounded Ollama answer — references live department headcounts. Execution time shown below reply.", - "narration": "The H R Insight answer references real numbers from the database. The execution time shown below the reply includes both the database query and Ollama inference time.", - "articles": [ - "6.2", - "6.3" - ], - "tags": [ - "ai-hr-insight", - "rag", - "grounded-answer", - "execution-time", - "ollama" - ], - "useFor": "Key proof-of-concept image for Articles 6.2 and 6.3." - }, - { - "path": "screenshots-output/series-6-ai-app-features/ai-nl-search-empty.png", - "audioPath": "screenshots-output/series-6-ai-app-features/ai-nl-search-empty.wav", - "series": "series-6-ai-app-features", - "filename": "ai-nl-search-empty.png", - "capturedAt": "2026-04-22T03:44:53.537Z", - "description": "Natural Language Search page at /ai/nl-search — empty state with search input and prompt to type a query.", - "narration": "The NL Search page shows a single text input. Type a plain-English description of the employees you are looking for.", - "articles": [ - "6.4" - ], - "tags": [ - "ai-nl-search", - "empty-state", - "natural-language" - ], - "useFor": "Show the initial NL Search state in Article 6.4." - }, - { - "path": "screenshots-output/series-6-ai-app-features/ai-vector-search-empty.png", - "audioPath": "screenshots-output/series-6-ai-app-features/ai-vector-search-empty.wav", - "series": "series-6-ai-app-features", - "filename": "ai-vector-search-empty.png", - "capturedAt": "2026-04-22T03:45:08.808Z", - "description": "Vector Search page at /ai/vector-search — empty state with search input and prompt to describe a position.", - "narration": "The Vector Search page uses semantic similarity to find positions. Describe what you are looking for in plain English.", - "articles": [ - "6.5" - ], - "tags": [ - "ai-vector-search", - "empty-state", - "semantic-search" - ], - "useFor": "Show the initial Vector Search state in Article 6.5." - } - ] -} \ No newline at end of file diff --git a/docs/screenshots/series-6-ai-app-features/ai-assistant-empty-state.png b/docs/screenshots/series-6-ai-app-features/ai-assistant-empty-state.png deleted file mode 100644 index 7cd6021..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/ai-assistant-empty-state.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/ai-assistant-page-full.png b/docs/screenshots/series-6-ai-app-features/ai-assistant-page-full.png deleted file mode 100644 index 7cd6021..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/ai-assistant-page-full.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/ai-hr-insight-empty.png b/docs/screenshots/series-6-ai-app-features/ai-hr-insight-empty.png deleted file mode 100644 index aa88ba2..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/ai-hr-insight-empty.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/ai-hr-insight-with-answer.png b/docs/screenshots/series-6-ai-app-features/ai-hr-insight-with-answer.png deleted file mode 100644 index 52a0160..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/ai-hr-insight-with-answer.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/ai-nl-search-empty.png b/docs/screenshots/series-6-ai-app-features/ai-nl-search-empty.png deleted file mode 100644 index 74e59ed..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/ai-nl-search-empty.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/ai-submenu-sidebar.png b/docs/screenshots/series-6-ai-app-features/ai-submenu-sidebar.png deleted file mode 100644 index 9b2945b..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/ai-submenu-sidebar.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/ai-vector-search-empty.png b/docs/screenshots/series-6-ai-app-features/ai-vector-search-empty.png deleted file mode 100644 index 6da2c16..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/ai-vector-search-empty.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/dashboard-metrics-charts.png b/docs/screenshots/series-6-ai-app-features/dashboard-metrics-charts.png deleted file mode 100644 index 9b2945b..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/dashboard-metrics-charts.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/employee-list-table.png b/docs/screenshots/series-6-ai-app-features/employee-list-table.png deleted file mode 100644 index 6f07325..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/employee-list-table.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/swagger-ai-chat-endpoint.png b/docs/screenshots/series-6-ai-app-features/swagger-ai-chat-endpoint.png deleted file mode 100644 index 40f2f14..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/swagger-ai-chat-endpoint.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/swagger-ai-endpoints.png b/docs/screenshots/series-6-ai-app-features/swagger-ai-endpoints.png deleted file mode 100644 index eec379b..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/swagger-ai-endpoints.png and /dev/null differ diff --git a/docs/screenshots/series-6-ai-app-features/swagger-ui-overview.png b/docs/screenshots/series-6-ai-app-features/swagger-ui-overview.png deleted file mode 100644 index 95058ee..0000000 Binary files a/docs/screenshots/series-6-ai-app-features/swagger-ui-overview.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-api-endpoints.png b/docs/screenshots/webapi/swagger-api-endpoints.png deleted file mode 100644 index 9a85a4c..0000000 Binary files a/docs/screenshots/webapi/swagger-api-endpoints.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-cache-resource-expanded.png b/docs/screenshots/webapi/swagger-cache-resource-expanded.png deleted file mode 100644 index 4738d36..0000000 Binary files a/docs/screenshots/webapi/swagger-cache-resource-expanded.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-dashboard-resource-expanded.png b/docs/screenshots/webapi/swagger-dashboard-resource-expanded.png deleted file mode 100644 index aeebae6..0000000 Binary files a/docs/screenshots/webapi/swagger-dashboard-resource-expanded.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-departments-resource-expanded.png b/docs/screenshots/webapi/swagger-departments-resource-expanded.png deleted file mode 100644 index a69f296..0000000 Binary files a/docs/screenshots/webapi/swagger-departments-resource-expanded.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-employee-resource-expanded.png b/docs/screenshots/webapi/swagger-employee-resource-expanded.png deleted file mode 100644 index ad36a25..0000000 Binary files a/docs/screenshots/webapi/swagger-employee-resource-expanded.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-employees-resource-expanded.png b/docs/screenshots/webapi/swagger-employees-resource-expanded.png deleted file mode 100644 index ad36a25..0000000 Binary files a/docs/screenshots/webapi/swagger-employees-resource-expanded.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-meta-resource-expanded.png b/docs/screenshots/webapi/swagger-meta-resource-expanded.png deleted file mode 100644 index 253152e..0000000 Binary files a/docs/screenshots/webapi/swagger-meta-resource-expanded.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-positions-resource-expanded.png b/docs/screenshots/webapi/swagger-positions-resource-expanded.png deleted file mode 100644 index 0dc589c..0000000 Binary files a/docs/screenshots/webapi/swagger-positions-resource-expanded.png and /dev/null differ diff --git a/docs/screenshots/webapi/swagger-salaryranges-resource-expanded.png b/docs/screenshots/webapi/swagger-salaryranges-resource-expanded.png deleted file mode 100644 index 58463f6..0000000 Binary files a/docs/screenshots/webapi/swagger-salaryranges-resource-expanded.png and /dev/null differ diff --git a/docs/screenshots/webapi/webapi-image-catalog.md b/docs/screenshots/webapi/webapi-image-catalog.md deleted file mode 100644 index 891cc31..0000000 --- a/docs/screenshots/webapi/webapi-image-catalog.md +++ /dev/null @@ -1,28 +0,0 @@ -# Web API Image Catalog - -1. `swagger-api-endpoints.png` -Figure title: Swagger overview of available API endpoints - -2. `swagger-cache-resource-expanded.png` -Figure title: Expanded Cache resource endpoints in Swagger - -3. `swagger-dashboard-resource-expanded.png` -Figure title: Expanded Dashboard resource endpoints in Swagger - -4. `swagger-departments-resource-expanded.png` -Figure title: Expanded Departments resource endpoints in Swagger - -5. `swagger-employee-resource-expanded.png` -Figure title: Expanded Employee resource endpoints in Swagger - -6. `swagger-employees-resource-expanded.png` -Figure title: Expanded Employees resource endpoints in Swagger - -7. `swagger-meta-resource-expanded.png` -Figure title: Expanded Meta resource endpoints in Swagger - -8. `swagger-positions-resource-expanded.png` -Figure title: Expanded Positions resource endpoints in Swagger - -9. `swagger-salaryranges-resource-expanded.png` -Figure title: Expanded SalaryRanges resource endpoints in Swagger diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts deleted file mode 100644 index 54a906a..0000000 --- a/e2e/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/e2e/seed.spec.ts b/e2e/seed.spec.ts deleted file mode 100644 index ef5ce4c..0000000 --- a/e2e/seed.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Test group', () => { - test('seed', async ({ page }) => { - // generate code here. - }); -}); diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..57614f9 Binary files /dev/null and b/favicon.ico differ diff --git a/fonts/Material_Icons.css b/fonts/Material_Icons.css new file mode 100644 index 0000000..95f3add --- /dev/null +++ b/fonts/Material_Icons.css @@ -0,0 +1,23 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url(./flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} diff --git a/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2 b/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2 new file mode 100644 index 0000000..34cdd2a Binary files /dev/null and b/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2 differ diff --git a/i18n/en-US.json b/i18n/en-US.json new file mode 100644 index 0000000..05e6ffa --- /dev/null +++ b/i18n/en-US.json @@ -0,0 +1,159 @@ +{ + "menu": { + "dashboard": "Dashboard", + "employees": "Employees", + "employees.employeeList": "List", + "employees.addEmployee": "Create", + "departments": "Departments", + "departments.departmentList": "List", + "departments.addDepartment": "Create", + "positions": "Positions", + "positions.positionList": "List", + "positions.addPosition": "Create", + "salaryRanges": "Salary Ranges", + "salaryRanges.salaryRangeList": "List", + "salaryRanges.addSalaryRange": "Create", + "ai": "AI", + "ai.aiAssistant": "AI Assistant", + "ai.aiHrInsight": "HR Insight", + "ai.aiNlSearch": "NL Search", + "ai.aiVectorSearch": "Vector Search", + "design": "Design", + "design.colors": "Color System", + "design.icons": "Material Icons", + "material": "Material", + "material.form-controls": "Form Controls", + "material.form-controls.autocomplete": "Autocomplete", + "material.form-controls.checkbox": "Checkbox", + "material.form-controls.datepicker": "Datepicker", + "material.form-controls.form-field": "Form Field", + "material.form-controls.input": "Input", + "material.form-controls.radio": "Radio", + "material.form-controls.select": "Select", + "material.form-controls.slider": "Slider", + "material.form-controls.slide-toggle": "Slide Toggle", + "material.navigation": "Navigation", + "material.navigation.menu": "Menu", + "material.navigation.sidenav": "Sidenav", + "material.navigation.toolbar": "Toolbar", + "material.layout": "Layout", + "material.layout.card": "Card", + "material.layout.divider": "Divider", + "material.layout.expansion": "Expansion Panel", + "material.layout.grid-list": "Grid List", + "material.layout.list": "List", + "material.layout.stepper": "Stepper", + "material.layout.tab": "Tab", + "material.layout.tree": "Tree", + "material.buttons-indicators": "Buttons & Indicators", + "material.buttons-indicators.button": "Buttons", + "material.buttons-indicators.button-toggle": "Button Toggle", + "material.buttons-indicators.badge": "Badge", + "material.buttons-indicators.chips": "Chips", + "material.buttons-indicators.icon": "Icon", + "material.buttons-indicators.progress-spinner": "Progress Spinner", + "material.buttons-indicators.progress-bar": "Progress Bar", + "material.buttons-indicators.ripple": "Ripple", + "material.popups-modals": "Popups & Modals", + "material.popups-modals.bottom-sheet": "Bottom Sheet", + "material.popups-modals.dialog": "Dialog", + "material.popups-modals.snackbar": "Snackbar", + "material.popups-modals.tooltip": "Tooltip", + "material.data-table": "Data Table", + "material.data-table.paginator": "Paginator", + "material.data-table.sort": "Sort", + "material.data-table.table": "Table", + "media": "Media", + "media.gallery": "Gallery", + "forms": "Forms", + "forms.form-elements": "Form Elements", + "forms.dynamic-form": "Dynamic Form", + "forms.select": "Select", + "forms.datetime": "Date Time", + "tables": "Tables", + "tables.kitchen-sink": "Kitchen Sink", + "tables.remote-data": "Remote Data", + "profile": "Profile", + "profile.overview": "Overview", + "profile.settings": "Settings", + "extensions": "Extensions", + "sessions": "Sessions", + "sessions.403": "403", + "sessions.404": "404", + "sessions.500": "500", + "utilities": "Utilities", + "utilities.css-grid": "CSS Grid", + "utilities.css-helpers": "CSS Helpers", + "menu-level": "Menu Level", + "menu-level.level-1-1": "Level 1.1", + "menu-level.level-1-2": "Level 1.2", + "menu-level.level-1-1.level-2-1": "Level 2.1", + "menu-level.level-1-1.level-2-2": "Level 2.2", + "menu-level.level-1-1.level-2-1.level-3-1": "Level 3.1", + "menu-level.level-1-1.level-2-1.level-3-1.level-4-1": "Level 4.1", + "permissions": "Permissions", + "permissions.role-switching": "Role Switching", + "permissions.route-guard": "Route Guard", + "permissions.test": "Permission Test" + }, + "validation": { + "required": "This field is required", + "min_length": "This value should be no less than {{number}} characters", + "max_length": "This value should be no more than {{number}} characters", + "min": "This value should be no less than {{number}}", + "max": "This value should be no more than {{number}}", + "exist": "The {{value}} has exists", + "inconsistent": "Inconsistent with {{value}}", + "invalid_email": "Invalid email" + }, + "paginator": { + "items_per_page_label": "Items per page:", + "next_page_label": "Next page", + "previous_page_label": "Previous page", + "first_page_label": "First page", + "last_page_label": "Last page", + "range_page_label_1": "no record", + "range_page_label_2": "{{startIndex}} - {{endIndex}} of {{length}}" + }, + "en_us": "English", + "zh_cn": "Simplified Chinese", + "zh_tw": "Traditional Chinese", + "system": "System", + "position": "Position", + "name": "Name", + "weight": "Weight", + "symbol": "Symbol", + "gender": "Gender", + "mobile": "Mobile", + "tele": "Telephone", + "birthday": "Birthday", + "city": "City", + "address": "Address", + "website": "Website", + "company": "Company", + "email": "Email", + "operation": "Operation", + "edit": "Edit", + "delete": "Delete", + "confirm_delete": "Confirm delete?", + "ok": "Ok", + "close": "Close", + "profile": "Profile", + "edit_profile": "Edit profile", + "logout": "logout", + "login": "Login", + "login_title": "Welcome Back", + "have_no_account": "Don't have an account", + "create_one_account": "Click here to create one", + "please_enter": "Please enter", + "username": "Username", + "password": "Password", + "confirm_password": "Confirm Password", + "remember_me": "Remember Me", + "register": "Create account", + "register_welcome": "Welcome", + "register_title": "It only takes a few seconds to create your account", + "read_and_agree": "I have read and agree to the terms of service", + "have_an_account": "Already have an account", + "restore_defaults": "Restore defaults" +} diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json new file mode 100644 index 0000000..3770a76 --- /dev/null +++ b/i18n/zh-CN.json @@ -0,0 +1,142 @@ +{ + "menu": { + "dashboard": "数据大盘", + "design": "设计", + "design.colors": "颜色系统", + "design.icons": "Material 图标库", + "material": "Material", + "material.form-controls": "表单控件", + "material.form-controls.autocomplete": "自动完成", + "material.form-controls.checkbox": "检查框", + "material.form-controls.datepicker": "日期选择器", + "material.form-controls.form-field": "表单字段", + "material.form-controls.input": "输入框", + "material.form-controls.radio": "单选按钮", + "material.form-controls.select": "选择框", + "material.form-controls.slider": "滑竿", + "material.form-controls.slide-toggle": "滑块开关", + "material.navigation": "导航", + "material.navigation.menu": "菜单", + "material.navigation.sidenav": "侧边栏", + "material.navigation.toolbar": "工具栏", + "material.layout": "布局", + "material.layout.card": "卡片", + "material.layout.divider": "分割器", + "material.layout.expansion": "可展开面板", + "material.layout.grid-list": "网格列表", + "material.layout.list": "列表", + "material.layout.stepper": "步进器", + "material.layout.tab": "选项卡", + "material.layout.tree": "树", + "material.buttons-indicators": "按钮与指示器", + "material.buttons-indicators.button": "按钮", + "material.buttons-indicators.button-toggle": "开关按钮", + "material.buttons-indicators.badge": "徽章", + "material.buttons-indicators.chips": "芯片", + "material.buttons-indicators.icon": "图标", + "material.buttons-indicators.progress-spinner": "进度圈", + "material.buttons-indicators.progress-bar": "进度条", + "material.buttons-indicators.ripple": "水波", + "material.popups-modals": "弹框与模态框", + "material.popups-modals.bottom-sheet": "底部操作表", + "material.popups-modals.dialog": "对话框", + "material.popups-modals.snackbar": "快餐栏", + "material.popups-modals.tooltip": "提示框", + "material.data-table": "数据表", + "material.data-table.paginator": "分页器", + "material.data-table.sort": "排序头", + "material.data-table.table": "表格", + "media": "媒体", + "media.gallery": "图片画廊", + "forms": "表单", + "forms.form-elements": "表单元素", + "forms.dynamic-form": "动态表单", + "forms.select": "选择框", + "forms.datetime": "日期时间", + "tables": "表格", + "tables.kitchen-sink": "基础演示", + "tables.remote-data": "远程数据", + "profile": "个人信息", + "profile.overview": "概述", + "profile.settings": "设置", + "extensions": "扩展组件库", + "sessions": "会话", + "sessions.403": "403", + "sessions.404": "404", + "sessions.500": "500", + "utilities": "辅助工具", + "utilities.css-grid": "CSS 栅格", + "utilities.css-helpers": "CSS 辅助类", + "menu-level": "菜单层级", + "menu-level.level-1-1": "层级 1.1", + "menu-level.level-1-2": "层级 1.2", + "menu-level.level-1-1.level-2-1": "层级 2.1", + "menu-level.level-1-1.level-2-2": "层级 2.2", + "menu-level.level-1-1.level-2-1.level-3-1": "层级 3.1", + "menu-level.level-1-1.level-2-1.level-3-1.level-4-1": "层级 4.1", + "permissions": "权限管理", + "permissions.role-switching": "切换角色", + "permissions.route-guard": "路由守卫", + "permissions.test": "权限测试" + }, + "validation": { + "required": "该字段必填", + "min_length": "必须不小于 {{number}} 个字符", + "max_length": "必须不大于 {{number}} 个字符", + "min": "值必须不小于 {{number}}", + "max": "值必须不大于 {{number}}", + "exist": "{{value}} 已存在", + "inconsistent": "与 {{value}} 不一致", + "invalid_email": "邮箱格式不正确" + }, + "paginator": { + "items_per_page_label": "每页共:", + "next_page_label": "下一页", + "previous_page_label": "前一页", + "first_page_label": "首页", + "last_page_label": "尾页", + "range_page_label_1": "无数据", + "range_page_label_2": "第 {{startIndex}} - {{endIndex}} 行,共 {{length}} 行" + }, + "en_us": "英语", + "zh_cn": "简体中文", + "zh_tw": "繁体中文", + "system": "跟随系统", + "position": "序号", + "name": "姓名", + "weight": "体重", + "symbol": "代号", + "gender": "性别", + "mobile": "手机号", + "tele": "固话", + "birthday": "出生日期", + "city": "城市", + "address": "家庭地址", + "company": "公司", + "website": "网址", + "email": "邮箱", + "operation": "操作", + "edit": "编辑", + "delete": "删除", + "confirm_delete": "确认删除?", + "ok": "确定", + "close": "关闭", + "profile": "个人信息", + "edit_profile": "修改资料", + "logout": "退出", + "login": "登入", + "login_title": "欢迎回来", + "have_no_account": "没有账号", + "create_one_account": "点此创建一个", + "please_enter": "请输入", + "username": "用户名", + "password": "密码", + "confirm_password": "确认密码", + "remember_me": "记住我", + "register": "创建账号", + "register_welcome": "欢迎", + "register_title": "创建您的帐户只需几秒钟", + "read_and_agree": "我已阅读并同意服务条款", + "have_an_account": "已经有帐号了", + "restore_defaults": "还原配置" +} diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json new file mode 100644 index 0000000..49e12a7 --- /dev/null +++ b/i18n/zh-TW.json @@ -0,0 +1,142 @@ +{ + "menu": { + "dashboard": "數據大盤", + "design": "設計", + "design.colors": "颜色系统", + "design.icons": "Material 圖標庫", + "material": "Material", + "material.form-controls": "表單控件", + "material.form-controls.autocomplete": "自動完成", + "material.form-controls.checkbox": "檢查框", + "material.form-controls.datepicker": "日期選擇器", + "material.form-controls.form-field": "表單字段", + "material.form-controls.input": "輸入框", + "material.form-controls.radio": "單選按鈕", + "material.form-controls.select": "選擇框", + "material.form-controls.slider": "滑竿", + "material.form-controls.slide-toggle": "滑塊開關", + "material.navigation": "導航", + "material.navigation.menu": "菜單", + "material.navigation.sidenav": "側邊欄", + "material.navigation.toolbar": "工具欄", + "material.layout": "布局", + "material.layout.card": "卡片", + "material.layout.divider": "分割器", + "material.layout.expansion": "可展開面板", + "material.layout.grid-list": "網格列表", + "material.layout.list": "列表", + "material.layout.stepper": "步進器", + "material.layout.tab": "選項卡", + "material.layout.tree": "樹", + "material.buttons-indicators": "按鈕與指示器", + "material.buttons-indicators.button": "按鈕", + "material.buttons-indicators.button-toggle": "開關按鈕", + "material.buttons-indicators.badge": "徽章", + "material.buttons-indicators.chips": "芯片", + "material.buttons-indicators.icon": "圖標", + "material.buttons-indicators.progress-spinner": "進度圈", + "material.buttons-indicators.progress-bar": "進度條", + "material.buttons-indicators.ripple": "水波", + "material.popups-modals": "彈框與模態框", + "material.popups-modals.bottom-sheet": "底部操作表", + "material.popups-modals.dialog": "對話框", + "material.popups-modals.snackbar": "快餐欄", + "material.popups-modals.tooltip": "提示框", + "material.data-table": "數據表", + "material.data-table.paginator": "分頁器", + "material.data-table.sort": "排序頭", + "material.data-table.table": "表格", + "media": "媒體", + "media.gallery": "圖片畫廊", + "forms": "表單", + "forms.form-elements": "表單元素", + "forms.dynamic-form": "動態表單", + "forms.select": "選擇框", + "forms.datetime": "日期時間", + "tables": "表格", + "tables.kitchen-sink": "基礎演示", + "tables.remote-data": "遠程數據", + "profile": "個人信息", + "profile.overview": "概述", + "profile.settings": "設置", + "extensions": "擴展組件庫", + "sessions": "會話", + "sessions.403": "403", + "sessions.404": "404", + "sessions.500": "500", + "utilities": "輔助工具", + "utilities.css-grid": "CSS 柵格", + "utilities.css-helpers": "CSS 輔助類", + "menu-level": "菜單層級", + "menu-level.level-1-1": "層級 1.1", + "menu-level.level-1-2": "層級 1.2", + "menu-level.level-1-1.level-2-1": "層級 2.1", + "menu-level.level-1-1.level-2-2": "層級 2.2", + "menu-level.level-1-1.level-2-1.level-3-1": "層級 3.1", + "menu-level.level-1-1.level-2-1.level-3-1.level-4-1": "層級 4.1", + "permissions": "權限管理", + "permissions.role-switching": "切換角色", + "permissions.route-guard": "路由守衛", + "permissions.test": "權限測試" + }, + "validation": { + "required": "該字段必填", + "min_length": "必須不小于 {{number}} 個字符", + "max_length": "必須不大于 {{number}} 個字符", + "min": "值必须不小于 {{number}}", + "max": "值必須不大于 {{number}}", + "exist": "{{value}} 已存在", + "inconsistent": "與 {{value}} 不壹致", + "invalid_email": "郵箱格式不正確" + }, + "paginator": { + "items_per_page_label": "每頁共:", + "next_page_label": "下壹頁", + "previous_page_label": "前壹頁", + "first_page_label": "首頁", + "last_page_label": "尾頁", + "range_page_label_1": "無數據", + "range_page_label_2": "第 {{startIndex}} - {{endIndex}} 行,共 {{length}} 行" + }, + "en_us": "英語", + "zh_cn": "簡體中文", + "zh_tw": "繁體中文", + "system": "跟隨系統", + "position": "序號", + "name": "姓名", + "weight": "體重", + "symbol": "代號", + "gender": "性别", + "mobile": "手機號", + "tele": "固話", + "birthday": "出生日期", + "city": "城市", + "address": "家庭地址", + "company": "公司", + "website": "網址", + "email": "郵箱", + "operation": "操作", + "edit": "編輯", + "delete": "删除", + "confirm_delete": "確認刪除?", + "ok": "確定", + "close": "關閉", + "profile": "個人信息", + "edit_profile": "修改資料", + "logout": "退出", + "login": "登入", + "login_title": "歡迎回來", + "have_no_account": "沒有賬號", + "create_one_account": "點此創建壹個", + "please_enter": "請輸入", + "username": "用戶名", + "password": "密碼", + "confirm_password": "確認密碼", + "remember_me": "記住我", + "register": "創建賬號", + "register_welcome": "歡迎", + "register_title": "創建您的帳戶只需幾秒鐘", + "read_and_agree": "我已閱讀並同意服務條款", + "have_an_account": "已經有帳號了", + "restore_defaults": "還原配置" +} diff --git a/images/avatar-default.jpg b/images/avatar-default.jpg new file mode 100644 index 0000000..8294366 Binary files /dev/null and b/images/avatar-default.jpg differ diff --git a/images/avatar.jpg b/images/avatar.jpg new file mode 100644 index 0000000..36c5681 Binary files /dev/null and b/images/avatar.jpg differ diff --git a/images/matero.png b/images/matero.png new file mode 100644 index 0000000..21ff683 Binary files /dev/null and b/images/matero.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..209b762 --- /dev/null +++ b/index.html @@ -0,0 +1,79 @@ + + + + + Talent Management + + + + + + + + + + +

LOADING

+ + diff --git a/infra/main.bicep b/infra/main.bicep deleted file mode 100644 index baa2995..0000000 --- a/infra/main.bicep +++ /dev/null @@ -1,138 +0,0 @@ -// ============================================================================= -// main.bicep — Talent Management full-stack Azure infrastructure -// ============================================================================= -// Provisions all resources for the AngularNetTutorial three-tier stack: -// - App Service Plan (B1, shared) -// - Web App: Talent Management API -// - Web App: Duende IdentityServer -// - Static Web App: Angular client (Free tier) -// - Azure SQL logical server (shared) -// - SQL database: TalentManagementApiDb -// - SQL database: IdentityServerDb -// -// Deploy command (from repo root): -// az deployment group create \ -// --resource-group rg-talent-dev \ -// --template-file infra/main.bicep \ -// --parameters infra/parameters/dev.bicepparam \ -// --parameters sqlAdminPassword=$SQL_ADMIN_PASSWORD -// ============================================================================= - -@description('Azure region for all resources') -param location string = resourceGroup().location - -@description('Name of the App Service Plan') -param appServicePlanName string - -@description('Name of the API Web App') -param apiAppName string - -@description('Name of the IdentityServer Web App') -param identityAppName string - -@description('Name of the IdentityServer Admin Web App') -param identityAdminAppName string - -@description('Name of the Angular Static Web App') -param staticWebAppName string - -@description('Name of the Azure SQL logical server') -param sqlServerName string - -@description('Name of the API database') -param apiDbName string - -@description('Name of the IdentityServer database') -param identityDbName string - -@description('SQL administrator login name') -param sqlAdminLogin string - -@description('SQL administrator password — pass from GitHub Secret, never store in parameters file') -@secure() -param sqlAdminPassword string - -@description('Name of the Azure Key Vault') -param keyVaultName string - -// ─── App Service Plan ───────────────────────────────────────────────────────── -module appServicePlan 'modules/appServicePlan.bicep' = { - name: 'appServicePlan' - params: { - appServicePlanName: appServicePlanName - location: location - } -} - -// ─── Web Apps ───────────────────────────────────────────────────────────────── -module apiApp 'modules/webApp.bicep' = { - name: 'apiApp' - params: { - webAppName: apiAppName - location: location - appServicePlanId: appServicePlan.outputs.id - } -} - -module identityApp 'modules/webApp.bicep' = { - name: 'identityApp' - params: { - webAppName: identityAppName - location: location - appServicePlanId: appServicePlan.outputs.id - } -} - -module identityAdminApp 'modules/webApp.bicep' = { - name: 'identityAdminApp' - params: { - webAppName: identityAdminAppName - location: location - appServicePlanId: appServicePlan.outputs.id - } -} - -// ─── Angular Static Web App ─────────────────────────────────────────────────── -// Static Web Apps are not available in eastus — use eastus2 -module angularSwa 'modules/staticWebApp.bicep' = { - name: 'angularSwa' - params: { - staticWebAppName: staticWebAppName - location: 'westus2' - } -} - -// ─── SQL Server + Databases ─────────────────────────────────────────────────── -module sqlServer 'modules/sqlServer.bicep' = { - name: 'sqlServer' - params: { - sqlServerName: sqlServerName - location: location - sqlAdminLogin: sqlAdminLogin - sqlAdminPassword: sqlAdminPassword - apiDbName: apiDbName - identityDbName: identityDbName - } -} - -// ─── Key Vault ──────────────────────────────────────────────────────────────── -module keyVault 'modules/keyVault.bicep' = { - name: 'keyVault' - params: { - keyVaultName: keyVaultName - location: location - readerPrincipalIds: [ - apiApp.outputs.principalId - identityApp.outputs.principalId - identityAdminApp.outputs.principalId - ] - } -} - -// ─── Outputs (used by deployment workflows and post-deployment config) ───────── -output apiAppUrl string = apiApp.outputs.url -output identityAppUrl string = identityApp.outputs.url -output identityAdminAppUrl string = identityAdminApp.outputs.url -output angularAppUrl string = angularSwa.outputs.url -output sqlServerFqdn string = sqlServer.outputs.sqlServerFqdn -output keyVaultUri string = keyVault.outputs.uri diff --git a/infra/modules/appServicePlan.bicep b/infra/modules/appServicePlan.bicep deleted file mode 100644 index 3c37093..0000000 --- a/infra/modules/appServicePlan.bicep +++ /dev/null @@ -1,19 +0,0 @@ -@description('Name of the App Service Plan') -param appServicePlanName string - -@description('Azure region for all resources') -param location string - -resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = { - name: appServicePlanName - location: location - sku: { - name: 'B1' - tier: 'Basic' - } - properties: { - reserved: false // Windows - } -} - -output id string = appServicePlan.id diff --git a/infra/modules/keyVault.bicep b/infra/modules/keyVault.bicep deleted file mode 100644 index 553a6fa..0000000 --- a/infra/modules/keyVault.bicep +++ /dev/null @@ -1,45 +0,0 @@ -@description('Name of the Key Vault') -param keyVaultName string - -@description('Azure region for the Key Vault') -param location string - -@description('Principal IDs of managed identities to grant secret read access') -param readerPrincipalIds array = [] - -resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { - name: keyVaultName - location: location - properties: { - sku: { - family: 'A' - name: 'standard' - } - tenantId: subscription().tenantId - enableRbacAuthorization: true - enableSoftDelete: true - softDeleteRetentionInDays: 7 - enabledForDeployment: false - enabledForTemplateDeployment: false - enabledForDiskEncryption: false - } -} - -// Grant each managed identity the Key Vault Secrets User role (read secrets) -@batchSize(1) -resource secretsUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (principalId, i) in readerPrincipalIds: { - // Role assignment scope must be the vault resource - scope: keyVault - // Deterministic GUID: vaultId + principalId - name: guid(keyVault.id, principalId, '4633458b-17de-408a-b874-0445c86b69e6') - properties: { - // Key Vault Secrets User built-in role - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') - principalId: principalId - principalType: 'ServicePrincipal' - } -}] - -output id string = keyVault.id -output name string = keyVault.name -output uri string = keyVault.properties.vaultUri diff --git a/infra/modules/sqlServer.bicep b/infra/modules/sqlServer.bicep deleted file mode 100644 index 2b0cba6..0000000 --- a/infra/modules/sqlServer.bicep +++ /dev/null @@ -1,73 +0,0 @@ -@description('Name of the Azure SQL logical server') -param sqlServerName string - -@description('Azure region for all resources') -param location string - -@description('SQL administrator login name') -param sqlAdminLogin string - -@description('SQL administrator password') -@secure() -param sqlAdminPassword string - -@description('Name of the API database') -param apiDbName string - -@description('Name of the IdentityServer database') -param identityDbName string - -resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = { - name: sqlServerName - location: location - properties: { - administratorLogin: sqlAdminLogin - administratorLoginPassword: sqlAdminPassword - minimalTlsVersion: '1.2' - publicNetworkAccess: 'Enabled' - } -} - -// Allow Azure services to connect (required for App Service) -resource allowAzureServices 'Microsoft.Sql/servers/firewallRules@2023-05-01-preview' = { - parent: sqlServer - name: 'AllowAllWindowsAzureIps' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } -} - -resource apiDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = { - parent: sqlServer - name: apiDbName - location: location - sku: { - name: 'Basic' - tier: 'Basic' - capacity: 5 - } - properties: { - collation: 'SQL_Latin1_General_CP1_CI_AS' - maxSizeBytes: 2147483648 // 2 GB - } -} - -resource identityDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = { - parent: sqlServer - name: identityDbName - location: location - sku: { - name: 'Basic' - tier: 'Basic' - capacity: 5 - } - properties: { - collation: 'SQL_Latin1_General_CP1_CI_AS' - maxSizeBytes: 2147483648 // 2 GB - } -} - -output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName -output apiDbName string = apiDbName -output identityDbName string = identityDbName diff --git a/infra/modules/staticWebApp.bicep b/infra/modules/staticWebApp.bicep deleted file mode 100644 index d13471f..0000000 --- a/infra/modules/staticWebApp.bicep +++ /dev/null @@ -1,23 +0,0 @@ -@description('Name of the Static Web App') -param staticWebAppName string - -@description('Azure region for all resources') -param location string - -resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = { - name: staticWebAppName - location: location - sku: { - name: 'Free' - tier: 'Free' - } - properties: { - buildProperties: { - skipGithubActionWorkflowGeneration: true - } - } -} - -output id string = staticWebApp.id -output defaultHostName string = staticWebApp.properties.defaultHostname -output url string = 'https://${staticWebApp.properties.defaultHostname}' diff --git a/infra/modules/webApp.bicep b/infra/modules/webApp.bicep deleted file mode 100644 index 9dba988..0000000 --- a/infra/modules/webApp.bicep +++ /dev/null @@ -1,31 +0,0 @@ -@description('Name of the Web App') -param webAppName string - -@description('Azure region for the Web App') -param location string - -@description('Resource ID of the App Service Plan') -param appServicePlanId string - -resource webApp 'Microsoft.Web/sites@2023-01-01' = { - name: webAppName - location: location - identity: { - type: 'SystemAssigned' - } - properties: { - serverFarmId: appServicePlanId - httpsOnly: true - siteConfig: { - netFrameworkVersion: 'v10.0' - http20Enabled: true - minTlsVersion: '1.2' - ftpsState: 'Disabled' - } - } -} - -output id string = webApp.id -output defaultHostName string = webApp.properties.defaultHostName -output url string = 'https://${webApp.properties.defaultHostName}' -output principalId string = webApp.identity.principalId diff --git a/infra/parameters/dev.bicepparam b/infra/parameters/dev.bicepparam deleted file mode 100644 index 8d05950..0000000 --- a/infra/parameters/dev.bicepparam +++ /dev/null @@ -1,23 +0,0 @@ -// dev.bicepparam — parameter values for the dev environment -// SQL password is read from the SQL_ADMIN_PASSWORD environment variable. -// Set it before deploying: $env:SQL_ADMIN_PASSWORD = 'your-password' - -using '../main.bicep' - -// ─── Resource naming (Cloud Adoption Framework convention) ──────────────────── -// Pattern: {type}-{workload}-{qualifier}-{env} - -param appServicePlanName = 'asp-talent-f1-dev' -param apiAppName = 'app-talent-api-dev' -param identityAppName = 'app-talent-ids-dev' -param identityAdminAppName = 'app-talent-admin-dev' -param staticWebAppName = 'swa-talent-ui-dev' -param sqlServerName = 'sql-talent-dev' -param apiDbName = 'sqldb-talent-api-dev' -param identityDbName = 'sqldb-talent-ids-dev' - -param keyVaultName = 'kv-talent-dev' - -// SQL admin credentials -param sqlAdminLogin = 'sqladmin' -param sqlAdminPassword = readEnvironmentVariable('SQL_ADMIN_PASSWORD') diff --git a/infra/scripts/setup-oidc.ps1 b/infra/scripts/setup-oidc.ps1 deleted file mode 100644 index 8644b56..0000000 --- a/infra/scripts/setup-oidc.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -# ============================================================================= -# setup-oidc.ps1 — One-time Azure OIDC setup for GitHub Actions (PowerShell) -# ============================================================================= -# -# Run this script ONCE per environment to wire up passwordless deployment. -# It creates an App Registration, adds a Federated Identity Credential so -# GitHub Actions can authenticate with Azure using short-lived OIDC tokens -# (no stored passwords), and saves the three required values as GitHub secrets. -# -# Prerequisites: -# az login (logged in to Azure CLI) -# az account set --subscription "..." (correct subscription selected) -# gh auth login (logged in to GitHub CLI) -# -# Usage: -# .\infra\scripts\setup-oidc.ps1 -# ============================================================================= - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -# --- Configuration ------------------------------------------------------------ -# Edit these values before running. - -$APP_NAME = "github-actions-talent-dev" # App Registration display name -$RESOURCE_GROUP = "rg-talent-dev" # Resource group to manage -$LOCATION = "eastus" # Azure region -$GITHUB_ORG = "workcontrolgit" # GitHub organisation or username -$GITHUB_REPO = "AngularNetTutorial" # GitHub repository name -$BRANCH = "main" # Branch that triggers deployments -$SQL_ADMIN_PASSWORD = 'Tr@7vK#2mX$9pL4!' # SQL admin password -# ------------------------------------------------------------------------------ - -Write-Host "" -Write-Host "========================================================" -Write-Host " Azure OIDC Setup for GitHub Actions" -Write-Host " App: $APP_NAME" -Write-Host " Repo: $GITHUB_ORG/$GITHUB_REPO (branch: $BRANCH)" -Write-Host " RG: $RESOURCE_GROUP ($LOCATION)" -Write-Host "========================================================" -Write-Host "" - -# --- Step 1: Create App Registration ------------------------------------------ -Write-Host ">>> Step 1: Create App Registration" - -$APP_ID = az ad app create ` - --display-name $APP_NAME ` - --query appId ` - --output tsv - -Write-Host " Created App Registration: $APP_ID" - -# --- Step 2: Create Service Principal ----------------------------------------- -Write-Host ">>> Step 2: Create Service Principal" - -az ad sp create --id $APP_ID --output none - -Write-Host " Service Principal created" - -# --- Step 3: Add Federated Identity Credential -------------------------------- -Write-Host ">>> Step 3: Add Federated Identity Credential" - -$TEMP_CRED_FILE = [System.IO.Path]::GetTempFileName() + ".json" -@{ - name = "github-actions-branch-$BRANCH" - issuer = "https://token.actions.githubusercontent.com" - subject = "repo:${GITHUB_ORG}/${GITHUB_REPO}:ref:refs/heads/$BRANCH" - audiences = @("api://AzureADTokenExchange") - description = "GitHub Actions OIDC for $GITHUB_ORG/$GITHUB_REPO branch $BRANCH" -} | ConvertTo-Json | Set-Content -Path $TEMP_CRED_FILE -Encoding UTF8 - -az ad app federated-credential create ` - --id $APP_ID ` - --parameters "@$TEMP_CRED_FILE" ` - --output none - -Remove-Item $TEMP_CRED_FILE -Force - -Write-Host " Federated credential added (branch: $BRANCH)" - -# --- Step 4a: Create Resource Group ------------------------------------------- -Write-Host ">>> Step 4a: Create Resource Group (idempotent)" - -az group create ` - --name $RESOURCE_GROUP ` - --location $LOCATION ` - --output none - -Write-Host " Resource group ready: $RESOURCE_GROUP" - -# --- Step 4b: Grant Contributor Role on Resource Group ------------------------ -Write-Host ">>> Step 4b: Grant Contributor role on $RESOURCE_GROUP" - -$SUBSCRIPTION_ID = az account show --query id --output tsv -$SCOPE = "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP" - -az role assignment create ` - --assignee $APP_ID ` - --role "Contributor" ` - --scope $SCOPE ` - --output none - -Write-Host " Contributor role granted on: $SCOPE" - -# --- Step 5: Retrieve Tenant ID ----------------------------------------------- -$TENANT_ID = az account show --query tenantId --output tsv - -# --- Step 6: Save Secrets to GitHub Repository -------------------------------- -Write-Host ">>> Step 6: Save secrets to GitHub repository" - -gh secret set AZURE_CLIENT_ID --body $APP_ID --repo "${GITHUB_ORG}/${GITHUB_REPO}" -gh secret set AZURE_TENANT_ID --body $TENANT_ID --repo "${GITHUB_ORG}/${GITHUB_REPO}" -gh secret set AZURE_SUBSCRIPTION_ID --body $SUBSCRIPTION_ID --repo "${GITHUB_ORG}/${GITHUB_REPO}" -gh secret set SQL_ADMIN_PASSWORD --body $SQL_ADMIN_PASSWORD --repo "${GITHUB_ORG}/${GITHUB_REPO}" - -Write-Host " AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, SQL_ADMIN_PASSWORD saved" - -# --- Done --------------------------------------------------------------------- -Write-Host "" -Write-Host "========================================================" -Write-Host " Summary" -Write-Host "========================================================" -Write-Host " AZURE_CLIENT_ID: $APP_ID" -Write-Host " AZURE_TENANT_ID: $TENANT_ID" -Write-Host " AZURE_SUBSCRIPTION_ID: $SUBSCRIPTION_ID" -Write-Host " SQL_ADMIN_PASSWORD: (set)" -Write-Host "" -Write-Host " Verify all 4 secrets at:" -Write-Host " https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/settings/secrets/actions" -Write-Host "" -Write-Host " OIDC setup complete. GitHub Actions can now deploy to Azure" -Write-Host " without any stored passwords or client secrets." -Write-Host "========================================================" diff --git a/infra/scripts/setup-oidc.sh b/infra/scripts/setup-oidc.sh deleted file mode 100644 index 71a47d4..0000000 --- a/infra/scripts/setup-oidc.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# setup-oidc.sh — One-time Azure OIDC setup for GitHub Actions -# ============================================================================= -# -# Run this script ONCE per environment to wire up passwordless deployment. -# It creates an App Registration, adds a Federated Identity Credential so -# GitHub Actions can authenticate with Azure using short-lived OIDC tokens -# (no stored passwords), and saves the three required values as GitHub secrets. -# -# Prerequisites: -# az login (logged in to Azure CLI) -# az account set --subscription "..." (correct subscription selected) -# gh auth login (logged in to GitHub CLI) -# -# Usage: -# chmod +x infra/scripts/setup-oidc.sh -# ./infra/scripts/setup-oidc.sh -# ============================================================================= - -set -euo pipefail - -# ─── Configuration ──────────────────────────────────────────────────────────── -# Edit these values before running. - -APP_NAME="github-actions-talent-dev" # App Registration display name in Azure AD -RESOURCE_GROUP="rg-talent-dev" # Resource group the deployment identity can manage -LOCATION="eastus" # Azure region for the resource group -GITHUB_ORG="workcontrolgit" # GitHub organisation or username -GITHUB_REPO="AngularNetTutorial" # GitHub repository name (no owner prefix) -BRANCH="master" # Branch that triggers deployments -# ────────────────────────────────────────────────────────────────────────────── - -echo "" -echo "========================================================" -echo " Azure OIDC Setup for GitHub Actions" -echo " App: $APP_NAME" -echo " Repo: $GITHUB_ORG/$GITHUB_REPO (branch: $BRANCH)" -echo " RG: $RESOURCE_GROUP ($LOCATION)" -echo "========================================================" -echo "" - -# ─── Step 1: Create App Registration ────────────────────────────────────────── -echo ">>> Step 1: Create App Registration" - -APP_ID=$(az ad app create \ - --display-name "$APP_NAME" \ - --query appId \ - --output tsv) - -echo " Created App Registration: $APP_ID" - -# ─── Step 2: Create Service Principal ───────────────────────────────────────── -echo ">>> Step 2: Create Service Principal" - -az ad sp create --id "$APP_ID" --output none - -echo " Service Principal created" - -# ─── Step 3: Add Federated Identity Credential ──────────────────────────────── -echo ">>> Step 3: Add Federated Identity Credential" -# -# This tells Azure: "trust JWT tokens from GitHub Actions for this exact -# repo and branch — no password needed." - -FEDERATED_CREDENTIAL=$(cat <>> Step 4a: Create Resource Group (idempotent)" - -az group create \ - --name "$RESOURCE_GROUP" \ - --location "$LOCATION" \ - --output none - -echo " Resource group ready: $RESOURCE_GROUP" - -# ─── Step 4b: Grant Contributor Role on Resource Group ──────────────────────── -echo ">>> Step 4b: Grant Contributor role on $RESOURCE_GROUP" -# -# Scoped to the resource group only — not the entire subscription. -# Least-privilege: the deployment identity can only touch rg-talent-dev. - -SUBSCRIPTION_ID=$(az account show --query id --output tsv) -SCOPE="/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}" - -az role assignment create \ - --assignee "$APP_ID" \ - --role "Contributor" \ - --scope "$SCOPE" \ - --output none - -echo " Contributor role granted on: $SCOPE" - -# ─── Step 5: Retrieve Tenant ID ─────────────────────────────────────────────── -TENANT_ID=$(az account show --query tenantId --output tsv) - -# ─── Step 6: Save Secrets to GitHub Repository ──────────────────────────────── -echo ">>> Step 6: Save secrets to GitHub repository" - -gh secret set AZURE_CLIENT_ID --body "$APP_ID" --repo "${GITHUB_ORG}/${GITHUB_REPO}" -gh secret set AZURE_TENANT_ID --body "$TENANT_ID" --repo "${GITHUB_ORG}/${GITHUB_REPO}" -gh secret set AZURE_SUBSCRIPTION_ID --body "$SUBSCRIPTION_ID" --repo "${GITHUB_ORG}/${GITHUB_REPO}" - -echo " AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID saved" - -# ─── Done ───────────────────────────────────────────────────────────────────── -echo "" -echo "========================================================" -echo " Summary" -echo "========================================================" -echo " AZURE_CLIENT_ID: $APP_ID" -echo " AZURE_TENANT_ID: $TENANT_ID" -echo " AZURE_SUBSCRIPTION_ID: $SUBSCRIPTION_ID" -echo "" -echo " One secret still needed — set it manually:" -echo "" -echo " gh secret set SQL_ADMIN_PASSWORD --repo ${GITHUB_ORG}/${GITHUB_REPO}" -echo "" -echo " Then verify all 4 secrets at:" -echo " https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/settings/secrets/actions" -echo "" -echo " OIDC setup complete. GitHub Actions can now deploy to Azure" -echo " without any stored passwords or client secrets." -echo "========================================================" diff --git a/main-GPDDDAI7.js b/main-GPDDDAI7.js new file mode 100644 index 0000000..cb7b453 --- /dev/null +++ b/main-GPDDDAI7.js @@ -0,0 +1,190 @@ +var FU=Object.defineProperty,NU=Object.defineProperties;var LU=Object.getOwnPropertyDescriptors;var I_=Object.getOwnPropertySymbols;var vA=Object.prototype.hasOwnProperty,yA=Object.prototype.propertyIsEnumerable;var bA=(t,n,e)=>n in t?FU(t,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[n]=e,I=(t,n)=>{for(var e in n||={})vA.call(n,e)&&bA(t,e,n[e]);if(I_)for(var e of I_(n))yA.call(n,e)&&bA(t,e,n[e]);return t},Me=(t,n)=>NU(t,LU(n));var xA=t=>typeof t=="symbol"?t:t+"",cd=(t,n)=>{var e={};for(var i in t)vA.call(t,i)&&n.indexOf(i)<0&&(e[i]=t[i]);if(t!=null&&I_)for(var i of I_(t))n.indexOf(i)<0&&yA.call(t,i)&&(e[i]=t[i]);return e};var yn=(t,n,e)=>new Promise((i,r)=>{var o=l=>{try{s(e.next(l))}catch(c){r(c)}},a=l=>{try{s(e.throw(l))}catch(c){r(c)}},s=l=>l.done?i(l.value):Promise.resolve(l.value).then(o,a);s((e=e.apply(t,n)).next())});var Rw;function A_(){return Rw}function as(t){let n=Rw;return Rw=t,n}var CA=Symbol("NotFound");function ju(t){return t===CA||t?.name==="\u0275NotFound"}var vr=null,O_=!1,Pw=1,VU=null,mn=Symbol("SIGNAL");function tt(t){let n=vr;return vr=t,n}function N_(){return vr}var dd={version:0,lastCleanEpoch:0,dirty:!1,producers:void 0,producersTail:void 0,consumers:void 0,consumersTail:void 0,recomputing:!1,consumerAllowSignalWrites:!1,consumerIsAlwaysLive:!1,kind:"unknown",producerMustRecompute:()=>!1,producerRecomputeValue:()=>{},consumerMarkedDirty:()=>{},consumerOnSignalRead:()=>{}};function Wl(t){if(O_)throw new Error("");if(vr===null)return;vr.consumerOnSignalRead(t);let n=vr.producersTail;if(n!==void 0&&n.producer===t)return;let e,i=vr.recomputing;if(i&&(e=n!==void 0?n.nextProducer:vr.producers,e!==void 0&&e.producer===t)){vr.producersTail=e,e.lastReadVersion=t.version;return}let r=t.consumersTail;if(r!==void 0&&r.consumer===vr&&(!i||jU(r,vr)))return;let o=Hu(vr),a={producer:t,consumer:vr,nextProducer:e,prevConsumer:r,lastReadVersion:t.version,nextConsumer:void 0};vr.producersTail=a,n!==void 0?n.nextProducer=a:vr.producers=a,o&&EA(t,a)}function wA(){Pw++}function L_(t){if(!(Hu(t)&&!t.dirty)&&!(!t.dirty&&t.lastCleanEpoch===Pw)){if(!t.producerMustRecompute(t)&&!md(t)){F_(t);return}t.producerRecomputeValue(t),F_(t)}}function Fw(t){if(t.consumers===void 0)return;let n=O_;O_=!0;try{for(let e=t.consumers;e!==void 0;e=e.nextConsumer){let i=e.consumer;i.dirty||BU(i)}}finally{O_=n}}function Nw(){return vr?.consumerAllowSignalWrites!==!1}function BU(t){t.dirty=!0,Fw(t),t.consumerMarkedDirty?.(t)}function F_(t){t.dirty=!1,t.lastCleanEpoch=Pw}function Gl(t){return t&&DA(t),tt(t)}function DA(t){t.producersTail=void 0,t.recomputing=!0}function ud(t,n){tt(n),t&&MA(t)}function MA(t){t.recomputing=!1;let n=t.producersTail,e=n!==void 0?n.nextProducer:t.producers;if(e!==void 0){if(Hu(t))do e=Lw(e);while(e!==void 0);n!==void 0?n.nextProducer=void 0:t.producers=void 0}}function md(t){for(let n=t.producers;n!==void 0;n=n.nextProducer){let e=n.producer,i=n.lastReadVersion;if(i!==e.version||(L_(e),i!==e.version))return!0}return!1}function ql(t){if(Hu(t)){let n=t.producers;for(;n!==void 0;)n=Lw(n)}t.producers=void 0,t.producersTail=void 0,t.consumers=void 0,t.consumersTail=void 0}function EA(t,n){let e=t.consumersTail,i=Hu(t);if(e!==void 0?(n.nextConsumer=e.nextConsumer,e.nextConsumer=n):(n.nextConsumer=void 0,t.consumers=n),n.prevConsumer=e,t.consumersTail=n,!i)for(let r=t.producers;r!==void 0;r=r.nextProducer)EA(r.producer,r)}function Lw(t){let n=t.producer,e=t.nextProducer,i=t.nextConsumer,r=t.prevConsumer;if(t.nextConsumer=void 0,t.prevConsumer=void 0,i!==void 0?i.prevConsumer=r:n.consumersTail=r,r!==void 0)r.nextConsumer=i;else if(n.consumers=i,!Hu(n)){let o=n.producers;for(;o!==void 0;)o=Lw(o)}return e}function Hu(t){return t.consumerIsAlwaysLive||t.consumers!==void 0}function V_(t){VU?.(t)}function jU(t,n){let e=n.producersTail;if(e!==void 0){let i=n.producers;do{if(i===t)return!0;if(i===e)break;i=i.nextProducer}while(i!==void 0)}return!1}function B_(t,n){return Object.is(t,n)}function yp(t,n){let e=Object.create(HU);e.computation=t,n!==void 0&&(e.equal=n);let i=()=>{if(L_(e),Wl(e),e.value===vp)throw e.error;return e.value};return i[mn]=e,V_(e),i}var R_=Symbol("UNSET"),P_=Symbol("COMPUTING"),vp=Symbol("ERRORED"),HU=Me(I({},dd),{value:R_,dirty:!0,error:null,equal:B_,kind:"computed",producerMustRecompute(t){return t.value===R_||t.value===P_},producerRecomputeValue(t){if(t.value===P_)throw new Error("");let n=t.value;t.value=P_;let e=Gl(t),i,r=!1;try{i=t.computation(),tt(null),r=n!==R_&&n!==vp&&i!==vp&&t.equal(n,i)}catch(o){i=vp,t.error=o}finally{ud(t,e)}if(r){t.value=n;return}t.value=i,t.version++}});function zU(){throw new Error}var SA=zU;function kA(t){SA(t)}function Vw(t){SA=t}var UU=null;function Bw(t,n){let e=Object.create(xp);e.value=t,n!==void 0&&(e.equal=n);let i=()=>TA(e);return i[mn]=e,V_(e),[i,a=>hd(e,a),a=>jw(e,a)]}function TA(t){return Wl(t),t.value}function hd(t,n){Nw()||kA(t),t.equal(t.value,n)||(t.value=n,$U(t))}function jw(t,n){Nw()||kA(t),hd(t,n(t.value))}var xp=Me(I({},dd),{equal:B_,value:void 0,kind:"signal"});function $U(t){t.version++,wA(),Fw(t),UU?.(t)}function Xe(t){return typeof t=="function"}function Yl(t){let e=t(i=>{Error.call(i),i.stack=new Error().stack});return e.prototype=Object.create(Error.prototype),e.prototype.constructor=e,e}var j_=Yl(t=>function(e){t(this),this.message=e?`${e.length} errors occurred during unsubscription: +${e.map((i,r)=>`${r+1}) ${i.toString()}`).join(` + `)}`:"",this.name="UnsubscriptionError",this.errors=e});function pd(t,n){if(t){let e=t.indexOf(n);0<=e&&t.splice(e,1)}}var ke=class t{constructor(n){this.initialTeardown=n,this.closed=!1,this._parentage=null,this._finalizers=null}unsubscribe(){let n;if(!this.closed){this.closed=!0;let{_parentage:e}=this;if(e)if(this._parentage=null,Array.isArray(e))for(let o of e)o.remove(this);else e.remove(this);let{initialTeardown:i}=this;if(Xe(i))try{i()}catch(o){n=o instanceof j_?o.errors:[o]}let{_finalizers:r}=this;if(r){this._finalizers=null;for(let o of r)try{IA(o)}catch(a){n=n??[],a instanceof j_?n=[...n,...a.errors]:n.push(a)}}if(n)throw new j_(n)}}add(n){var e;if(n&&n!==this)if(this.closed)IA(n);else{if(n instanceof t){if(n.closed||n._hasParent(this))return;n._addParent(this)}(this._finalizers=(e=this._finalizers)!==null&&e!==void 0?e:[]).push(n)}}_hasParent(n){let{_parentage:e}=this;return e===n||Array.isArray(e)&&e.includes(n)}_addParent(n){let{_parentage:e}=this;this._parentage=Array.isArray(e)?(e.push(n),e):e?[e,n]:n}_removeParent(n){let{_parentage:e}=this;e===n?this._parentage=null:Array.isArray(e)&&pd(e,n)}remove(n){let{_finalizers:e}=this;e&&pd(e,n),n instanceof t&&n._removeParent(this)}};ke.EMPTY=(()=>{let t=new ke;return t.closed=!0,t})();var Hw=ke.EMPTY;function H_(t){return t instanceof ke||t&&"closed"in t&&Xe(t.remove)&&Xe(t.add)&&Xe(t.unsubscribe)}function IA(t){Xe(t)?t():t.unsubscribe()}var Aa={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var zu={setTimeout(t,n,...e){let{delegate:i}=zu;return i?.setTimeout?i.setTimeout(t,n,...e):setTimeout(t,n,...e)},clearTimeout(t){let{delegate:n}=zu;return(n?.clearTimeout||clearTimeout)(t)},delegate:void 0};function z_(t){zu.setTimeout(()=>{let{onUnhandledError:n}=Aa;if(n)n(t);else throw t})}function fd(){}var AA=zw("C",void 0,void 0);function OA(t){return zw("E",void 0,t)}function RA(t){return zw("N",t,void 0)}function zw(t,n,e){return{kind:t,value:n,error:e}}var gd=null;function Uu(t){if(Aa.useDeprecatedSynchronousErrorHandling){let n=!gd;if(n&&(gd={errorThrown:!1,error:null}),t(),n){let{errorThrown:e,error:i}=gd;if(gd=null,e)throw i}}else t()}function PA(t){Aa.useDeprecatedSynchronousErrorHandling&&gd&&(gd.errorThrown=!0,gd.error=t)}var _d=class extends ke{constructor(n){super(),this.isStopped=!1,n?(this.destination=n,H_(n)&&n.add(this)):this.destination=qU}static create(n,e,i){return new il(n,e,i)}next(n){this.isStopped?$w(RA(n),this):this._next(n)}error(n){this.isStopped?$w(OA(n),this):(this.isStopped=!0,this._error(n))}complete(){this.isStopped?$w(AA,this):(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe(),this.destination=null)}_next(n){this.destination.next(n)}_error(n){try{this.destination.error(n)}finally{this.unsubscribe()}}_complete(){try{this.destination.complete()}finally{this.unsubscribe()}}},WU=Function.prototype.bind;function Uw(t,n){return WU.call(t,n)}var Ww=class{constructor(n){this.partialObserver=n}next(n){let{partialObserver:e}=this;if(e.next)try{e.next(n)}catch(i){U_(i)}}error(n){let{partialObserver:e}=this;if(e.error)try{e.error(n)}catch(i){U_(i)}else U_(n)}complete(){let{partialObserver:n}=this;if(n.complete)try{n.complete()}catch(e){U_(e)}}},il=class extends _d{constructor(n,e,i){super();let r;if(Xe(n)||!n)r={next:n??void 0,error:e??void 0,complete:i??void 0};else{let o;this&&Aa.useDeprecatedNextContext?(o=Object.create(n),o.unsubscribe=()=>this.unsubscribe(),r={next:n.next&&Uw(n.next,o),error:n.error&&Uw(n.error,o),complete:n.complete&&Uw(n.complete,o)}):r=n}this.destination=new Ww(r)}};function U_(t){Aa.useDeprecatedSynchronousErrorHandling?PA(t):z_(t)}function GU(t){throw t}function $w(t,n){let{onStoppedNotification:e}=Aa;e&&zu.setTimeout(()=>e(t,n))}var qU={closed:!0,next:fd,error:GU,complete:fd};var $u=typeof Symbol=="function"&&Symbol.observable||"@@observable";function Lr(t){return t}function Gw(...t){return qw(t)}function qw(t){return t.length===0?Lr:t.length===1?t[0]:function(e){return t.reduce((i,r)=>r(i),e)}}var Ne=(()=>{class t{constructor(e){e&&(this._subscribe=e)}lift(e){let i=new t;return i.source=this,i.operator=e,i}subscribe(e,i,r){let o=QU(e)?e:new il(e,i,r);return Uu(()=>{let{operator:a,source:s}=this;o.add(a?a.call(o,s):s?this._subscribe(o):this._trySubscribe(o))}),o}_trySubscribe(e){try{return this._subscribe(e)}catch(i){e.error(i)}}forEach(e,i){return i=FA(i),new i((r,o)=>{let a=new il({next:s=>{try{e(s)}catch(l){o(l),a.unsubscribe()}},error:o,complete:r});this.subscribe(a)})}_subscribe(e){var i;return(i=this.source)===null||i===void 0?void 0:i.subscribe(e)}[$u](){return this}pipe(...e){return qw(e)(this)}toPromise(e){return e=FA(e),new e((i,r)=>{let o;this.subscribe(a=>o=a,a=>r(a),()=>i(o))})}}return t.create=n=>new t(n),t})();function FA(t){var n;return(n=t??Aa.Promise)!==null&&n!==void 0?n:Promise}function YU(t){return t&&Xe(t.next)&&Xe(t.error)&&Xe(t.complete)}function QU(t){return t&&t instanceof _d||YU(t)&&H_(t)}function Yw(t){return Xe(t?.lift)}function Ke(t){return n=>{if(Yw(n))return n.lift(function(e){try{return t(e,this)}catch(i){this.error(i)}});throw new TypeError("Unable to lift unknown Observable type")}}function $e(t,n,e,i,r){return new Qw(t,n,e,i,r)}var Qw=class extends _d{constructor(n,e,i,r,o,a){super(n),this.onFinalize=o,this.shouldUnsubscribe=a,this._next=e?function(s){try{e(s)}catch(l){n.error(l)}}:super._next,this._error=r?function(s){try{r(s)}catch(l){n.error(l)}finally{this.unsubscribe()}}:super._error,this._complete=i?function(){try{i()}catch(s){n.error(s)}finally{this.unsubscribe()}}:super._complete}unsubscribe(){var n;if(!this.shouldUnsubscribe||this.shouldUnsubscribe()){let{closed:e}=this;super.unsubscribe(),!e&&((n=this.onFinalize)===null||n===void 0||n.call(this))}}};function Wu(){return Ke((t,n)=>{let e=null;t._refCount++;let i=$e(n,void 0,void 0,void 0,()=>{if(!t||t._refCount<=0||0<--t._refCount){e=null;return}let r=t._connection,o=e;e=null,r&&(!o||r===o)&&r.unsubscribe(),n.unsubscribe()});t.subscribe(i),i.closed||(e=t.connect())})}var Ql=class extends Ne{constructor(n,e){super(),this.source=n,this.subjectFactory=e,this._subject=null,this._refCount=0,this._connection=null,Yw(n)&&(this.lift=n.lift)}_subscribe(n){return this.getSubject().subscribe(n)}getSubject(){let n=this._subject;return(!n||n.isStopped)&&(this._subject=this.subjectFactory()),this._subject}_teardown(){this._refCount=0;let{_connection:n}=this;this._subject=this._connection=null,n?.unsubscribe()}connect(){let n=this._connection;if(!n){n=this._connection=new ke;let e=this.getSubject();n.add(this.source.subscribe($e(e,void 0,()=>{this._teardown(),e.complete()},i=>{this._teardown(),e.error(i)},()=>this._teardown()))),n.closed&&(this._connection=null,n=ke.EMPTY)}return n}refCount(){return Wu()(this)}};var Gu={schedule(t){let n=requestAnimationFrame,e=cancelAnimationFrame,{delegate:i}=Gu;i&&(n=i.requestAnimationFrame,e=i.cancelAnimationFrame);let r=n(o=>{e=void 0,t(o)});return new ke(()=>e?.(r))},requestAnimationFrame(...t){let{delegate:n}=Gu;return(n?.requestAnimationFrame||requestAnimationFrame)(...t)},cancelAnimationFrame(...t){let{delegate:n}=Gu;return(n?.cancelAnimationFrame||cancelAnimationFrame)(...t)},delegate:void 0};var NA=Yl(t=>function(){t(this),this.name="ObjectUnsubscribedError",this.message="object unsubscribed"});var z=(()=>{class t extends Ne{constructor(){super(),this.closed=!1,this.currentObservers=null,this.observers=[],this.isStopped=!1,this.hasError=!1,this.thrownError=null}lift(e){let i=new $_(this,this);return i.operator=e,i}_throwIfClosed(){if(this.closed)throw new NA}next(e){Uu(()=>{if(this._throwIfClosed(),!this.isStopped){this.currentObservers||(this.currentObservers=Array.from(this.observers));for(let i of this.currentObservers)i.next(e)}})}error(e){Uu(()=>{if(this._throwIfClosed(),!this.isStopped){this.hasError=this.isStopped=!0,this.thrownError=e;let{observers:i}=this;for(;i.length;)i.shift().error(e)}})}complete(){Uu(()=>{if(this._throwIfClosed(),!this.isStopped){this.isStopped=!0;let{observers:e}=this;for(;e.length;)e.shift().complete()}})}unsubscribe(){this.isStopped=this.closed=!0,this.observers=this.currentObservers=null}get observed(){var e;return((e=this.observers)===null||e===void 0?void 0:e.length)>0}_trySubscribe(e){return this._throwIfClosed(),super._trySubscribe(e)}_subscribe(e){return this._throwIfClosed(),this._checkFinalizedStatuses(e),this._innerSubscribe(e)}_innerSubscribe(e){let{hasError:i,isStopped:r,observers:o}=this;return i||r?Hw:(this.currentObservers=null,o.push(e),new ke(()=>{this.currentObservers=null,pd(o,e)}))}_checkFinalizedStatuses(e){let{hasError:i,thrownError:r,isStopped:o}=this;i?e.error(r):o&&e.complete()}asObservable(){let e=new Ne;return e.source=this,e}}return t.create=(n,e)=>new $_(n,e),t})(),$_=class extends z{constructor(n,e){super(),this.destination=n,this.source=e}next(n){var e,i;(i=(e=this.destination)===null||e===void 0?void 0:e.next)===null||i===void 0||i.call(e,n)}error(n){var e,i;(i=(e=this.destination)===null||e===void 0?void 0:e.error)===null||i===void 0||i.call(e,n)}complete(){var n,e;(e=(n=this.destination)===null||n===void 0?void 0:n.complete)===null||e===void 0||e.call(n)}_subscribe(n){var e,i;return(i=(e=this.source)===null||e===void 0?void 0:e.subscribe(n))!==null&&i!==void 0?i:Hw}};var rt=class extends z{constructor(n){super(),this._value=n}get value(){return this.getValue()}_subscribe(n){let e=super._subscribe(n);return!e.closed&&n.next(this._value),e}getValue(){let{hasError:n,thrownError:e,_value:i}=this;if(n)throw e;return this._throwIfClosed(),i}next(n){super.next(this._value=n)}};var Cp={now(){return(Cp.delegate||Date).now()},delegate:void 0};var ss=class extends z{constructor(n=1/0,e=1/0,i=Cp){super(),this._bufferSize=n,this._windowTime=e,this._timestampProvider=i,this._buffer=[],this._infiniteTimeWindow=!0,this._infiniteTimeWindow=e===1/0,this._bufferSize=Math.max(1,n),this._windowTime=Math.max(1,e)}next(n){let{isStopped:e,_buffer:i,_infiniteTimeWindow:r,_timestampProvider:o,_windowTime:a}=this;e||(i.push(n),!r&&i.push(o.now()+a)),this._trimBuffer(),super.next(n)}_subscribe(n){this._throwIfClosed(),this._trimBuffer();let e=this._innerSubscribe(n),{_infiniteTimeWindow:i,_buffer:r}=this,o=r.slice();for(let a=0;aLA(n)&&t()),n},clearImmediate(t){LA(t)}};var{setImmediate:ZU,clearImmediate:XU}=VA,Dp={setImmediate(...t){let{delegate:n}=Dp;return(n?.setImmediate||ZU)(...t)},clearImmediate(t){let{delegate:n}=Dp;return(n?.clearImmediate||XU)(t)},delegate:void 0};var G_=class extends Kl{constructor(n,e){super(n,e),this.scheduler=n,this.work=e}requestAsyncId(n,e,i=0){return i!==null&&i>0?super.requestAsyncId(n,e,i):(n.actions.push(this),n._scheduled||(n._scheduled=Dp.setImmediate(n.flush.bind(n,void 0))))}recycleAsyncId(n,e,i=0){var r;if(i!=null?i>0:this.delay>0)return super.recycleAsyncId(n,e,i);let{actions:o}=n;e!=null&&((r=o[o.length-1])===null||r===void 0?void 0:r.id)!==e&&(Dp.clearImmediate(e),n._scheduled===e&&(n._scheduled=void 0))}};var qu=class t{constructor(n,e=t.now){this.schedulerActionCtor=n,this.now=e}schedule(n,e=0,i){return new this.schedulerActionCtor(this,n).schedule(i,e)}};qu.now=Cp.now;var Zl=class extends qu{constructor(n,e=qu.now){super(n,e),this.actions=[],this._active=!1}flush(n){let{actions:e}=this;if(this._active){e.push(n);return}let i;this._active=!0;do if(i=n.execute(n.state,n.delay))break;while(n=e.shift());if(this._active=!1,i){for(;n=e.shift();)n.unsubscribe();throw i}}};var q_=class extends Zl{flush(n){this._active=!0;let e=this._scheduled;this._scheduled=void 0;let{actions:i}=this,r;n=n||i.shift();do if(r=n.execute(n.state,n.delay))break;while((n=i[0])&&n.id===e&&i.shift());if(this._active=!1,r){for(;(n=i[0])&&n.id===e&&i.shift();)n.unsubscribe();throw r}}};var Y_=new q_(G_);var nl=new Zl(Kl),BA=nl;var Q_=class extends Kl{constructor(n,e){super(n,e),this.scheduler=n,this.work=e}requestAsyncId(n,e,i=0){return i!==null&&i>0?super.requestAsyncId(n,e,i):(n.actions.push(this),n._scheduled||(n._scheduled=Gu.requestAnimationFrame(()=>n.flush(void 0))))}recycleAsyncId(n,e,i=0){var r;if(i!=null?i>0:this.delay>0)return super.recycleAsyncId(n,e,i);let{actions:o}=n;e!=null&&e===n._scheduled&&((r=o[o.length-1])===null||r===void 0?void 0:r.id)!==e&&(Gu.cancelAnimationFrame(e),n._scheduled=void 0)}};var K_=class extends Zl{flush(n){this._active=!0;let e;n?e=n.id:(e=this._scheduled,this._scheduled=void 0);let{actions:i}=this,r;n=n||i.shift();do if(r=n.execute(n.state,n.delay))break;while((n=i[0])&&n.id===e&&i.shift());if(this._active=!1,r){for(;(n=i[0])&&n.id===e&&i.shift();)n.unsubscribe();throw r}}};var Z_=new K_(Q_);var zi=new Ne(t=>t.complete());function X_(t){return t&&Xe(t.schedule)}function Xw(t){return t[t.length-1]}function J_(t){return Xe(Xw(t))?t.pop():void 0}function ls(t){return X_(Xw(t))?t.pop():void 0}function jA(t,n){return typeof Xw(t)=="number"?t.pop():n}function zA(t,n,e,i){function r(o){return o instanceof e?o:new e(function(a){a(o)})}return new(e||(e=Promise))(function(o,a){function s(d){try{c(i.next(d))}catch(p){a(p)}}function l(d){try{c(i.throw(d))}catch(p){a(p)}}function c(d){d.done?o(d.value):r(d.value).then(s,l)}c((i=i.apply(t,n||[])).next())})}function HA(t){var n=typeof Symbol=="function"&&Symbol.iterator,e=n&&t[n],i=0;if(e)return e.call(t);if(t&&typeof t.length=="number")return{next:function(){return t&&i>=t.length&&(t=void 0),{value:t&&t[i++],done:!t}}};throw new TypeError(n?"Object is not iterable.":"Symbol.iterator is not defined.")}function bd(t){return this instanceof bd?(this.v=t,this):new bd(t)}function UA(t,n,e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var i=e.apply(t,n||[]),r,o=[];return r=Object.create((typeof AsyncIterator=="function"?AsyncIterator:Object).prototype),s("next"),s("throw"),s("return",a),r[Symbol.asyncIterator]=function(){return this},r;function a(b){return function(y){return Promise.resolve(y).then(b,p)}}function s(b,y){i[b]&&(r[b]=function(w){return new Promise(function(C,D){o.push([b,w,C,D])>1||l(b,w)})},y&&(r[b]=y(r[b])))}function l(b,y){try{c(i[b](y))}catch(w){_(o[0][3],w)}}function c(b){b.value instanceof bd?Promise.resolve(b.value.v).then(d,p):_(o[0][2],b)}function d(b){l("next",b)}function p(b){l("throw",b)}function _(b,y){b(y),o.shift(),o.length&&l(o[0][0],o[0][1])}}function $A(t){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var n=t[Symbol.asyncIterator],e;return n?n.call(t):(t=typeof HA=="function"?HA(t):t[Symbol.iterator](),e={},i("next"),i("throw"),i("return"),e[Symbol.asyncIterator]=function(){return this},e);function i(o){e[o]=t[o]&&function(a){return new Promise(function(s,l){a=t[o](a),r(s,l,a.done,a.value)})}}function r(o,a,s,l){Promise.resolve(l).then(function(c){o({value:c,done:s})},a)}}var Yu=t=>t&&typeof t.length=="number"&&typeof t!="function";function eb(t){return Xe(t?.then)}function tb(t){return Xe(t[$u])}function ib(t){return Symbol.asyncIterator&&Xe(t?.[Symbol.asyncIterator])}function nb(t){return new TypeError(`You provided ${t!==null&&typeof t=="object"?"an invalid object":`'${t}'`} where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`)}function JU(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var rb=JU();function ob(t){return Xe(t?.[rb])}function ab(t){return UA(this,arguments,function*(){let e=t.getReader();try{for(;;){let{value:i,done:r}=yield bd(e.read());if(r)return yield bd(void 0);yield yield bd(i)}}finally{e.releaseLock()}})}function sb(t){return Xe(t?.getReader)}function zt(t){if(t instanceof Ne)return t;if(t!=null){if(tb(t))return e$(t);if(Yu(t))return t$(t);if(eb(t))return i$(t);if(ib(t))return WA(t);if(ob(t))return n$(t);if(sb(t))return r$(t)}throw nb(t)}function e$(t){return new Ne(n=>{let e=t[$u]();if(Xe(e.subscribe))return e.subscribe(n);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function t$(t){return new Ne(n=>{for(let e=0;e{t.then(e=>{n.closed||(n.next(e),n.complete())},e=>n.error(e)).then(null,z_)})}function n$(t){return new Ne(n=>{for(let e of t)if(n.next(e),n.closed)return;n.complete()})}function WA(t){return new Ne(n=>{o$(t,n).catch(e=>n.error(e))})}function r$(t){return WA(ab(t))}function o$(t,n){var e,i,r,o;return zA(this,void 0,void 0,function*(){try{for(e=$A(t);i=yield e.next(),!i.done;){let a=i.value;if(n.next(a),n.closed)return}}catch(a){r={error:a}}finally{try{i&&!i.done&&(o=e.return)&&(yield o.call(e))}finally{if(r)throw r.error}}n.complete()})}function yr(t,n,e,i=0,r=!1){let o=n.schedule(function(){e(),r?t.add(this.schedule(null,i)):this.unsubscribe()},i);if(t.add(o),!r)return o}function lb(t,n=0){return Ke((e,i)=>{e.subscribe($e(i,r=>yr(i,t,()=>i.next(r),n),()=>yr(i,t,()=>i.complete(),n),r=>yr(i,t,()=>i.error(r),n)))})}function cb(t,n=0){return Ke((e,i)=>{i.add(t.schedule(()=>e.subscribe(i),n))})}function GA(t,n){return zt(t).pipe(cb(n),lb(n))}function qA(t,n){return zt(t).pipe(cb(n),lb(n))}function YA(t,n){return new Ne(e=>{let i=0;return n.schedule(function(){i===t.length?e.complete():(e.next(t[i++]),e.closed||this.schedule())})})}function QA(t,n){return new Ne(e=>{let i;return yr(e,n,()=>{i=t[rb](),yr(e,n,()=>{let r,o;try{({value:r,done:o}=i.next())}catch(a){e.error(a);return}o?e.complete():e.next(r)},0,!0)}),()=>Xe(i?.return)&&i.return()})}function db(t,n){if(!t)throw new Error("Iterable cannot be null");return new Ne(e=>{yr(e,n,()=>{let i=t[Symbol.asyncIterator]();yr(e,n,()=>{i.next().then(r=>{r.done?e.complete():e.next(r.value)})},0,!0)})})}function KA(t,n){return db(ab(t),n)}function ZA(t,n){if(t!=null){if(tb(t))return GA(t,n);if(Yu(t))return YA(t,n);if(eb(t))return qA(t,n);if(ib(t))return db(t,n);if(ob(t))return QA(t,n);if(sb(t))return KA(t,n)}throw nb(t)}function Ut(t,n){return n?ZA(t,n):zt(t)}function Q(...t){let n=ls(t);return Ut(t,n)}function er(t,n){let e=Xe(t)?t:()=>t,i=r=>r.error(e());return new Ne(n?r=>n.schedule(i,0,r):i)}function Gi(t){return!!t&&(t instanceof Ne||Xe(t.lift)&&Xe(t.subscribe))}var rl=Yl(t=>function(){t(this),this.name="EmptyError",this.message="no elements in sequence"});function ub(t){return t instanceof Date&&!isNaN(t)}var a$=Yl(t=>function(e=null){t(this),this.message="Timeout has occurred",this.name="TimeoutError",this.info=e});function Jw(t,n){let{first:e,each:i,with:r=s$,scheduler:o=n??nl,meta:a=null}=ub(t)?{first:t}:typeof t=="number"?{each:t}:t;if(e==null&&i==null)throw new TypeError("No timeout provided.");return Ke((s,l)=>{let c,d,p=null,_=0,b=y=>{d=yr(l,o,()=>{try{c.unsubscribe(),zt(r({meta:a,lastValue:p,seen:_})).subscribe(l)}catch(w){l.error(w)}},y)};c=s.subscribe($e(l,y=>{d?.unsubscribe(),_++,l.next(p=y),i>0&&b(i)},void 0,void 0,()=>{d?.closed||d?.unsubscribe(),p=null})),!_&&b(e!=null?typeof e=="number"?e:+e-o.now():i)})}function s$(t){throw new a$(t)}function se(t,n){return Ke((e,i)=>{let r=0;e.subscribe($e(i,o=>{i.next(t.call(n,o,r++))}))})}var{isArray:l$}=Array;function c$(t,n){return l$(n)?t(...n):t(n)}function Qu(t){return se(n=>c$(t,n))}var{isArray:d$}=Array,{getPrototypeOf:u$,prototype:m$,keys:h$}=Object;function mb(t){if(t.length===1){let n=t[0];if(d$(n))return{args:n,keys:null};if(p$(n)){let e=h$(n);return{args:e.map(i=>n[i]),keys:e}}}return{args:t,keys:null}}function p$(t){return t&&typeof t=="object"&&u$(t)===m$}function hb(t,n){return t.reduce((e,i,r)=>(e[i]=n[r],e),{})}function yo(...t){let n=ls(t),e=J_(t),{args:i,keys:r}=mb(t);if(i.length===0)return Ut([],n);let o=new Ne(f$(i,n,r?a=>hb(r,a):Lr));return e?o.pipe(Qu(e)):o}function f$(t,n,e=Lr){return i=>{XA(n,()=>{let{length:r}=t,o=new Array(r),a=r,s=r;for(let l=0;l{let c=Ut(t[l],n),d=!1;c.subscribe($e(i,p=>{o[l]=p,d||(d=!0,s--),s||i.next(e(o.slice()))},()=>{--a||i.complete()}))},i)},i)}}function XA(t,n,e){t?yr(e,t,n):n()}function JA(t,n,e,i,r,o,a,s){let l=[],c=0,d=0,p=!1,_=()=>{p&&!l.length&&!c&&n.complete()},b=w=>c{o&&n.next(w),c++;let C=!1;zt(e(w,d++)).subscribe($e(n,D=>{r?.(D),o?b(D):n.next(D)},()=>{C=!0},void 0,()=>{if(C)try{for(c--;l.length&&cy(D)):y(D)}_()}catch(D){n.error(D)}}))};return t.subscribe($e(n,b,()=>{p=!0,_()})),()=>{s?.()}}function Vt(t,n,e=1/0){return Xe(n)?Vt((i,r)=>se((o,a)=>n(i,o,r,a))(zt(t(i,r))),e):(typeof n=="number"&&(e=n),Ke((i,r)=>JA(i,r,t,e)))}function xo(t=1/0){return Vt(Lr,t)}function eO(){return xo(1)}function Co(...t){return eO()(Ut(t,ls(t)))}function Fn(t){return new Ne(n=>{zt(t()).subscribe(n)})}function cs(...t){let n=J_(t),{args:e,keys:i}=mb(t),r=new Ne(o=>{let{length:a}=e;if(!a){o.complete();return}let s=new Array(a),l=a,c=a;for(let d=0;d{p||(p=!0,c--),s[d]=_},()=>l--,void 0,()=>{(!l||!p)&&(c||o.next(i?hb(i,s):s),o.complete())}))}});return n?r.pipe(Qu(n)):r}var g$=["addListener","removeListener"],_$=["addEventListener","removeEventListener"],b$=["on","off"];function ol(t,n,e,i){if(Xe(e)&&(i=e,e=void 0),i)return ol(t,n,e).pipe(Qu(i));let[r,o]=x$(t)?_$.map(a=>s=>t[a](n,s,e)):v$(t)?g$.map(tO(t,n)):y$(t)?b$.map(tO(t,n)):[];if(!r&&Yu(t))return Vt(a=>ol(a,n,e))(zt(t));if(!r)throw new TypeError("Invalid event target");return new Ne(a=>{let s=(...l)=>a.next(1o(s)})}function tO(t,n){return e=>i=>t[e](n,i)}function v$(t){return Xe(t.addListener)&&Xe(t.removeListener)}function y$(t){return Xe(t.on)&&Xe(t.off)}function x$(t){return Xe(t.addEventListener)&&Xe(t.removeEventListener)}function ds(t=0,n,e=BA){let i=-1;return n!=null&&(X_(n)?e=n:i=n),new Ne(r=>{let o=ub(t)?+t-e.now():t;o<0&&(o=0);let a=0;return e.schedule(function(){r.closed||(r.next(a++),0<=i?this.schedule(void 0,i):r.complete())},o)})}function it(...t){let n=ls(t),e=jA(t,1/0),i=t;return i.length?i.length===1?zt(i[0]):xo(e)(Ut(i,n)):zi}var{isArray:C$}=Array;function iO(t){return t.length===1&&C$(t[0])?t[0]:t}function ce(t,n){return Ke((e,i)=>{let r=0;e.subscribe($e(i,o=>t.call(n,o,r++)&&i.next(o)))})}function Ku(...t){return t=iO(t),t.length===1?zt(t[0]):new Ne(w$(t))}function w$(t){return n=>{let e=[];for(let i=0;e&&!n.closed&&i{if(e){for(let o=0;o{let i=!1,r=null,o=null,a=!1,s=()=>{if(o?.unsubscribe(),o=null,i){i=!1;let c=r;r=null,e.next(c)}a&&e.complete()},l=()=>{o=null,a&&e.complete()};n.subscribe($e(e,c=>{i=!0,r=c,o||zt(t(c)).subscribe(o=$e(e,s,l))},()=>{a=!0,(!i||!o||o.closed)&&e.complete()}))})}function Xl(t,n=nl){return nO(()=>ds(t,n))}function ei(t){return Ke((n,e)=>{let i=null,r=!1,o;i=n.subscribe($e(e,void 0,void 0,a=>{o=zt(t(a,ei(t)(n))),i?(i.unsubscribe(),i=null,o.subscribe(e)):r=!0})),r&&(i.unsubscribe(),i=null,o.subscribe(e))})}function rO(t,n,e,i,r){return(o,a)=>{let s=e,l=n,c=0;o.subscribe($e(a,d=>{let p=c++;l=s?t(l,d,p):(s=!0,d),i&&a.next(l)},r&&(()=>{s&&a.next(l),a.complete()})))}}function jo(t,n){return Xe(n)?Vt(t,n,1):Vt(t,1)}function Dt(t,n=nl){return Ke((e,i)=>{let r=null,o=null,a=null,s=()=>{if(r){r.unsubscribe(),r=null;let c=o;o=null,i.next(c)}};function l(){let c=a+t,d=n.now();if(d{o=c,a=n.now(),r||(r=n.schedule(l,t),i.add(r))},()=>{s(),i.complete()},void 0,()=>{o=r=null}))})}function Jl(t){return Ke((n,e)=>{let i=!1;n.subscribe($e(e,r=>{i=!0,e.next(r)},()=>{i||e.next(t),e.complete()}))})}function mt(t){return t<=0?()=>zi:Ke((n,e)=>{let i=0;n.subscribe($e(e,r=>{++i<=t&&(e.next(r),t<=i&&e.complete())}))})}function oO(){return Ke((t,n)=>{t.subscribe($e(n,fd))})}function Zu(t){return se(()=>t)}function eD(t,n){return n?e=>Co(n.pipe(mt(1),oO()),e.pipe(eD(t))):Vt((e,i)=>zt(t(e,i)).pipe(mt(1),Zu(e)))}function Oa(t,n=nl){let e=ds(t,n);return eD(()=>e)}function Nn(t,n=Lr){return t=t??D$,Ke((e,i)=>{let r,o=!0;e.subscribe($e(i,a=>{let s=n(a);(o||!t(r,s))&&(o=!1,r=s,i.next(a))}))})}function D$(t,n){return t===n}function pb(t=M$){return Ke((n,e)=>{let i=!1;n.subscribe($e(e,r=>{i=!0,e.next(r)},()=>i?e.complete():e.error(t())))})}function M$(){return new rl}function tD(t,n){return Ke((e,i)=>{let r=0;e.subscribe($e(i,o=>{t.call(n,o,r++,e)||(i.next(!1),i.complete())},()=>{i.next(!0),i.complete()}))})}function Xr(t){return Ke((n,e)=>{try{n.subscribe(e)}finally{e.add(t)}})}function xn(t,n){let e=arguments.length>=2;return i=>i.pipe(t?ce((r,o)=>t(r,o,i)):Lr,mt(1),e?Jl(n):pb(()=>new rl))}function Xu(t){return t<=0?()=>zi:Ke((n,e)=>{let i=[];n.subscribe($e(e,r=>{i.push(r),t{for(let r of i)e.next(r);e.complete()},void 0,()=>{i=null}))})}function iD(t,n){let e=arguments.length>=2;return i=>i.pipe(t?ce((r,o)=>t(r,o,i)):Lr,Xu(1),e?Jl(n):pb(()=>new rl))}function fb(){return Ke((t,n)=>{let e,i=!1;t.subscribe($e(n,r=>{let o=e;e=r,i&&n.next([o,r]),i=!0}))})}function nD(t,n){return Ke(rO(t,n,arguments.length>=2,!0))}function ec(t={}){let{connector:n=()=>new z,resetOnError:e=!0,resetOnComplete:i=!0,resetOnRefCountZero:r=!0}=t;return o=>{let a,s,l,c=0,d=!1,p=!1,_=()=>{s?.unsubscribe(),s=void 0},b=()=>{_(),a=l=void 0,d=p=!1},y=()=>{let w=a;b(),w?.unsubscribe()};return Ke((w,C)=>{c++,!p&&!d&&_();let D=l=l??n();C.add(()=>{c--,c===0&&!p&&!d&&(s=rD(y,r))}),D.subscribe(C),!a&&c>0&&(a=new il({next:F=>D.next(F),error:F=>{p=!0,_(),s=rD(b,e,F),D.error(F)},complete:()=>{d=!0,_(),s=rD(b,i),D.complete()}}),zt(w).subscribe(a))})(o)}}function rD(t,n,...e){if(n===!0){t();return}if(n===!1)return;let i=new il({next:()=>{i.unsubscribe(),t()}});return zt(n(...e)).subscribe(i)}function vd(t,n,e){let i,r=!1;return t&&typeof t=="object"?{bufferSize:i=1/0,windowTime:n=1/0,refCount:r=!1,scheduler:e}=t:i=t??1/0,ec({connector:()=>new ss(i,n,e),resetOnError:!0,resetOnComplete:!1,resetOnRefCountZero:r})}function us(t){return ce((n,e)=>t<=e)}function Ue(...t){let n=ls(t);return Ke((e,i)=>{(n?Co(t,e,n):Co(t,e)).subscribe(i)})}function je(t,n){return Ke((e,i)=>{let r=null,o=0,a=!1,s=()=>a&&!r&&i.complete();e.subscribe($e(i,l=>{r?.unsubscribe();let c=0,d=o++;zt(t(l,d)).subscribe(r=$e(i,p=>i.next(n?n(l,p,d,c++):p),()=>{r=null,s()}))},()=>{a=!0,s()}))})}function xe(t){return Ke((n,e)=>{zt(t).subscribe($e(e,()=>e.complete(),fd)),!e.closed&&n.subscribe(e)})}function oD(t,n=!1){return Ke((e,i)=>{let r=0;e.subscribe($e(i,o=>{let a=t(o,r++);(a||n)&&i.next(o),!a&&i.complete()}))})}function He(t,n,e){let i=Xe(t)||n||e?{next:t,error:n,complete:e}:t;return i?Ke((r,o)=>{var a;(a=i.subscribe)===null||a===void 0||a.call(i);let s=!0;r.subscribe($e(o,l=>{var c;(c=i.next)===null||c===void 0||c.call(i,l),o.next(l)},()=>{var l;s=!1,(l=i.complete)===null||l===void 0||l.call(i),o.complete()},l=>{var c;s=!1,(c=i.error)===null||c===void 0||c.call(i,l),o.error(l)},()=>{var l,c;s&&((l=i.unsubscribe)===null||l===void 0||l.call(i)),(c=i.finalize)===null||c===void 0||c.call(i)}))}):Lr}function aO(t){let n=tt(null);try{return t()}finally{tt(n)}}var sO=Me(I({},dd),{consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,dirty:!0,kind:"effect"});function lO(t){if(t.dirty=!1,t.version>0&&!md(t))return;t.version++;let n=Gl(t);try{t.cleanup(),t.fn()}finally{ud(t,n)}}var tc=class{full;major;minor;patch;constructor(n){this.full=n;let e=n.split(".");this.major=e[0],this.minor=e[1],this.patch=e.slice(2).join(".")}},Tp=new tc("20.3.16");var vb="https://angular.dev/best-practices/security#preventing-cross-site-scripting-xss",ue=class extends Error{code;constructor(n,e){super(zo(n,e)),this.code=n}};function E$(t){return`NG0${Math.abs(t)}`}function zo(t,n){return`${E$(t)}${n?": "+n:""}`}var Uo=globalThis;function si(t){for(let n in t)if(t[n]===si)return n;throw Error("")}function uO(t,n){for(let e in n)n.hasOwnProperty(e)&&!t.hasOwnProperty(e)&&(t[e]=n[e])}function sl(t){if(typeof t=="string")return t;if(Array.isArray(t))return`[${t.map(sl).join(", ")}]`;if(t==null)return""+t;let n=t.overriddenName||t.name;if(n)return`${n}`;let e=t.toString();if(e==null)return""+e;let i=e.indexOf(` +`);return i>=0?e.slice(0,i):e}function yb(t,n){return t?n?`${t} ${n}`:t:n||""}var S$=si({__forward_ref__:si});function li(t){return t.__forward_ref__=li,t.toString=function(){return sl(this())},t}function Cn(t){return _D(t)?t():t}function _D(t){return typeof t=="function"&&t.hasOwnProperty(S$)&&t.__forward_ref__===li}function R(t){return{token:t.token,providedIn:t.providedIn||null,factory:t.factory,value:void 0}}function J(t){return{providers:t.providers||[],imports:t.imports||[]}}function Ip(t){return k$(t,xb)}function bD(t){return Ip(t)!==null}function k$(t,n){return t.hasOwnProperty(n)&&t[n]||null}function T$(t){let n=t?.[xb]??null;return n||null}function sD(t){return t&&t.hasOwnProperty(_b)?t[_b]:null}var xb=si({\u0275prov:si}),_b=si({\u0275inj:si}),O=class{_desc;ngMetadataName="InjectionToken";\u0275prov;constructor(n,e){this._desc=n,this.\u0275prov=void 0,typeof e=="number"?this.__NG_ELEMENT_ID__=e:e!==void 0&&(this.\u0275prov=R({token:this,providedIn:e.providedIn||"root",factory:e.factory}))}get multi(){return this}toString(){return`InjectionToken ${this._desc}`}};function vD(t){return t&&!!t.\u0275providers}var yD=si({\u0275cmp:si}),xD=si({\u0275dir:si}),CD=si({\u0275pipe:si}),wD=si({\u0275mod:si}),Ep=si({\u0275fac:si}),Dd=si({__NG_ELEMENT_ID__:si}),cO=si({__NG_ENV_ID__:si});function $o(t){return typeof t=="string"?t:t==null?"":String(t)}function mO(t){return typeof t=="function"?t.name||t.toString():typeof t=="object"&&t!=null&&typeof t.type=="function"?t.type.name||t.type.toString():$o(t)}var hO=si({ngErrorCode:si}),I$=si({ngErrorMessage:si}),A$=si({ngTokenPath:si});function DD(t,n){return pO("",-200,n)}function Cb(t,n){throw new ue(-201,!1)}function pO(t,n,e){let i=new ue(n,t);return i[hO]=n,i[I$]=t,e&&(i[A$]=e),i}function O$(t){return t[hO]}var lD;function fO(){return lD}function Vr(t){let n=lD;return lD=t,n}function MD(t,n,e){let i=Ip(t);if(i&&i.providedIn=="root")return i.value===void 0?i.value=i.factory():i.value;if(e&8)return null;if(n!==void 0)return n;Cb(t,"Injector")}var R$={},yd=R$,cD="__NG_DI_FLAG__",dD=class{injector;constructor(n){this.injector=n}retrieve(n,e){let i=xd(e)||0;try{return this.injector.get(n,i&8?null:yd,i)}catch(r){if(ju(r))return r;throw r}}};function P$(t,n=0){let e=A_();if(e===void 0)throw new ue(-203,!1);if(e===null)return MD(t,void 0,n);{let i=F$(n),r=e.retrieve(t,i);if(ju(r)){if(i.optional)return null;throw r}return r}}function pe(t,n=0){return(fO()||P$)(Cn(t),n)}function u(t,n){return pe(t,xd(n))}function xd(t){return typeof t>"u"||typeof t=="number"?t:0|(t.optional&&8)|(t.host&&1)|(t.self&&2)|(t.skipSelf&&4)}function F$(t){return{optional:!!(t&8),host:!!(t&1),self:!!(t&2),skipSelf:!!(t&4)}}function uD(t){let n=[];for(let e=0;eArray.isArray(e)?wb(e,n):n(e))}function SD(t,n,e){n>=t.length?t.push(e):t.splice(n,0,e)}function Ap(t,n){return n>=t.length-1?t.pop():t.splice(n,1)[0]}function bO(t,n){let e=[];for(let i=0;in;){let o=r-2;t[r]=t[o],r--}t[n]=e,t[n+1]=i}}function Op(t,n,e){let i=em(t,n);return i>=0?t[i|1]=e:(i=~i,vO(t,i,n,e)),i}function Db(t,n){let e=em(t,n);if(e>=0)return t[e|1]}function em(t,n){return L$(t,n,1)}function L$(t,n,e){let i=0,r=t.length>>e;for(;r!==i;){let o=i+(r-i>>1),a=t[o<n?r=o:i=o+1}return~(r<{e.push(a)};return wb(n,a=>{let s=a;bb(s,o,[],i)&&(r||=[],r.push(s))}),r!==void 0&&CO(r,o),e}function CO(t,n){for(let e=0;e{n(o,i)})}}function bb(t,n,e,i){if(t=Cn(t),!t)return!1;let r=null,o=sD(t),a=!o&&hs(t);if(!o&&!a){let l=t.ngModule;if(o=sD(l),o)r=l;else return!1}else{if(a&&!a.standalone)return!1;r=t}let s=i.has(r);if(a){if(s)return!1;if(i.add(r),a.dependencies){let l=typeof a.dependencies=="function"?a.dependencies():a.dependencies;for(let c of l)bb(c,n,e,i)}}else if(o){if(o.imports!=null&&!s){i.add(r);let c;try{wb(o.imports,d=>{bb(d,n,e,i)&&(c||=[],c.push(d))})}finally{}c!==void 0&&CO(c,n)}if(!s){let c=ic(r)||(()=>new r);n({provide:r,useFactory:c,deps:xr},r),n({provide:TD,useValue:r,multi:!0},r),n({provide:ms,useValue:()=>pe(r),multi:!0},r)}let l=o.providers;if(l!=null&&!s){let c=t;OD(l,d=>{n(d,c)})}}else return!1;return r!==t&&t.providers!==void 0}function OD(t,n){for(let e of t)vD(e)&&(e=e.\u0275providers),Array.isArray(e)?OD(e,n):n(e)}var V$=si({provide:String,useValue:si});function wO(t){return t!==null&&typeof t=="object"&&V$ in t}function B$(t){return!!(t&&t.useExisting)}function j$(t){return!!(t&&t.useFactory)}function Cd(t){return typeof t=="function"}function DO(t){return!!t.useClass}var Rp=new O(""),gb={},dO={},aD;function tm(){return aD===void 0&&(aD=new Sp),aD}var ti=class{},wd=class extends ti{parent;source;scopes;records=new Map;_ngOnDestroyHooks=new Set;_onDestroyHooks=[];get destroyed(){return this._destroyed}_destroyed=!1;injectorDefTypes;constructor(n,e,i,r){super(),this.parent=e,this.source=i,this.scopes=r,hD(n,a=>this.processProvider(a)),this.records.set(kD,Ju(void 0,this)),r.has("environment")&&this.records.set(ti,Ju(void 0,this));let o=this.records.get(Rp);o!=null&&typeof o.value=="string"&&this.scopes.add(o.value),this.injectorDefTypes=new Set(this.get(TD,xr,{self:!0}))}retrieve(n,e){let i=xd(e)||0;try{return this.get(n,yd,i)}catch(r){if(ju(r))return r;throw r}}destroy(){Mp(this),this._destroyed=!0;let n=tt(null);try{for(let i of this._ngOnDestroyHooks)i.ngOnDestroy();let e=this._onDestroyHooks;this._onDestroyHooks=[];for(let i of e)i()}finally{this.records.clear(),this._ngOnDestroyHooks.clear(),this.injectorDefTypes.clear(),tt(n)}}onDestroy(n){return Mp(this),this._onDestroyHooks.push(n),()=>this.removeOnDestroy(n)}runInContext(n){Mp(this);let e=as(this),i=Vr(void 0),r;try{return n()}finally{as(e),Vr(i)}}get(n,e=yd,i){if(Mp(this),n.hasOwnProperty(cO))return n[cO](this);let r=xd(i),o,a=as(this),s=Vr(void 0);try{if(!(r&4)){let c=this.records.get(n);if(c===void 0){let d=W$(n)&&Ip(n);d&&this.injectableDefInScope(d)?c=Ju(mD(n),gb):c=null,this.records.set(n,c)}if(c!=null)return this.hydrate(n,c,r)}let l=r&2?tm():this.parent;return e=r&8&&e===yd?null:e,l.get(n,e)}catch(l){let c=O$(l);throw c===-200||c===-201?new ue(c,null):l}finally{Vr(s),as(a)}}resolveInjectorInitializers(){let n=tt(null),e=as(this),i=Vr(void 0),r;try{let o=this.get(ms,xr,{self:!0});for(let a of o)a()}finally{as(e),Vr(i),tt(n)}}toString(){let n=[],e=this.records;for(let i of e.keys())n.push(sl(i));return`R3Injector[${n.join(", ")}]`}processProvider(n){n=Cn(n);let e=Cd(n)?n:Cn(n&&n.provide),i=z$(n);if(!Cd(n)&&n.multi===!0){let r=this.records.get(e);r||(r=Ju(void 0,gb,!0),r.factory=()=>uD(r.multi),this.records.set(e,r)),e=n,r.multi.push(n)}this.records.set(e,i)}hydrate(n,e,i){let r=tt(null);try{if(e.value===dO)throw DD(sl(n));return e.value===gb&&(e.value=dO,e.value=e.factory(void 0,i)),typeof e.value=="object"&&e.value&&$$(e.value)&&this._ngOnDestroyHooks.add(e.value),e.value}finally{tt(r)}}injectableDefInScope(n){if(!n.providedIn)return!1;let e=Cn(n.providedIn);return typeof e=="string"?e==="any"||this.scopes.has(e):this.injectorDefTypes.has(e)}removeOnDestroy(n){let e=this._onDestroyHooks.indexOf(n);e!==-1&&this._onDestroyHooks.splice(e,1)}};function mD(t){let n=Ip(t),e=n!==null?n.factory:ic(t);if(e!==null)return e;if(t instanceof O)throw new ue(204,!1);if(t instanceof Function)return H$(t);throw new ue(204,!1)}function H$(t){if(t.length>0)throw new ue(204,!1);let e=T$(t);return e!==null?()=>e.factory(t):()=>new t}function z$(t){if(wO(t))return Ju(void 0,t.useValue);{let n=RD(t);return Ju(n,gb)}}function RD(t,n,e){let i;if(Cd(t)){let r=Cn(t);return ic(r)||mD(r)}else if(wO(t))i=()=>Cn(t.useValue);else if(j$(t))i=()=>t.useFactory(...uD(t.deps||[]));else if(B$(t))i=(r,o)=>pe(Cn(t.useExisting),o!==void 0&&o&8?8:void 0);else{let r=Cn(t&&(t.useClass||t.provide));if(U$(t))i=()=>new r(...uD(t.deps));else return ic(r)||mD(r)}return i}function Mp(t){if(t.destroyed)throw new ue(205,!1)}function Ju(t,n,e=!1){return{factory:t,value:n,multi:e?[]:void 0}}function U$(t){return!!t.deps}function $$(t){return t!==null&&typeof t=="object"&&typeof t.ngOnDestroy=="function"}function W$(t){return typeof t=="function"||typeof t=="object"&&t.ngMetadataName==="InjectionToken"}function hD(t,n){for(let e of t)Array.isArray(e)?hD(e,n):e&&vD(e)?hD(e.\u0275providers,n):n(e)}function Vn(t,n){let e;t instanceof wd?(Mp(t),e=t):e=new dD(t);let i,r=as(e),o=Vr(void 0);try{return n()}finally{as(r),Vr(o)}}function MO(){return fO()!==void 0||A_()!=null}var Pa=0,We=1,nt=2,wn=3,Wo=4,Br=5,Md=6,im=7,en=8,ll=9,ps=10,mi=11,nm=12,PD=13,Ed=14,Cr=15,rc=16,Sd=17,fs=18,Pp=19,FD=20,al=21,Sb=22,cl=23,wo=24,kd=25,Td=26,vi=27,EO=1,ND=6,oc=7,Fp=8,Id=9,tn=10;function gs(t){return Array.isArray(t)&&typeof t[EO]=="object"}function Fa(t){return Array.isArray(t)&&t[EO]===!0}function LD(t){return(t.flags&4)!==0}function dl(t){return t.componentOffset>-1}function rm(t){return(t.flags&1)===1}function Na(t){return!!t.template}function om(t){return(t[nt]&512)!==0}function Ad(t){return(t[nt]&256)===256}var VD="svg",SO="math";function Go(t){for(;Array.isArray(t);)t=t[Pa];return t}function BD(t,n){return Go(n[t])}function qo(t,n){return Go(n[t.index])}function Np(t,n){return t.data[n]}function am(t,n){return t[n]}function jD(t,n,e,i){e>=t.data.length&&(t.data[e]=null,t.blueprint[e]=null),n[e]=i}function Yo(t,n){let e=n[t];return gs(e)?e:e[Pa]}function kO(t){return(t[nt]&4)===4}function kb(t){return(t[nt]&128)===128}function TO(t){return Fa(t[wn])}function Do(t,n){return n==null?null:t[n]}function HD(t){t[Sd]=0}function zD(t){t[nt]&1024||(t[nt]|=1024,kb(t)&&ac(t))}function IO(t,n){for(;t>0;)n=n[Ed],t--;return n}function Lp(t){return!!(t[nt]&9216||t[wo]?.dirty)}function Tb(t){t[ps].changeDetectionScheduler?.notify(8),t[nt]&64&&(t[nt]|=1024),Lp(t)&&ac(t)}function ac(t){t[ps].changeDetectionScheduler?.notify(0);let n=nc(t);for(;n!==null&&!(n[nt]&8192||(n[nt]|=8192,!kb(n)));)n=nc(n)}function UD(t,n){if(Ad(t))throw new ue(911,!1);t[al]===null&&(t[al]=[]),t[al].push(n)}function AO(t,n){if(t[al]===null)return;let e=t[al].indexOf(n);e!==-1&&t[al].splice(e,1)}function nc(t){let n=t[wn];return Fa(n)?n[wn]:n}function $D(t){return t[im]??=[]}function WD(t){return t.cleanup??=[]}function OO(t,n,e,i){let r=$D(n);r.push(e),t.firstCreatePass&&WD(t).push(i,r.length-1)}var bt={lFrame:UO(null),bindingsEnabled:!0,skipHydrationRootTNode:null};var pD=!1;function RO(){return bt.lFrame.elementDepthCount}function PO(){bt.lFrame.elementDepthCount++}function GD(){bt.lFrame.elementDepthCount--}function Ib(){return bt.bindingsEnabled}function qD(){return bt.skipHydrationRootTNode!==null}function YD(t){return bt.skipHydrationRootTNode===t}function QD(){bt.skipHydrationRootTNode=null}function Le(){return bt.lFrame.lView}function Di(){return bt.lFrame.tView}function k(t){return bt.lFrame.contextLView=t,t[en]}function T(t){return bt.lFrame.contextLView=null,t}function sn(){let t=KD();for(;t!==null&&t.type===64;)t=t.parent;return t}function KD(){return bt.lFrame.currentTNode}function FO(){let t=bt.lFrame,n=t.currentTNode;return t.isParent?n:n.parent}function sm(t,n){let e=bt.lFrame;e.currentTNode=t,e.isParent=n}function ZD(){return bt.lFrame.isParent}function XD(){bt.lFrame.isParent=!1}function NO(){return bt.lFrame.contextLView}function JD(){return pD}function lm(t){let n=pD;return pD=t,n}function _s(){let t=bt.lFrame,n=t.bindingRootIndex;return n===-1&&(n=t.bindingRootIndex=t.tView.bindingStartIndex),n}function e1(){return bt.lFrame.bindingIndex}function LO(t){return bt.lFrame.bindingIndex=t}function bs(){return bt.lFrame.bindingIndex++}function Vp(t){let n=bt.lFrame,e=n.bindingIndex;return n.bindingIndex=n.bindingIndex+t,e}function VO(){return bt.lFrame.inI18n}function BO(t,n){let e=bt.lFrame;e.bindingIndex=e.bindingRootIndex=t,Ab(n)}function jO(){return bt.lFrame.currentDirectiveIndex}function Ab(t){bt.lFrame.currentDirectiveIndex=t}function HO(t){let n=bt.lFrame.currentDirectiveIndex;return n===-1?null:t[n]}function Ob(){return bt.lFrame.currentQueryIndex}function Bp(t){bt.lFrame.currentQueryIndex=t}function G$(t){let n=t[We];return n.type===2?n.declTNode:n.type===1?t[Br]:null}function t1(t,n,e){if(e&4){let r=n,o=t;for(;r=r.parent,r===null&&!(e&1);)if(r=G$(o),r===null||(o=o[Ed],r.type&10))break;if(r===null)return!1;n=r,t=o}let i=bt.lFrame=zO();return i.currentTNode=n,i.lView=t,!0}function Rb(t){let n=zO(),e=t[We];bt.lFrame=n,n.currentTNode=e.firstChild,n.lView=t,n.tView=e,n.contextLView=t,n.bindingIndex=e.bindingStartIndex,n.inI18n=!1}function zO(){let t=bt.lFrame,n=t===null?null:t.child;return n===null?UO(t):n}function UO(t){let n={currentTNode:null,isParent:!0,lView:null,tView:null,selectedIndex:-1,contextLView:null,elementDepthCount:0,currentNamespace:null,currentDirectiveIndex:-1,bindingRootIndex:-1,bindingIndex:-1,currentQueryIndex:0,parent:t,child:null,inI18n:!1};return t!==null&&(t.child=n),n}function $O(){let t=bt.lFrame;return bt.lFrame=t.parent,t.currentTNode=null,t.lView=null,t}var i1=$O;function Pb(){let t=$O();t.isParent=!0,t.tView=null,t.selectedIndex=-1,t.contextLView=null,t.elementDepthCount=0,t.currentDirectiveIndex=-1,t.currentNamespace=null,t.bindingRootIndex=-1,t.bindingIndex=-1,t.currentQueryIndex=0}function WO(t){return(bt.lFrame.contextLView=IO(t,bt.lFrame.contextLView))[en]}function La(){return bt.lFrame.selectedIndex}function sc(t){bt.lFrame.selectedIndex=t}function cm(){let t=bt.lFrame;return Np(t.tView,t.selectedIndex)}function ii(){bt.lFrame.currentNamespace=VD}function Qo(){q$()}function q$(){bt.lFrame.currentNamespace=null}function GO(){return bt.lFrame.currentNamespace}var qO=!0;function Fb(){return qO}function jp(t){qO=t}function fD(t,n=null,e=null,i){let r=n1(t,n,e,i);return r.resolveInjectorInitializers(),r}function n1(t,n=null,e=null,i,r=new Set){let o=[e||xr,Eb(t)];return i=i||(typeof t=="object"?void 0:sl(t)),new wd(o,n||tm(),i||null,r)}var de=class t{static THROW_IF_NOT_FOUND=yd;static NULL=new Sp;static create(n,e){if(Array.isArray(n))return fD({name:""},e,n,"");{let i=n.name??"";return fD({name:i},n.parent,n.providers,i)}}static \u0275prov=R({token:t,providedIn:"any",factory:()=>pe(kD)});static __NG_ELEMENT_ID__=-1},_e=new O(""),ln=(()=>{class t{static __NG_ELEMENT_ID__=Y$;static __NG_ENV_ID__=e=>e}return t})(),kp=class extends ln{_lView;constructor(n){super(),this._lView=n}get destroyed(){return Ad(this._lView)}onDestroy(n){let e=this._lView;return UD(e,n),()=>AO(e,n)}};function Y$(){return new kp(Le())}var Ln=class{_console=console;handleError(n){this._console.error("ERROR",n)}},wr=new O("",{providedIn:"root",factory:()=>{let t=u(ti),n;return e=>{t.destroyed&&!n?setTimeout(()=>{throw e}):(n??=t.get(Ln),n.handleError(e))}}}),YO={provide:ms,useValue:()=>void u(Ln),multi:!0},Q$=new O("",{providedIn:"root",factory:()=>{let t=u(_e).defaultView;if(!t)return;let n=u(wr),e=o=>{n(o.reason),o.preventDefault()},i=o=>{o.error?n(o.error):n(new Error(o.message,{cause:o})),o.preventDefault()},r=()=>{t.addEventListener("unhandledrejection",e),t.addEventListener("error",i)};typeof Zone<"u"?Zone.root.run(r):r(),u(ln).onDestroy(()=>{t.removeEventListener("error",i),t.removeEventListener("unhandledrejection",e)})}});function r1(){return Jr([xO(()=>void u(Q$))])}function vs(t){return typeof t=="function"&&t[mn]!==void 0}function he(t,n){let[e,i,r]=Bw(t,n?.equal),o=e,a=o[mn];return o.set=i,o.update=r,o.asReadonly=Nb.bind(o),o}function Nb(){let t=this[mn];if(t.readonlyFn===void 0){let n=()=>this();n[mn]=t,t.readonlyFn=n}return t.readonlyFn}function o1(t){return vs(t)&&typeof t.set=="function"}var Od=(()=>{class t{view;node;constructor(e,i){this.view=e,this.node=i}static __NG_ELEMENT_ID__=K$}return t})();function K$(){return new Od(Le(),sn())}var Ho=class{},Hp=new O("",{providedIn:"root",factory:()=>!1});var a1=new O(""),s1=new O(""),ys=(()=>{class t{taskId=0;pendingTasks=new Set;destroyed=!1;pendingTask=new rt(!1);get hasPendingTasks(){return this.destroyed?!1:this.pendingTask.value}get hasPendingTasksObservable(){return this.destroyed?new Ne(e=>{e.next(!1),e.complete()}):this.pendingTask}add(){!this.hasPendingTasks&&!this.destroyed&&this.pendingTask.next(!0);let e=this.taskId++;return this.pendingTasks.add(e),e}has(e){return this.pendingTasks.has(e)}remove(e){this.pendingTasks.delete(e),this.pendingTasks.size===0&&this.hasPendingTasks&&this.pendingTask.next(!1)}ngOnDestroy(){this.pendingTasks.clear(),this.hasPendingTasks&&this.pendingTask.next(!1),this.destroyed=!0,this.pendingTask.unsubscribe()}static \u0275prov=R({token:t,providedIn:"root",factory:()=>new t})}return t})(),dm=(()=>{class t{internalPendingTasks=u(ys);scheduler=u(Ho);errorHandler=u(wr);add(){let e=this.internalPendingTasks.add();return()=>{this.internalPendingTasks.has(e)&&(this.scheduler.notify(11),this.internalPendingTasks.remove(e))}}run(e){let i=this.add();e().catch(this.errorHandler).finally(i)}static \u0275prov=R({token:t,providedIn:"root",factory:()=>new t})}return t})();function Rd(...t){}var zp=(()=>{class t{static \u0275prov=R({token:t,providedIn:"root",factory:()=>new gD})}return t})(),gD=class{dirtyEffectCount=0;queues=new Map;add(n){this.enqueue(n),this.schedule(n)}schedule(n){n.dirty&&this.dirtyEffectCount++}remove(n){let e=n.zone,i=this.queues.get(e);i.has(n)&&(i.delete(n),n.dirty&&this.dirtyEffectCount--)}enqueue(n){let e=n.zone;this.queues.has(e)||this.queues.set(e,new Set);let i=this.queues.get(e);i.has(n)||i.add(n)}flush(){for(;this.dirtyEffectCount>0;){let n=!1;for(let[e,i]of this.queues)e===null?n||=this.flushQueue(i):n||=e.run(()=>this.flushQueue(i));n||(this.dirtyEffectCount=0)}}flushQueue(n){let e=!1;for(let i of n)i.dirty&&(this.dirtyEffectCount--,e=!0,i.run());return e}};function vm(t){return{toString:t}.toString()}var Lb="__parameters__";function iW(t){return function(...e){if(t){let i=t(...e);for(let r in i)this[r]=i[r]}}}function PR(t,n,e){return vm(()=>{let i=iW(n);function r(...o){if(this instanceof r)return i.apply(this,o),this;let a=new r(...o);return s.annotation=a,s;function s(l,c,d){let p=l.hasOwnProperty(Lb)?l[Lb]:Object.defineProperty(l,Lb,{value:[]})[Lb];for(;p.length<=d;)p.push(null);return(p[d]=p[d]||[]).push(a),l}}return r.prototype.ngMetadataName=t,r.annotationCls=r,r})}var Ds=ED(PR("Optional"),8);var dc=ED(PR("SkipSelf"),4);var Vd=Function;function nW(t){return typeof t=="function"}var Yb=class{previousValue;currentValue;firstChange;constructor(n,e,i){this.previousValue=n,this.currentValue=e,this.firstChange=i}isFirstChange(){return this.firstChange}};function FR(t,n,e,i){n!==null?n.applyValueToInputSignal(n,i):t[e]=i}var Oe=(()=>{let t=()=>NR;return t.ngInherit=!0,t})();function NR(t){return t.type.prototype.ngOnChanges&&(t.setInput=oW),rW}function rW(){let t=VR(this),n=t?.current;if(n){let e=t.previous;if(e===Ra)t.previous=n;else for(let i in n)e[i]=n[i];t.current=null,this.ngOnChanges(n)}}function oW(t,n,e,i,r){let o=this.declaredInputs[i],a=VR(t)||aW(t,{previous:Ra,current:null}),s=a.current||(a.current={}),l=a.previous,c=l[o];s[o]=new Yb(c&&c.currentValue,e,l===Ra),FR(t,n,r,e)}var LR="__ngSimpleChanges__";function VR(t){return t[LR]||null}function aW(t,n){return t[LR]=n}var QO=[];var hi=function(t,n=null,e){for(let i=0;i=i)break}else n[l]<0&&(t[Sd]+=65536),(s>14>16&&(t[nt]&3)===n&&(t[nt]+=16384,KO(s,o)):KO(s,o)}var mm=-1,Fd=class{factory;name;injectImpl;resolving=!1;canSeeViewProviders;multi;componentProviders;index;providerFactory;constructor(n,e,i,r){this.factory=n,this.name=r,this.canSeeViewProviders=e,this.injectImpl=i}};function cW(t){return(t.flags&8)!==0}function dW(t){return(t.flags&16)!==0}function uW(t,n,e){let i=0;for(;in){a=o-1;break}}}for(;o>16}function Kb(t,n){let e=hW(t),i=n;for(;e>0;)i=i[Ed],e--;return i}var v1=!0;function Zb(t){let n=v1;return v1=t,n}var pW=256,UR=pW-1,$R=5,fW=0,xs={};function gW(t,n,e){let i;typeof e=="string"?i=e.charCodeAt(0)||0:e.hasOwnProperty(Dd)&&(i=e[Dd]),i==null&&(i=e[Dd]=fW++);let r=i&UR,o=1<>$R)]|=o}function Xb(t,n){let e=WR(t,n);if(e!==-1)return e;let i=n[We];i.firstCreatePass&&(t.injectorIndex=n.length,c1(i.data,t),c1(n,null),c1(i.blueprint,null));let r=rM(t,n),o=t.injectorIndex;if(zR(r)){let a=Qb(r),s=Kb(r,n),l=s[We].data;for(let c=0;c<8;c++)n[o+c]=s[a+c]|l[a+c]}return n[o+8]=r,o}function c1(t,n){t.push(0,0,0,0,0,0,0,0,n)}function WR(t,n){return t.injectorIndex===-1||t.parent&&t.parent.injectorIndex===t.injectorIndex||n[t.injectorIndex+8]===null?-1:t.injectorIndex}function rM(t,n){if(t.parent&&t.parent.injectorIndex!==-1)return t.parent.injectorIndex;let e=0,i=null,r=n;for(;r!==null;){if(i=KR(r),i===null)return mm;if(e++,r=r[Ed],i.injectorIndex!==-1)return i.injectorIndex|e<<16}return mm}function y1(t,n,e){gW(t,n,e)}function _W(t,n){if(n==="class")return t.classes;if(n==="style")return t.styles;let e=t.attrs;if(e){let i=e.length,r=0;for(;r>20,p=i?s:s+d,_=r?s+d:c;for(let b=p;b<_;b++){let y=a[b];if(b=l&&y.type===e)return b}if(r){let b=a[l];if(b&&Na(b)&&b.type===e)return l}return null}function Wp(t,n,e,i,r){let o=t[e],a=n.data;if(o instanceof Fd){let s=o;if(s.resolving){let b=mO(a[e]);throw DD(b)}let l=Zb(s.canSeeViewProviders);s.resolving=!0;let c=a[e].type||a[e],d,p=s.injectImpl?Vr(s.injectImpl):null,_=t1(t,i,0);try{o=t[e]=s.factory(void 0,r,a,t,i),n.firstCreatePass&&e>=i.directiveStart&&sW(e,a[e],n)}finally{p!==null&&Vr(p),Zb(l),s.resolving=!1,i1()}}return o}function vW(t){if(typeof t=="string")return t.charCodeAt(0)||0;let n=t.hasOwnProperty(Dd)?t[Dd]:void 0;return typeof n=="number"?n>=0?n&UR:yW:n}function XO(t,n,e){let i=1<>$R)]&i)}function JO(t,n){return!(t&2)&&!(t&1&&n)}var Pd=class{_tNode;_lView;constructor(n,e){this._tNode=n,this._lView=e}get(n,e,i){return YR(this._tNode,this._lView,n,xd(i),e)}};function yW(){return new Pd(sn(),Le())}function ge(t){return vm(()=>{let n=t.prototype.constructor,e=n[Ep]||x1(n),i=Object.prototype,r=Object.getPrototypeOf(t.prototype).constructor;for(;r&&r!==i;){let o=r[Ep]||x1(r);if(o&&o!==e)return o;r=Object.getPrototypeOf(r)}return o=>new o})}function x1(t){return _D(t)?()=>{let n=x1(Cn(t));return n&&n()}:ic(t)}function xW(t,n,e,i,r){let o=t,a=n;for(;o!==null&&a!==null&&a[nt]&2048&&!om(a);){let s=QR(o,a,e,i|2,xs);if(s!==xs)return s;let l=o.parent;if(!l){let c=a[FD];if(c){let d=c.get(e,xs,i);if(d!==xs)return d}l=KR(a),a=a[Ed]}o=l}return r}function KR(t){let n=t[We],e=n.type;return e===2?n.declTNode:e===1?t[Br]:null}function Jp(t){return _W(sn(),t)}function CW(){return ym(sn(),Le())}function ym(t,n){return new Y(qo(t,n))}var Y=(()=>{class t{nativeElement;constructor(e){this.nativeElement=e}static __NG_ELEMENT_ID__=CW}return t})();function ZR(t){return t instanceof Y?t.nativeElement:t}function wW(){return this._results[Symbol.iterator]()}var Dr=class{_emitDistinctChangesOnly;dirty=!0;_onDirty=void 0;_results=[];_changesDetected=!1;_changes=void 0;length=0;first=void 0;last=void 0;get changes(){return this._changes??=new z}constructor(n=!1){this._emitDistinctChangesOnly=n}get(n){return this._results[n]}map(n){return this._results.map(n)}filter(n){return this._results.filter(n)}find(n){return this._results.find(n)}reduce(n,e){return this._results.reduce(n,e)}forEach(n){this._results.forEach(n)}some(n){return this._results.some(n)}toArray(){return this._results.slice()}toString(){return this._results.toString()}reset(n,e){this.dirty=!1;let i=_O(n);(this._changesDetected=!gO(this._results,i,e))&&(this._results=i,this.length=i.length,this.last=i[this.length-1],this.first=i[0])}notifyOnChanges(){this._changes!==void 0&&(this._changesDetected||!this._emitDistinctChangesOnly)&&this._changes.next(this)}onDirty(n){this._onDirty=n}setDirty(){this.dirty=!0,this._onDirty?.()}destroy(){this._changes!==void 0&&(this._changes.complete(),this._changes.unsubscribe())}[Symbol.iterator]=wW};function XR(t){return(t.flags&128)===128}var oM=(function(t){return t[t.OnPush=0]="OnPush",t[t.Default=1]="Default",t})(oM||{}),JR=new Map,DW=0;function MW(){return DW++}function EW(t){JR.set(t[Pp],t)}function C1(t){JR.delete(t[Pp])}var eR="__ngContext__";function pm(t,n){gs(n)?(t[eR]=n[Pp],EW(n)):t[eR]=n}function eP(t){return iP(t[nm])}function tP(t){return iP(t[Wo])}function iP(t){for(;t!==null&&!Fa(t);)t=t[Wo];return t}var w1;function aM(t){w1=t}function sM(){if(w1!==void 0)return w1;if(typeof document<"u")return document;throw new ue(210,!1)}var uc=new O("",{providedIn:"root",factory:()=>SW}),SW="ng",m0=new O(""),hl=new O("",{providedIn:"platform",factory:()=>"unknown"});var ef=new O(""),xm=new O("",{providedIn:"root",factory:()=>sM().body?.querySelector("[ngCspNonce]")?.getAttribute("ngCspNonce")||null});var kW="h",TW="b";var nP="r";var rP="di";var oP=!1,aP=new O("",{providedIn:"root",factory:()=>oP});var h0=new O("");var IW=(t,n,e,i)=>{};function AW(t,n,e,i){IW(t,n,e,i)}function p0(t){return(t.flags&32)===32}var OW=()=>null;function sP(t,n,e=!1){return OW(t,n,e)}function lP(t,n){let e=t.contentQueries;if(e!==null){let i=tt(null);try{for(let r=0;rt,createScript:t=>t,createScriptURL:t=>t})}catch{}return Vb}function f0(t){return RW()?.createHTML(t)||t}var Bb;function cP(){if(Bb===void 0&&(Bb=null,Uo.trustedTypes))try{Bb=Uo.trustedTypes.createPolicy("angular#unsafe-bypass",{createHTML:t=>t,createScript:t=>t,createScriptURL:t=>t})}catch{}return Bb}function tR(t){return cP()?.createHTML(t)||t}function iR(t){return cP()?.createScriptURL(t)||t}var ml=class{changingThisBreaksApplicationSecurity;constructor(n){this.changingThisBreaksApplicationSecurity=n}toString(){return`SafeValue must use [property]=binding: ${this.changingThisBreaksApplicationSecurity} (see ${vb})`}},M1=class extends ml{getTypeName(){return"HTML"}},E1=class extends ml{getTypeName(){return"Style"}},S1=class extends ml{getTypeName(){return"Script"}},k1=class extends ml{getTypeName(){return"URL"}},T1=class extends ml{getTypeName(){return"ResourceURL"}};function eo(t){return t instanceof ml?t.changingThisBreaksApplicationSecurity:t}function Ms(t,n){let e=dP(t);if(e!=null&&e!==n){if(e==="ResourceURL"&&n==="URL")return!0;throw new Error(`Required a safe ${n}, got a ${e} (see ${vb})`)}return e===n}function dP(t){return t instanceof ml&&t.getTypeName()||null}function cM(t){return new M1(t)}function dM(t){return new E1(t)}function uM(t){return new S1(t)}function mM(t){return new k1(t)}function hM(t){return new T1(t)}function PW(t){let n=new A1(t);return FW()?new I1(n):n}var I1=class{inertDocumentHelper;constructor(n){this.inertDocumentHelper=n}getInertBodyElement(n){n=""+n;try{let e=new window.DOMParser().parseFromString(f0(n),"text/html").body;return e===null?this.inertDocumentHelper.getInertBodyElement(n):(e.firstChild?.remove(),e)}catch{return null}}},A1=class{defaultDoc;inertDocument;constructor(n){this.defaultDoc=n,this.inertDocument=this.defaultDoc.implementation.createHTMLDocument("sanitization-inert")}getInertBodyElement(n){let e=this.inertDocument.createElement("template");return e.innerHTML=f0(n),e}};function FW(){try{return!!new window.DOMParser().parseFromString(f0(""),"text/html")}catch{return!1}}var NW=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:\/?#]*(?:[\/?#]|$))/i;function tf(t){return t=String(t),t.match(NW)?t:"unsafe:"+t}function pl(t){let n={};for(let e of t.split(","))n[e]=!0;return n}function nf(...t){let n={};for(let e of t)for(let i in e)e.hasOwnProperty(i)&&(n[i]=!0);return n}var uP=pl("area,br,col,hr,img,wbr"),mP=pl("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),hP=pl("rp,rt"),LW=nf(hP,mP),VW=nf(mP,pl("address,article,aside,blockquote,caption,center,del,details,dialog,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,main,map,menu,nav,ol,pre,section,summary,table,ul")),BW=nf(hP,pl("a,abbr,acronym,audio,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,picture,q,ruby,rp,rt,s,samp,small,source,span,strike,strong,sub,sup,time,track,tt,u,var,video")),nR=nf(uP,VW,BW,LW),pP=pl("background,cite,href,itemtype,longdesc,poster,src,xlink:href"),jW=pl("abbr,accesskey,align,alt,autoplay,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,controls,coords,datetime,default,dir,download,face,headers,height,hidden,hreflang,hspace,ismap,itemscope,itemprop,kind,label,lang,language,loop,media,muted,nohref,nowrap,open,preload,rel,rev,role,rows,rowspan,rules,scope,scrolling,shape,size,sizes,span,srclang,srcset,start,summary,tabindex,target,title,translate,type,usemap,valign,value,vspace,width"),HW=pl("aria-activedescendant,aria-atomic,aria-autocomplete,aria-busy,aria-checked,aria-colcount,aria-colindex,aria-colspan,aria-controls,aria-current,aria-describedby,aria-details,aria-disabled,aria-dropeffect,aria-errormessage,aria-expanded,aria-flowto,aria-grabbed,aria-haspopup,aria-hidden,aria-invalid,aria-keyshortcuts,aria-label,aria-labelledby,aria-level,aria-live,aria-modal,aria-multiline,aria-multiselectable,aria-orientation,aria-owns,aria-placeholder,aria-posinset,aria-pressed,aria-readonly,aria-relevant,aria-required,aria-roledescription,aria-rowcount,aria-rowindex,aria-rowspan,aria-selected,aria-setsize,aria-sort,aria-valuemax,aria-valuemin,aria-valuenow,aria-valuetext"),zW=nf(pP,jW,HW),UW=pl("script,style,template"),O1=class{sanitizedSomething=!1;buf=[];sanitizeChildren(n){let e=n.firstChild,i=!0,r=[];for(;e;){if(e.nodeType===Node.ELEMENT_NODE?i=this.startElement(e):e.nodeType===Node.TEXT_NODE?this.chars(e.nodeValue):this.sanitizedSomething=!0,i&&e.firstChild){r.push(e),e=GW(e);continue}for(;e;){e.nodeType===Node.ELEMENT_NODE&&this.endElement(e);let o=WW(e);if(o){e=o;break}e=r.pop()}}return this.buf.join("")}startElement(n){let e=rR(n).toLowerCase();if(!nR.hasOwnProperty(e))return this.sanitizedSomething=!0,!UW.hasOwnProperty(e);this.buf.push("<"),this.buf.push(e);let i=n.attributes;for(let r=0;r"),!0}endElement(n){let e=rR(n).toLowerCase();nR.hasOwnProperty(e)&&!uP.hasOwnProperty(e)&&(this.buf.push(""))}chars(n){this.buf.push(oR(n))}};function $W(t,n){return(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_CONTAINED_BY)!==Node.DOCUMENT_POSITION_CONTAINED_BY}function WW(t){let n=t.nextSibling;if(n&&t!==n.previousSibling)throw fP(n);return n}function GW(t){let n=t.firstChild;if(n&&$W(t,n))throw fP(n);return n}function rR(t){let n=t.nodeName;return typeof n=="string"?n:"FORM"}function fP(t){return new Error(`Failed to sanitize html because the element is clobbered: ${t.outerHTML}`)}var qW=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,YW=/([^\#-~ |!])/g;function oR(t){return t.replace(/&/g,"&").replace(qW,function(n){let e=n.charCodeAt(0),i=n.charCodeAt(1);return"&#"+((e-55296)*1024+(i-56320)+65536)+";"}).replace(YW,function(n){return"&#"+n.charCodeAt(0)+";"}).replace(//g,">")}var jb;function g0(t,n){let e=null;try{jb=jb||PW(t);let i=n?String(n):"";e=jb.getInertBodyElement(i);let r=5,o=i;do{if(r===0)throw new Error("Failed to sanitize html because the input is unstable");r--,i=o,o=e.innerHTML,e=jb.getInertBodyElement(i)}while(i!==o);let s=new O1().sanitizeChildren(aR(e)||e);return f0(s)}finally{if(e){let i=aR(e)||e;for(;i.firstChild;)i.firstChild.remove()}}}function aR(t){return"content"in t&&QW(t)?t.content:null}function QW(t){return t.nodeType===Node.ELEMENT_NODE&&t.nodeName==="TEMPLATE"}var KW=/^>|^->||--!>|)/g,XW="\u200B$1\u200B";function JW(t){return t.replace(KW,n=>n.replace(ZW,XW))}function e7(t,n){return t.createText(n)}function t7(t,n,e){t.setValue(n,e)}function i7(t,n){return t.createComment(JW(n))}function gP(t,n,e){return t.createElement(n,e)}function Jb(t,n,e,i,r){t.insertBefore(n,e,i,r)}function _P(t,n,e){t.appendChild(n,e)}function sR(t,n,e,i,r){i!==null?Jb(t,n,e,i,r):_P(t,n,e)}function bP(t,n,e,i){t.removeChild(null,n,e,i)}function n7(t,n,e){t.setAttribute(n,"style",e)}function r7(t,n,e){e===""?t.removeAttribute(n,"class"):t.setAttribute(n,"class",e)}function vP(t,n,e){let{mergedAttrs:i,classes:r,styles:o}=e;i!==null&&uW(t,n,i),r!==null&&r7(t,n,r),o!==null&&n7(t,n,o)}var Bn=(function(t){return t[t.NONE=0]="NONE",t[t.HTML=1]="HTML",t[t.STYLE=2]="STYLE",t[t.SCRIPT=3]="SCRIPT",t[t.URL=4]="URL",t[t.RESOURCE_URL=5]="RESOURCE_URL",t})(Bn||{});function rf(t){let n=fM();return n?tR(n.sanitize(Bn.HTML,t)||""):Ms(t,"HTML")?tR(eo(t)):g0(sM(),$o(t))}function to(t){let n=fM();return n?n.sanitize(Bn.URL,t)||"":Ms(t,"URL")?eo(t):tf($o(t))}function yP(t){let n=fM();if(n)return iR(n.sanitize(Bn.RESOURCE_URL,t)||"");if(Ms(t,"ResourceURL"))return iR(eo(t));throw new ue(904,!1)}var o7=new Set(["embed","frame","iframe","media","script"]),a7=new Set(["base","link","script"]);function s7(t,n){return n==="src"&&o7.has(t)||n==="href"&&a7.has(t)||n==="xlink:href"&&t==="script"?yP:to}function pM(t,n,e){return s7(n,e)(t)}function fM(){let t=Le();return t&&t[ps].sanitizer}function xP(t){return t instanceof Function?t():t}function l7(t,n,e){let i=t.length;for(;;){let r=t.indexOf(n,e);if(r===-1)return r;if(r===0||t.charCodeAt(r-1)<=32){let o=n.length;if(r+o===i||t.charCodeAt(r+o)<=32)return r}e=r+1}}var CP="ng-template";function c7(t,n,e,i){let r=0;if(i){for(;r-1){let o;for(;++ro?p="":p=r[d+1].toLowerCase(),i&2&&c!==p){if(Va(i))return!1;a=!0}}}}return Va(i)||a}function Va(t){return(t&1)===0}function m7(t,n,e,i){if(n===null)return-1;let r=0;if(i||!e){let o=!1;for(;r-1)for(e++;e0?'="'+s+'"':"")+"]"}else i&8?r+="."+a:i&4&&(r+=" "+a);else r!==""&&!Va(a)&&(n+=lR(o,r),r=""),i=a,o=o||!Va(i);e++}return r!==""&&(n+=lR(o,r)),n}function b7(t){return t.map(_7).join(",")}function v7(t){let n=[],e=[],i=1,r=2;for(;inull),a=i;if(n&&typeof n=="object"){let l=n;r=l.next?.bind(l),o=l.error?.bind(l),a=l.complete?.bind(l)}this.__isAsync&&(o=this.wrapInTimeout(o),r&&(r=this.wrapInTimeout(r)),a&&(a=this.wrapInTimeout(a)));let s=super.subscribe({next:r,error:o,complete:a});return n instanceof ke&&n.add(s),s}wrapInTimeout(n){return e=>{let i=this.pendingTasks?.add();setTimeout(()=>{try{n(e)}finally{i!==void 0&&this.pendingTasks?.remove(i)}})}}},U=P1;function kP(t){let n,e;function i(){t=Rd;try{e!==void 0&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(e),n!==void 0&&clearTimeout(n)}catch{}}return n=setTimeout(()=>{t(),i()}),typeof requestAnimationFrame=="function"&&(e=requestAnimationFrame(()=>{t(),i()})),()=>i()}function dR(t){return queueMicrotask(()=>t()),()=>{t=Rd}}var xM="isAngularZone",e0=xM+"_ID",D7=0,ae=class t{hasPendingMacrotasks=!1;hasPendingMicrotasks=!1;isStable=!0;onUnstable=new U(!1);onMicrotaskEmpty=new U(!1);onStable=new U(!1);onError=new U(!1);constructor(n){let{enableLongStackTrace:e=!1,shouldCoalesceEventChangeDetection:i=!1,shouldCoalesceRunChangeDetection:r=!1,scheduleInRootZone:o=SP}=n;if(typeof Zone>"u")throw new ue(908,!1);Zone.assertZonePatched();let a=this;a._nesting=0,a._outer=a._inner=Zone.current,Zone.TaskTrackingZoneSpec&&(a._inner=a._inner.fork(new Zone.TaskTrackingZoneSpec)),e&&Zone.longStackTraceZoneSpec&&(a._inner=a._inner.fork(Zone.longStackTraceZoneSpec)),a.shouldCoalesceEventChangeDetection=!r&&i,a.shouldCoalesceRunChangeDetection=r,a.callbackScheduled=!1,a.scheduleInRootZone=o,S7(a)}static isInAngularZone(){return typeof Zone<"u"&&Zone.current.get(xM)===!0}static assertInAngularZone(){if(!t.isInAngularZone())throw new ue(909,!1)}static assertNotInAngularZone(){if(t.isInAngularZone())throw new ue(909,!1)}run(n,e,i){return this._inner.run(n,e,i)}runTask(n,e,i,r){let o=this._inner,a=o.scheduleEventTask("NgZoneEvent: "+r,n,M7,Rd,Rd);try{return o.runTask(a,e,i)}finally{o.cancelTask(a)}}runGuarded(n,e,i){return this._inner.runGuarded(n,e,i)}runOutsideAngular(n){return this._outer.run(n)}},M7={};function CM(t){if(t._nesting==0&&!t.hasPendingMicrotasks&&!t.isStable)try{t._nesting++,t.onMicrotaskEmpty.emit(null)}finally{if(t._nesting--,!t.hasPendingMicrotasks)try{t.runOutsideAngular(()=>t.onStable.emit(null))}finally{t.isStable=!0}}}function E7(t){if(t.isCheckStableRunning||t.callbackScheduled)return;t.callbackScheduled=!0;function n(){kP(()=>{t.callbackScheduled=!1,F1(t),t.isCheckStableRunning=!0,CM(t),t.isCheckStableRunning=!1})}t.scheduleInRootZone?Zone.root.run(()=>{n()}):t._outer.run(()=>{n()}),F1(t)}function S7(t){let n=()=>{E7(t)},e=D7++;t._inner=t._inner.fork({name:"angular",properties:{[xM]:!0,[e0]:e,[e0+e]:!0},onInvokeTask:(i,r,o,a,s,l)=>{if(k7(l))return i.invokeTask(o,a,s,l);try{return uR(t),i.invokeTask(o,a,s,l)}finally{(t.shouldCoalesceEventChangeDetection&&a.type==="eventTask"||t.shouldCoalesceRunChangeDetection)&&n(),mR(t)}},onInvoke:(i,r,o,a,s,l,c)=>{try{return uR(t),i.invoke(o,a,s,l,c)}finally{t.shouldCoalesceRunChangeDetection&&!t.callbackScheduled&&!T7(l)&&n(),mR(t)}},onHasTask:(i,r,o,a)=>{i.hasTask(o,a),r===o&&(a.change=="microTask"?(t._hasPendingMicrotasks=a.microTask,F1(t),CM(t)):a.change=="macroTask"&&(t.hasPendingMacrotasks=a.macroTask))},onHandleError:(i,r,o,a)=>(i.handleError(o,a),t.runOutsideAngular(()=>t.onError.emit(a)),!1)})}function F1(t){t._hasPendingMicrotasks||(t.shouldCoalesceEventChangeDetection||t.shouldCoalesceRunChangeDetection)&&t.callbackScheduled===!0?t.hasPendingMicrotasks=!0:t.hasPendingMicrotasks=!1}function uR(t){t._nesting++,t.isStable&&(t.isStable=!1,t.onUnstable.emit(null))}function mR(t){t._nesting--,CM(t)}var Gp=class{hasPendingMicrotasks=!1;hasPendingMacrotasks=!1;isStable=!0;onUnstable=new U;onMicrotaskEmpty=new U;onStable=new U;onError=new U;run(n,e,i){return n.apply(e,i)}runGuarded(n,e,i){return n.apply(e,i)}runOutsideAngular(n){return n()}runTask(n,e,i,r){return n.apply(e,i)}};function k7(t){return TP(t,"__ignore_ng_zone__")}function T7(t){return TP(t,"__scheduler_tick__")}function TP(t,n){return!Array.isArray(t)||t.length!==1?!1:t[0]?.data?.[n]===!0}var v0=(()=>{class t{impl=null;execute(){this.impl?.execute()}static \u0275prov=R({token:t,providedIn:"root",factory:()=>new t})}return t})(),wM=[0,1,2,3],DM=(()=>{class t{ngZone=u(ae);scheduler=u(Ho);errorHandler=u(Ln,{optional:!0});sequences=new Set;deferredRegistrations=new Set;executing=!1;constructor(){u(mc,{optional:!0})}execute(){let e=this.sequences.size>0;e&&hi(16),this.executing=!0;for(let i of wM)for(let r of this.sequences)if(!(r.erroredOrDestroyed||!r.hooks[i]))try{r.pipelinedValue=this.ngZone.runOutsideAngular(()=>this.maybeTrace(()=>{let o=r.hooks[i];return o(r.pipelinedValue)},r.snapshot))}catch(o){r.erroredOrDestroyed=!0,this.errorHandler?.handleError(o)}this.executing=!1;for(let i of this.sequences)i.afterRun(),i.once&&(this.sequences.delete(i),i.destroy());for(let i of this.deferredRegistrations)this.sequences.add(i);this.deferredRegistrations.size>0&&this.scheduler.notify(7),this.deferredRegistrations.clear(),e&&hi(17)}register(e){let{view:i}=e;i!==void 0?((i[kd]??=[]).push(e),ac(i),i[nt]|=8192):this.executing?this.deferredRegistrations.add(e):this.addSequence(e)}addSequence(e){this.sequences.add(e),this.scheduler.notify(7)}unregister(e){this.executing&&this.sequences.has(e)?(e.erroredOrDestroyed=!0,e.pipelinedValue=void 0,e.once=!0):(this.sequences.delete(e),this.deferredRegistrations.delete(e))}maybeTrace(e,i){return i?i.run(b0.AFTER_NEXT_RENDER,e):e()}static \u0275prov=R({token:t,providedIn:"root",factory:()=>new t})}return t})(),qp=class{impl;hooks;view;once;snapshot;erroredOrDestroyed=!1;pipelinedValue=void 0;unregisterOnDestroy;constructor(n,e,i,r,o,a=null){this.impl=n,this.hooks=e,this.view=i,this.once=r,this.snapshot=a,this.unregisterOnDestroy=o?.onDestroy(()=>this.destroy())}afterRun(){this.erroredOrDestroyed=!1,this.pipelinedValue=void 0,this.snapshot?.dispose(),this.snapshot=null}destroy(){this.impl.unregister(this),this.unregisterOnDestroy?.();let n=this.view?.[kd];n&&(this.view[kd]=n.filter(e=>e!==this))}};function vt(t,n){let e=n?.injector??u(de);return Es("NgAfterNextRender"),A7(t,e,n,!0)}function I7(t){return t instanceof Function?[void 0,void 0,t,void 0]:[t.earlyRead,t.write,t.mixedReadWrite,t.read]}function A7(t,n,e,i){let r=n.get(v0);r.impl??=n.get(DM);let o=n.get(mc,null,{optional:!0}),a=e?.manualCleanup!==!0?n.get(ln):null,s=n.get(Od,null,{optional:!0}),l=new qp(r.impl,I7(t),s?.view,i,a,o?.snapshot(null));return r.impl.register(l),l}var IP=new O("",{providedIn:"root",factory:()=>({queue:new Set,isScheduled:!1,scheduler:null})});function AP(t,n,e){let i=t.get(IP);if(Array.isArray(n))for(let r of n)i.queue.add(r),e?.detachedLeaveAnimationFns?.push(r);else i.queue.add(n),e?.detachedLeaveAnimationFns?.push(n);i.scheduler&&i.scheduler(t)}function O7(t,n){let e=t.get(IP);if(n.detachedLeaveAnimationFns){for(let i of n.detachedLeaveAnimationFns)e.queue.delete(i);n.detachedLeaveAnimationFns=void 0}}function R7(t,n){for(let[e,i]of n)AP(t,i.animateFns)}function hR(t,n,e,i){let r=t?.[Td]?.enter;n!==null&&r&&r.has(e.index)&&R7(i,r)}function um(t,n,e,i,r,o,a,s){if(r!=null){let l,c=!1;Fa(r)?l=r:gs(r)&&(c=!0,r=r[Pa]);let d=Go(r);t===0&&i!==null?(hR(s,i,o,e),a==null?_P(n,i,d):Jb(n,i,d,a||null,!0)):t===1&&i!==null?(hR(s,i,o,e),Jb(n,i,d,a||null,!0)):t===2?pR(s,o,e,p=>{bP(n,d,c,p)}):t===3&&pR(s,o,e,()=>{n.destroyNode(d)}),l!=null&&$7(n,t,e,l,o,i,a)}}function P7(t,n){OP(t,n),n[Pa]=null,n[Br]=null}function F7(t,n,e,i,r,o){i[Pa]=r,i[Br]=n,x0(t,i,e,1,r,o)}function OP(t,n){n[ps].changeDetectionScheduler?.notify(9),x0(t,n,n[mi],2,null,null)}function N7(t){let n=t[nm];if(!n)return d1(t[We],t);for(;n;){let e=null;if(gs(n))e=n[nm];else{let i=n[tn];i&&(e=i)}if(!e){for(;n&&!n[Wo]&&n!==t;)gs(n)&&d1(n[We],n),n=n[wn];n===null&&(n=t),gs(n)&&d1(n[We],n),e=n&&n[Wo]}n=e}}function MM(t,n){let e=t[Id],i=e.indexOf(n);e.splice(i,1)}function y0(t,n){if(Ad(n))return;let e=n[mi];e.destroyNode&&x0(t,n,e,3,null,null),N7(n)}function d1(t,n){if(Ad(n))return;let e=tt(null);try{n[nt]&=-129,n[nt]|=256,n[wo]&&ql(n[wo]),B7(t,n),V7(t,n),n[We].type===1&&n[mi].destroy();let i=n[rc];if(i!==null&&Fa(n[wn])){i!==n[wn]&&MM(i,n);let r=n[fs];r!==null&&r.detachView(t)}C1(n)}finally{tt(e)}}function pR(t,n,e,i){let r=t?.[Td];if(r==null||r.leave==null||!r.leave.has(n.index))return i(!1);t&&Nd.add(t),AP(e,()=>{if(r.leave&&r.leave.has(n.index)){let a=r.leave.get(n.index),s=[];if(a){for(let l=0;l{t[Td].running=void 0,Nd.delete(t),n(!0)});return}n(!1)}function V7(t,n){let e=t.cleanup,i=n[im];if(e!==null)for(let a=0;a=0?i[s]():i[-s].unsubscribe(),a+=2}else{let s=i[e[a+1]];e[a].call(s)}i!==null&&(n[im]=null);let r=n[al];if(r!==null){n[al]=null;for(let a=0;avi&&EP(t,n,vi,!1),hi(a?2:0,r,e),e(i,r)}finally{sc(o),hi(a?3:1,r,e)}}function C0(t,n,e){K7(t,n,e),(e.flags&64)===64&&Z7(t,n,e)}function Cm(t,n,e=qo){let i=n.localNames;if(i!==null){let r=n.index+1;for(let o=0;onull;function Q7(t){return t==="class"?"className":t==="for"?"htmlFor":t==="formaction"?"formAction":t==="innerHtml"?"innerHTML":t==="readonly"?"readOnly":t==="tabindex"?"tabIndex":t}function VP(t,n,e,i,r,o){let a=n[We];if(M0(t,a,n,e,i)){dl(t)&&jP(n,t.index);return}t.type&3&&(e=Q7(e)),BP(t,n,e,i,r,o)}function BP(t,n,e,i,r,o){if(t.type&3){let a=qo(t,n);i=o!=null?o(i,t.value||"",e):i,r.setProperty(a,e,i)}else t.type&12}function jP(t,n){let e=Yo(n,t);e[nt]&16||(e[nt]|=64)}function K7(t,n,e){let i=e.directiveStart,r=e.directiveEnd;dl(e)&&C7(n,e,t.data[i+e.componentOffset]),t.firstCreatePass||Xb(e,n);let o=e.initialInputs;for(let a=i;a{ac(t.lView)},consumerOnSignalRead(){this.lView[wo]=this}});function c9(t){let n=t[wo]??Object.create(d9);return n.lView=t,n}var d9=Me(I({},dd),{consumerIsAlwaysLive:!0,kind:"template",consumerMarkedDirty:t=>{let n=nc(t.lView);for(;n&&!WP(n[We]);)n=nc(n);n&&zD(n)},consumerOnSignalRead(){this.lView[wo]=this}});function WP(t){return t.type!==2}function GP(t){if(t[cl]===null)return;let n=!0;for(;n;){let e=!1;for(let i of t[cl])i.dirty&&(e=!0,i.zone===null||Zone.current===i.zone?i.run():i.zone.run(()=>i.run()));n=e&&!!(t[nt]&8192)}}var u9=100;function qP(t,n=0){let i=t[ps].rendererFactory,r=!1;r||i.begin?.();try{m9(t,n)}finally{r||i.end?.()}}function m9(t,n){let e=JD();try{lm(!0),L1(t,n);let i=0;for(;Lp(t);){if(i===u9)throw new ue(103,!1);i++,L1(t,1)}}finally{lm(e)}}function h9(t,n,e,i){if(Ad(n))return;let r=n[nt],o=!1,a=!1;Rb(n);let s=!0,l=null,c=null;o||(WP(t)?(c=o9(n),l=Gl(c)):N_()===null?(s=!1,c=c9(n),l=Gl(c)):n[wo]&&(ql(n[wo]),n[wo]=null));try{HD(n),LO(t.bindingStartIndex),e!==null&&LP(t,n,e,2,i);let d=(r&3)===3;if(!o)if(d){let b=t.preOrderCheckHooks;b!==null&&Ub(n,b,null)}else{let b=t.preOrderHooks;b!==null&&$b(n,b,0,null),l1(n,0)}if(a||p9(n),GP(n),YP(n,0),t.contentQueries!==null&&lP(t,n),!o)if(d){let b=t.contentCheckHooks;b!==null&&Ub(n,b)}else{let b=t.contentHooks;b!==null&&$b(n,b,1),l1(n,1)}g9(t,n);let p=t.components;p!==null&&KP(n,p,0);let _=t.viewQuery;if(_!==null&&D1(2,_,i),!o)if(d){let b=t.viewCheckHooks;b!==null&&Ub(n,b)}else{let b=t.viewHooks;b!==null&&$b(n,b,2),l1(n,2)}if(t.firstUpdatePass===!0&&(t.firstUpdatePass=!1),n[Sb]){for(let b of n[Sb])b();n[Sb]=null}o||(UP(n),n[nt]&=-73)}catch(d){throw o||ac(n),d}finally{c!==null&&(ud(c,l),s&&s9(c)),Pb()}}function YP(t,n){for(let e=eP(t);e!==null;e=tP(e))for(let i=tn;i0&&(t[e-1][Wo]=i[Wo]);let o=Ap(t,tn+n);P7(i[We],i);let a=o[fs];a!==null&&a.detachView(o[We]),i[wn]=null,i[Wo]=null,i[nt]&=-129}return i}function _9(t,n,e,i){let r=tn+i,o=e.length;i>0&&(e[r-1][Wo]=n),i-1&&(Qp(n,i),Ap(e,i))}this._attachedToViewContainer=!1}y0(this._lView[We],this._lView)}onDestroy(n){UD(this._lView,n)}markForCheck(){IM(this._cdRefInjectingView||this._lView,4)}detach(){this._lView[nt]&=-129}reattach(){Tb(this._lView),this._lView[nt]|=128}detectChanges(){this._lView[nt]|=1024,qP(this._lView)}checkNoChanges(){}attachToViewContainerRef(){if(this._appRef)throw new ue(902,!1);this._attachedToViewContainer=!0}detachFromAppRef(){this._appRef=null;let n=om(this._lView),e=this._lView[rc];e!==null&&!n&&MM(e,this._lView),OP(this._lView[We],this._lView)}attachToAppRef(n){if(this._attachedToViewContainer)throw new ue(902,!1);this._appRef=n;let e=om(this._lView),i=this._lView[rc];i!==null&&!e&&eF(i,this._lView),Tb(this._lView)}};var te=(()=>{class t{_declarationLView;_declarationTContainer;elementRef;static __NG_ELEMENT_ID__=b9;constructor(e,i,r){this._declarationLView=e,this._declarationTContainer=i,this.elementRef=r}get ssrId(){return this._declarationTContainer.tView?.ssrId||null}createEmbeddedView(e,i){return this.createEmbeddedViewImpl(e,i)}createEmbeddedViewImpl(e,i,r){let o=of(this._declarationLView,this._declarationTContainer,e,{embeddedViewInjector:i,dehydratedView:r});return new lc(o)}}return t})();function b9(){return E0(sn(),Le())}function E0(t,n){return t.type&4?new te(n,t,ym(t,n)):null}function wm(t,n,e,i,r){let o=t.data[n];if(o===null)o=v9(t,n,e,i,r),VO()&&(o.flags|=32);else if(o.type&64){o.type=e,o.value=i,o.attrs=r;let a=FO();o.injectorIndex=a===null?-1:a.injectorIndex}return sm(o,!0),o}function v9(t,n,e,i,r){let o=KD(),a=ZD(),s=a?o:o&&o.parent,l=t.data[n]=x9(t,s,e,n,i,r);return y9(t,l,o,a),l}function y9(t,n,e,i){t.firstChild===null&&(t.firstChild=n),e!==null&&(i?e.child==null&&n.parent!==null&&(e.child=n):e.next===null&&(e.next=n,n.prev=e))}function x9(t,n,e,i,r,o){let a=n?n.injectorIndex:-1,s=0;return qD()&&(s|=128),{type:e,index:i,insertBeforeIndex:null,injectorIndex:a,directiveStart:-1,directiveEnd:-1,directiveStylingLast:-1,componentOffset:-1,propertyBindings:null,flags:s,providerIndexes:0,value:r,attrs:o,mergedAttrs:null,localNames:null,initialInputs:null,inputs:null,hostDirectiveInputs:null,outputs:null,hostDirectiveOutputs:null,directiveToIndex:null,tView:null,next:null,prev:null,projectionNext:null,child:null,parent:n,projection:null,styles:null,stylesWithoutHost:null,residualStyles:void 0,classes:null,classesWithoutHost:null,residualClasses:void 0,classBindings:0,styleBindings:0}}var kAe=new RegExp(`^(\\d+)*(${TW}|${kW})*(.*)`);function C9(t){let n=t[ND]??[],i=t[wn][mi],r=[];for(let o of n)o.data[rP]!==void 0?r.push(o):w9(o,i);t[ND]=r}function w9(t,n){let e=0,i=t.firstChild;if(i){let r=t.data[nP];for(;enull,M9=()=>null;function t0(t,n){return D9(t,n)}function tF(t,n,e){return M9(t,n,e)}var Bd=class{},S0=class{},V1=class{resolveComponentFactory(n){throw new ue(917,!1)}},sf=class{static NULL=new V1},hn=class{},ze=(()=>{class t{destroyNode=null;static __NG_ELEMENT_ID__=()=>E9()}return t})();function E9(){let t=Le(),n=sn(),e=Yo(n.index,t);return(gs(e)?e:t)[mi]}var iF=(()=>{class t{static \u0275prov=R({token:t,providedIn:"root",factory:()=>null})}return t})();var Gb={},B1=class{injector;parentInjector;constructor(n,e){this.injector=n,this.parentInjector=e}get(n,e,i){let r=this.injector.get(n,Gb,i);return r!==Gb||e===Gb?r:this.parentInjector.get(n,e,i)}};function i0(t,n,e){let i=e?t.styles:null,r=e?t.classes:null,o=0;if(n!==null)for(let a=0;a0&&(e.directiveToIndex=new Map);for(let _=0;_0;){let e=t[--n];if(typeof e=="number"&&e<0)return e}return 0}function P9(t,n,e){if(e){if(n.exportAs)for(let i=0;ii(Go(w[t.index])):t.index;dF(y,n,e,o,s,b,!1)}}return c}function V9(t){return t.startsWith("animation")||t.startsWith("transition")}function B9(t,n,e,i){let r=t.cleanup;if(r!=null)for(let o=0;ol?s[l]:null}typeof a=="string"&&(o+=2)}return null}function dF(t,n,e,i,r,o,a){let s=n.firstCreatePass?WD(n):null,l=$D(e),c=l.length;l.push(r,o),s&&s.push(i,t,c,(c+1)*(a?-1:1))}function yR(t,n,e,i,r,o){let a=n[e],s=n[We],c=s.data[e].outputs[i],p=a[c].subscribe(o);dF(t.index,s,n,r,o,p,!0)}var j1=Symbol("BINDING");var n0=class extends sf{ngModule;constructor(n){super(),this.ngModule=n}resolveComponentFactory(n){let e=hs(n);return new cc(e,this.ngModule)}};function j9(t){return Object.keys(t).map(n=>{let[e,i,r]=t[n],o={propName:e,templateName:n,isSignal:(i&_0.SignalBased)!==0};return r&&(o.transform=r),o})}function H9(t){return Object.keys(t).map(n=>({propName:t[n],templateName:n}))}function z9(t,n,e){let i=n instanceof ti?n:n?.injector;return i&&t.getStandaloneInjector!==null&&(i=t.getStandaloneInjector(i)||i),i?new B1(e,i):e}function U9(t){let n=t.get(hn,null);if(n===null)throw new ue(407,!1);let e=t.get(iF,null),i=t.get(Ho,null);return{rendererFactory:n,sanitizer:e,changeDetectionScheduler:i,ngReflect:!1}}function $9(t,n){let e=uF(t);return gP(n,e,e==="svg"?VD:e==="math"?SO:null)}function uF(t){return(t.selectors[0][0]||"div").toLowerCase()}var cc=class extends S0{componentDef;ngModule;selector;componentType;ngContentSelectors;isBoundToModule;cachedInputs=null;cachedOutputs=null;get inputs(){return this.cachedInputs??=j9(this.componentDef.inputs),this.cachedInputs}get outputs(){return this.cachedOutputs??=H9(this.componentDef.outputs),this.cachedOutputs}constructor(n,e){super(),this.componentDef=n,this.ngModule=e,this.componentType=n.type,this.selector=b7(n.selectors),this.ngContentSelectors=n.ngContentSelectors??[],this.isBoundToModule=!!e}create(n,e,i,r,o,a){hi(22);let s=tt(null);try{let l=this.componentDef,c=W9(i,l,a,o),d=z9(l,r||this.ngModule,n),p=U9(d),_=p.rendererFactory.createRenderer(null,l),b=i?G7(_,i,l.encapsulation,d):$9(l,_),y=a?.some(xR)||o?.some(D=>typeof D!="function"&&D.bindings.some(xR)),w=bM(null,c,null,512|DP(l),null,null,p,_,d,null,sP(b,d,!0));w[vi]=b,Rb(w);let C=null;try{let D=AM(vi,w,2,"#host",()=>c.directiveRegistry,!0,0);vP(_,b,D),pm(b,w),C0(c,w,D),lM(c,D,w),OM(c,D),e!==void 0&&q9(D,this.ngContentSelectors,e),C=Yo(D.index,w),w[en]=C[en],TM(c,w,null)}catch(D){throw C!==null&&C1(C),C1(w),D}finally{hi(23),Pb()}return new r0(this.componentType,w,!!y)}finally{tt(s)}}};function W9(t,n,e,i){let r=t?["ng-version","20.3.16"]:v7(n.selectors[0]),o=null,a=null,s=0;if(e)for(let d of e)s+=d[j1].requiredVars,d.create&&(d.targetIdx=0,(o??=[]).push(d)),d.update&&(d.targetIdx=0,(a??=[]).push(d));if(i)for(let d=0;d{if(e&1&&t)for(let i of t)i.create();if(e&2&&n)for(let i of n)i.update()}}function xR(t){let n=t[j1].kind;return n==="input"||n==="twoWay"}var r0=class extends Bd{_rootLView;_hasInputBindings;instance;hostView;changeDetectorRef;componentType;location;previousInputValues=null;_tNode;constructor(n,e,i){super(),this._rootLView=e,this._hasInputBindings=i,this._tNode=Np(e[We],vi),this.location=ym(this._tNode,e),this.instance=Yo(this._tNode.index,e)[en],this.hostView=this.changeDetectorRef=new lc(e,void 0),this.componentType=n}setInput(n,e){this._hasInputBindings;let i=this._tNode;if(this.previousInputValues??=new Map,this.previousInputValues.has(n)&&Object.is(this.previousInputValues.get(n),e))return;let r=this._rootLView,o=M0(i,r[We],r,n,e);this.previousInputValues.set(n,e);let a=Yo(i.index,r);IM(a,1)}get injector(){return new Pd(this._tNode,this._rootLView)}destroy(){this.hostView.destroy()}onDestroy(n){this.hostView.onDestroy(n)}};function q9(t,n,e){let i=t.projection=[];for(let r=0;r{class t{static __NG_ELEMENT_ID__=Y9}return t})();function Y9(){let t=sn();return hF(t,Le())}var Q9=st,mF=class extends Q9{_lContainer;_hostTNode;_hostLView;constructor(n,e,i){super(),this._lContainer=n,this._hostTNode=e,this._hostLView=i}get element(){return ym(this._hostTNode,this._hostLView)}get injector(){return new Pd(this._hostTNode,this._hostLView)}get parentInjector(){let n=rM(this._hostTNode,this._hostLView);if(zR(n)){let e=Kb(n,this._hostLView),i=Qb(n),r=e[We].data[i+8];return new Pd(r,e)}else return new Pd(null,this._hostLView)}clear(){for(;this.length>0;)this.remove(this.length-1)}get(n){let e=CR(this._lContainer);return e!==null&&e[n]||null}get length(){return this._lContainer.length-tn}createEmbeddedView(n,e,i){let r,o;typeof i=="number"?r=i:i!=null&&(r=i.index,o=i.injector);let a=t0(this._lContainer,n.ssrId),s=n.createEmbeddedViewImpl(e||{},o,a);return this.insertImpl(s,r,fm(this._hostTNode,a)),s}createComponent(n,e,i,r,o,a,s){let l=n&&!nW(n),c;if(l)c=e;else{let C=e||{};c=C.index,i=C.injector,r=C.projectableNodes,o=C.environmentInjector||C.ngModuleRef,a=C.directives,s=C.bindings}let d=l?n:new cc(hs(n)),p=i||this.parentInjector;if(!o&&d.ngModule==null){let D=(l?p:this.parentInjector).get(ti,null);D&&(o=D)}let _=hs(d.componentType??{}),b=t0(this._lContainer,_?.id??null),y=b?.firstChild??null,w=d.create(p,r,y,o,a,s);return this.insertImpl(w.hostView,c,fm(this._hostTNode,b)),w}insert(n,e){return this.insertImpl(n,e,!0)}insertImpl(n,e,i){let r=n._lView;if(TO(r)){let s=this.indexOf(n);if(s!==-1)this.detach(s);else{let l=r[wn],c=new mF(l,l[Br],l[wn]);c.detach(c.indexOf(n))}}let o=this._adjustIndex(e),a=this._lContainer;return af(a,r,o,i),n.attachToViewContainerRef(),SD(u1(a),o,n),n}move(n,e){return this.insert(n,e)}indexOf(n){let e=CR(this._lContainer);return e!==null?e.indexOf(n):-1}remove(n){let e=this._adjustIndex(n,-1),i=Qp(this._lContainer,e);i&&(Ap(u1(this._lContainer),e),y0(i[We],i))}detach(n){let e=this._adjustIndex(n,-1),i=Qp(this._lContainer,e);return i&&Ap(u1(this._lContainer),e)!=null?new lc(i):null}_adjustIndex(n,e=0){return n??this.length+e}};function CR(t){return t[Fp]}function u1(t){return t[Fp]||(t[Fp]=[])}function hF(t,n){let e,i=n[t.index];return Fa(i)?e=i:(e=ZP(i,n,null,t),n[t.index]=e,vM(n,e)),Z9(e,n,t,i),new mF(e,t,n)}function K9(t,n){let e=t[mi],i=e.createComment(""),r=qo(n,t),o=e.parentNode(r);return Jb(e,o,i,e.nextSibling(r),!1),i}var Z9=eG,X9=()=>!1;function J9(t,n,e){return X9(t,n,e)}function eG(t,n,e,i){if(t[oc])return;let r;e.type&8?r=Go(i):r=K9(n,e),t[oc]=r}var H1=class t{queryList;matches=null;constructor(n){this.queryList=n}clone(){return new t(this.queryList)}setDirty(){this.queryList.setDirty()}},z1=class t{queries;constructor(n=[]){this.queries=n}createEmbeddedView(n){let e=n.queries;if(e!==null){let i=n.contentQueries!==null?n.contentQueries[0]:e.length,r=[];for(let o=0;o0)i.push(a[s/2]);else{let c=o[s+1],d=n[-l];for(let p=tn;pn.trim())}function bF(t,n,e){t.queries===null&&(t.queries=new U1),t.queries.track(new $1(n,e))}function aG(t,n){let e=t.contentQueries||(t.contentQueries=[]),i=e.length?e[e.length-1]:-1;n!==i&&e.push(t.queries.length-1,n)}function FM(t,n){return t.queries.getByIndex(n)}function vF(t,n){let e=t[We],i=FM(e,n);return i.crossesNgTemplate?W1(e,t,n,[]):pF(e,t,i,n)}function NM(t,n,e){let i,r=yp(()=>{i._dirtyCounter();let o=sG(i,t);if(n&&o===void 0)throw new ue(-951,!1);return o});return i=r[mn],i._dirtyCounter=he(0),i._flatValue=void 0,r}function LM(t){return NM(!0,!1,t)}function VM(t){return NM(!0,!0,t)}function yF(t){return NM(!1,!1,t)}function xF(t,n){let e=t[mn];e._lView=Le(),e._queryIndex=n,e._queryList=PM(e._lView,n),e._queryList.onDirty(()=>e._dirtyCounter.update(i=>i+1))}function sG(t,n){let e=t._lView,i=t._queryIndex;if(e===void 0||i===void 0||e[nt]&4)return n?void 0:xr;let r=PM(e,i),o=vF(e,i);return r.reset(o,ZR),n?r.first:r._changesDetected||t._flatValue===void 0?t._flatValue=r.toArray():t._flatValue}var ws=class{},k0=class{};var a0=class extends ws{ngModuleType;_parent;_bootstrapComponents=[];_r3Injector;instance;destroyCbs=[];componentFactoryResolver=new n0(this);constructor(n,e,i,r=!0){super(),this.ngModuleType=n,this._parent=e;let o=ID(n);this._bootstrapComponents=xP(o.bootstrap),this._r3Injector=n1(n,e,[{provide:ws,useValue:this},{provide:sf,useValue:this.componentFactoryResolver},...i],sl(n),new Set(["environment"])),r&&this.resolveInjectorInitializers()}resolveInjectorInitializers(){this._r3Injector.resolveInjectorInitializers(),this.instance=this._r3Injector.get(this.ngModuleType)}get injector(){return this._r3Injector}destroy(){let n=this._r3Injector;!n.destroyed&&n.destroy(),this.destroyCbs.forEach(e=>e()),this.destroyCbs=null}onDestroy(n){this.destroyCbs.push(n)}},s0=class extends k0{moduleType;constructor(n){super(),this.moduleType=n}create(n){return new a0(this.moduleType,n,[])}};var Zp=class extends ws{injector;componentFactoryResolver=new n0(this);instance=null;constructor(n){super();let e=new wd([...n.providers,{provide:ws,useValue:this},{provide:sf,useValue:this.componentFactoryResolver}],n.parent||tm(),n.debugName,new Set(["environment"]));this.injector=e,n.runEnvironmentInitializers&&e.resolveInjectorInitializers()}destroy(){this.injector.destroy()}onDestroy(n){this.injector.onDestroy(n)}};function Dm(t,n,e=null){return new Zp({providers:t,parent:n,debugName:e,runEnvironmentInitializers:!0}).injector}var lG=(()=>{class t{_injector;cachedInjectors=new Map;constructor(e){this._injector=e}getOrCreateStandaloneInjector(e){if(!e.standalone)return null;if(!this.cachedInjectors.has(e)){let i=AD(!1,e.type),r=i.length>0?Dm([i],this._injector,`Standalone[${e.type.name}]`):null;this.cachedInjectors.set(e,r)}return this.cachedInjectors.get(e)}ngOnDestroy(){try{for(let e of this.cachedInjectors.values())e!==null&&e.destroy()}finally{this.cachedInjectors.clear()}}static \u0275prov=R({token:t,providedIn:"environment",factory:()=>new t(pe(ti))})}return t})();function E(t){return vm(()=>{let n=CF(t),e=Me(I({},n),{decls:t.decls,vars:t.vars,template:t.template,consts:t.consts||null,ngContentSelectors:t.ngContentSelectors,onPush:t.changeDetection===oM.OnPush,directiveDefs:null,pipeDefs:null,dependencies:n.standalone&&t.dependencies||null,getStandaloneInjector:n.standalone?r=>r.get(lG).getOrCreateStandaloneInjector(e):null,getExternalStyles:null,signals:t.signals??!1,data:t.data||{},encapsulation:t.encapsulation||ul.Emulated,styles:t.styles||xr,_:null,schemas:t.schemas||null,tView:null,id:""});n.standalone&&Es("NgStandalone"),wF(e);let i=t.dependencies;return e.directiveDefs=wR(i,cG),e.pipeDefs=wR(i,yO),e.id=mG(e),e})}function cG(t){return hs(t)||Mb(t)}function ee(t){return vm(()=>({type:t.type,bootstrap:t.bootstrap||xr,declarations:t.declarations||xr,imports:t.imports||xr,exports:t.exports||xr,transitiveCompileScopes:null,schemas:t.schemas||null,id:t.id||null}))}function dG(t,n){if(t==null)return Ra;let e={};for(let i in t)if(t.hasOwnProperty(i)){let r=t[i],o,a,s,l;Array.isArray(r)?(s=r[0],o=r[1],a=r[2]??o,l=r[3]||null):(o=r,a=r,s=_0.None,l=null),e[o]=[i,s,l],n[o]=a}return e}function uG(t){if(t==null)return Ra;let n={};for(let e in t)t.hasOwnProperty(e)&&(n[t[e]]=e);return n}function P(t){return vm(()=>{let n=CF(t);return wF(n),n})}function io(t){return{type:t.type,name:t.name,factory:null,pure:t.pure!==!1,standalone:t.standalone??!0,onDestroy:t.type.prototype.ngOnDestroy||null}}function CF(t){let n={};return{type:t.type,providersResolver:null,factory:null,hostBindings:t.hostBindings||null,hostVars:t.hostVars||0,hostAttrs:t.hostAttrs||null,contentQueries:t.contentQueries||null,declaredInputs:n,inputConfig:t.inputs||Ra,exportAs:t.exportAs||null,standalone:t.standalone??!0,signals:t.signals===!0,selectors:t.selectors||xr,viewQuery:t.viewQuery||null,features:t.features||null,setInput:null,resolveHostDirectives:null,hostDirectives:null,inputs:dG(t.inputs,n),outputs:uG(t.outputs),debugInfo:null}}function wF(t){t.features?.forEach(n=>n(t))}function wR(t,n){return t?()=>{let e=typeof t=="function"?t():t,i=[];for(let r of e){let o=n(r);o!==null&&i.push(o)}return i}:null}function mG(t){let n=0,e=typeof t.consts=="function"?"":t.consts,i=[t.selectors,t.ngContentSelectors,t.hostVars,t.hostAttrs,e,t.vars,t.decls,t.encapsulation,t.standalone,t.signals,t.exportAs,JSON.stringify(t.inputs),JSON.stringify(t.outputs),Object.getOwnPropertyNames(t.type.prototype),!!t.contentQueries,!!t.viewQuery];for(let o of i.join("|"))n=Math.imul(31,n)+o.charCodeAt(0)<<0;return n+=2147483648,"c"+n}function hG(t){return Object.getPrototypeOf(t.prototype).constructor}function le(t){let n=hG(t.type),e=!0,i=[t];for(;n;){let r;if(Na(t))r=n.\u0275cmp||n.\u0275dir;else{if(n.\u0275cmp)throw new ue(903,!1);r=n.\u0275dir}if(r){if(e){i.push(r);let a=t;a.inputs=m1(t.inputs),a.declaredInputs=m1(t.declaredInputs),a.outputs=m1(t.outputs);let s=r.hostBindings;s&&bG(t,s);let l=r.viewQuery,c=r.contentQueries;if(l&&gG(t,l),c&&_G(t,c),pG(t,r),uO(t.outputs,r.outputs),Na(r)&&r.data.animation){let d=t.data;d.animation=(d.animation||[]).concat(r.data.animation)}}let o=r.features;if(o)for(let a=0;a=0;i--){let r=t[i];r.hostVars=n+=r.hostVars,r.hostAttrs=hm(r.hostAttrs,e=hm(e,r.hostAttrs))}}function m1(t){return t===Ra?{}:t===xr?[]:t}function gG(t,n){let e=t.viewQuery;e?t.viewQuery=(i,r)=>{n(i,r),e(i,r)}:t.viewQuery=n}function _G(t,n){let e=t.contentQueries;e?t.contentQueries=(i,r,o)=>{n(i,r,o),e(i,r,o)}:t.contentQueries=n}function bG(t,n){let e=t.hostBindings;e?t.hostBindings=(i,r)=>{n(i,r),e(i,r)}:t.hostBindings=n}function Mm(t){let n=e=>{let i=Array.isArray(t);e.hostDirectives===null?(e.resolveHostDirectives=vG,e.hostDirectives=i?t.map(G1):[t]):i?e.hostDirectives.unshift(...t.map(G1)):e.hostDirectives.unshift(t)};return n.ngInherit=!0,n}function vG(t){let n=[],e=!1,i=null,r=null;for(let o=0;o{class t{log(e){console.log(e)}warn(e){console.warn(e)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"platform"})}return t})();var BM=new O("");function fl(t){return!!t&&typeof t.then=="function"}function I0(t){return!!t&&typeof t.subscribe=="function"}var jM=new O("");function hc(t){return Jr([{provide:jM,multi:!0,useValue:t}])}var HM=(()=>{class t{resolve;reject;initialized=!1;done=!1;donePromise=new Promise((e,i)=>{this.resolve=e,this.reject=i});appInits=u(jM,{optional:!0})??[];injector=u(de);constructor(){}runInitializers(){if(this.initialized)return;let e=[];for(let r of this.appInits){let o=Vn(this.injector,r);if(fl(o))e.push(o);else if(I0(o)){let a=new Promise((s,l)=>{o.subscribe({complete:s,error:l})});e.push(a)}}let i=()=>{this.done=!0,this.resolve()};Promise.all(e).then(()=>{i()}).catch(r=>{this.reject(r)}),e.length===0&&i(),this.initialized=!0}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),cf=new O("");function EF(){Vw(()=>{let t="";throw new ue(600,t)})}function SF(t){return t.isBoundToModule}var DG=10;var tr=(()=>{class t{_runningTick=!1;_destroyed=!1;_destroyListeners=[];_views=[];internalErrorHandler=u(wr);afterRenderManager=u(v0);zonelessEnabled=u(Hp);rootEffectScheduler=u(zp);dirtyFlags=0;tracingSnapshot=null;allTestViews=new Set;autoDetectTestViews=new Set;includeAllTestViews=!1;afterTick=new z;get allViews(){return[...(this.includeAllTestViews?this.allTestViews:this.autoDetectTestViews).keys(),...this._views]}get destroyed(){return this._destroyed}componentTypes=[];components=[];internalPendingTask=u(ys);get isStable(){return this.internalPendingTask.hasPendingTasksObservable.pipe(se(e=>!e))}constructor(){u(mc,{optional:!0})}whenStable(){let e;return new Promise(i=>{e=this.isStable.subscribe({next:r=>{r&&i()}})}).finally(()=>{e.unsubscribe()})}_injector=u(ti);_rendererFactory=null;get injector(){return this._injector}bootstrap(e,i){return this.bootstrapImpl(e,i)}bootstrapImpl(e,i,r=de.NULL){return this._injector.get(ae).run(()=>{hi(10);let a=e instanceof S0;if(!this._injector.get(HM).done){let y="";throw new ue(405,y)}let l;a?l=e:l=this._injector.get(sf).resolveComponentFactory(e),this.componentTypes.push(l.componentType);let c=SF(l)?void 0:this._injector.get(ws),d=i||l.selector,p=l.create(r,[],d,c),_=p.location.nativeElement,b=p.injector.get(BM,null);return b?.registerApplication(_),p.onDestroy(()=>{this.detachView(p.hostView),$p(this.components,p),b?.unregisterApplication(_)}),this._loadComponent(p),hi(11,p),p})}tick(){this.zonelessEnabled||(this.dirtyFlags|=1),this._tick()}_tick(){hi(12),this.tracingSnapshot!==null?this.tracingSnapshot.run(b0.CHANGE_DETECTION,this.tickImpl):this.tickImpl()}tickImpl=()=>{if(this._runningTick)throw new ue(101,!1);let e=tt(null);try{this._runningTick=!0,this.synchronize()}finally{this._runningTick=!1,this.tracingSnapshot?.dispose(),this.tracingSnapshot=null,tt(e),this.afterTick.next(),hi(13)}};synchronize(){this._rendererFactory===null&&!this._injector.destroyed&&(this._rendererFactory=this._injector.get(hn,null,{optional:!0}));let e=0;for(;this.dirtyFlags!==0&&e++Lp(e))){this.dirtyFlags|=2;return}else this.dirtyFlags&=-8}attachView(e){let i=e;this._views.push(i),i.attachToAppRef(this)}detachView(e){let i=e;$p(this._views,i),i.detachFromAppRef()}_loadComponent(e){this.attachView(e.hostView);try{this.tick()}catch(r){this.internalErrorHandler(r)}this.components.push(e),this._injector.get(cf,[]).forEach(r=>r(e))}ngOnDestroy(){if(!this._destroyed)try{this._destroyListeners.forEach(e=>e()),this._views.slice().forEach(e=>e.destroy())}finally{this._destroyed=!0,this._views=[],this._destroyListeners=[]}}onDestroy(e){return this._destroyListeners.push(e),()=>$p(this._destroyListeners,e)}destroy(){if(this._destroyed)throw new ue(406,!1);let e=this._injector;e.destroy&&!e.destroyed&&e.destroy()}get viewCount(){return this._views.length}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function $p(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}function pc(t,n){let e=Le(),i=bs();if(jr(e,i,n)){let r=Di(),o=cm();if(M0(o,r,e,t,n))dl(o)&&jP(e,o.index);else{let s=qo(o,e);HP(e[mi],s,null,o.value,t,n,null)}}return pc}function X(t,n,e,i){let r=Le(),o=bs();if(jr(r,o,n)){let a=Di(),s=cm();J7(s,r,t,n,e,i)}return X}var FAe=typeof document<"u"&&typeof document?.documentElement?.getAnimations=="function";function A0(){return Le()[Cr][en]}var q1=class{destroy(n){}updateValue(n,e){}swap(n,e){let i=Math.min(n,e),r=Math.max(n,e),o=this.detach(r);if(r-i>1){let a=this.detach(i);this.attach(i,o),this.attach(r,a)}else this.attach(i,o)}move(n,e){this.attach(e,this.detach(n))}};function h1(t,n,e,i,r){return t===e&&Object.is(n,i)?1:Object.is(r(t,n),r(e,i))?-1:0}function MG(t,n,e){let i,r,o=0,a=t.length-1,s=void 0;if(Array.isArray(n)){let l=n.length-1;for(;o<=a&&o<=l;){let c=t.at(o),d=n[o],p=h1(o,c,o,d,e);if(p!==0){p<0&&t.updateValue(o,d),o++;continue}let _=t.at(a),b=n[l],y=h1(a,_,l,b,e);if(y!==0){y<0&&t.updateValue(a,b),a--,l--;continue}let w=e(o,c),C=e(a,_),D=e(o,d);if(Object.is(D,C)){let F=e(l,b);Object.is(F,w)?(t.swap(o,a),t.updateValue(a,b),l--,a--):t.move(a,o),t.updateValue(o,d),o++;continue}if(i??=new l0,r??=SR(t,o,a,e),Y1(t,i,o,D))t.updateValue(o,d),o++,a++;else if(r.has(D))i.set(w,t.detach(o)),a--;else{let F=t.create(o,n[o]);t.attach(o,F),o++,a++}}for(;o<=l;)ER(t,i,e,o,n[o]),o++}else if(n!=null){let l=n[Symbol.iterator](),c=l.next();for(;!c.done&&o<=a;){let d=t.at(o),p=c.value,_=h1(o,d,o,p,e);if(_!==0)_<0&&t.updateValue(o,p),o++,c=l.next();else{i??=new l0,r??=SR(t,o,a,e);let b=e(o,p);if(Y1(t,i,o,b))t.updateValue(o,p),o++,a++,c=l.next();else if(!r.has(b))t.attach(o,t.create(o,p)),o++,a++,c=l.next();else{let y=e(o,d);i.set(y,t.detach(o)),a--}}}for(;!c.done;)ER(t,i,e,t.length,c.value),c=l.next()}for(;o<=a;)t.destroy(t.detach(a--));i?.forEach(l=>{t.destroy(l)})}function Y1(t,n,e,i){return n!==void 0&&n.has(i)?(t.attach(e,n.get(i)),n.delete(i),!0):!1}function ER(t,n,e,i,r){if(Y1(t,n,i,e(i,r)))t.updateValue(i,r);else{let o=t.create(i,r);t.attach(i,o)}}function SR(t,n,e,i){let r=new Set;for(let o=n;o<=e;o++)r.add(i(o,t.at(o)));return r}var l0=class{kvMap=new Map;_vMap=void 0;has(n){return this.kvMap.has(n)}delete(n){if(!this.has(n))return!1;let e=this.kvMap.get(n);return this._vMap!==void 0&&this._vMap.has(e)?(this.kvMap.set(n,this._vMap.get(e)),this._vMap.delete(e)):this.kvMap.delete(n),!0}get(n){return this.kvMap.get(n)}set(n,e){if(this.kvMap.has(n)){let i=this.kvMap.get(n);this._vMap===void 0&&(this._vMap=new Map);let r=this._vMap;for(;r.has(i);)i=r.get(i);r.set(i,e)}else this.kvMap.set(n,e)}forEach(n){for(let[e,i]of this.kvMap)if(n(i,e),this._vMap!==void 0){let r=this._vMap;for(;r.has(i);)i=r.get(i),n(i,e)}}};function L(t,n,e,i,r,o,a,s){Es("NgControlFlow");let l=Le(),c=Di(),d=Do(c.consts,o);return gm(l,c,t,n,e,i,r,d,256,a,s),zM}function zM(t,n,e,i,r,o,a,s){Es("NgControlFlow");let l=Le(),c=Di(),d=Do(c.consts,o);return gm(l,c,t,n,e,i,r,d,512,a,s),zM}function V(t,n){Es("NgControlFlow");let e=Le(),i=bs(),r=e[i]!==jn?e[i]:-1,o=r!==-1?c0(e,vi+r):void 0,a=0;if(jr(e,i,t)){let s=tt(null);try{if(o!==void 0&&JP(o,a),t!==-1){let l=vi+t,c=c0(e,l),d=X1(e[We],l),p=tF(c,d,e),_=of(e,d,n,{dehydratedView:p});af(c,_,a,fm(d,p))}}finally{tt(s)}}else if(o!==void 0){let s=XP(o,a);s!==void 0&&(s[en]=n)}}var Q1=class{lContainer;$implicit;$index;constructor(n,e,i){this.lContainer=n,this.$implicit=e,this.$index=i}get $count(){return this.lContainer.length-tn}};function qi(t){return t}function Em(t,n){return n}var K1=class{hasEmptyBlock;trackByFn;liveCollection;constructor(n,e,i){this.hasEmptyBlock=n,this.trackByFn=e,this.liveCollection=i}};function Mt(t,n,e,i,r,o,a,s,l,c,d,p,_){Es("NgControlFlow");let b=Le(),y=Di(),w=l!==void 0,C=Le(),D=s?a.bind(C[Cr][en]):a,F=new K1(w,D);C[vi+t]=F,gm(b,y,t+1,n,e,i,r,Do(y.consts,o),256),w&&gm(b,y,t+2,l,c,d,p,Do(y.consts,_),512)}var Z1=class extends q1{lContainer;hostLView;templateTNode;operationsCounter=void 0;needsIndexUpdate=!1;constructor(n,e,i){super(),this.lContainer=n,this.hostLView=e,this.templateTNode=i}get length(){return this.lContainer.length-tn}at(n){return this.getLView(n)[en].$implicit}attach(n,e){let i=e[Md];this.needsIndexUpdate||=n!==this.length,af(this.lContainer,e,n,fm(this.templateTNode,i)),EG(this.lContainer,n)}detach(n){return this.needsIndexUpdate||=n!==this.length-1,SG(this.lContainer,n),kG(this.lContainer,n)}create(n,e){let i=t0(this.lContainer,this.templateTNode.tView.ssrId),r=of(this.hostLView,this.templateTNode,new Q1(this.lContainer,e,n),{dehydratedView:i});return this.operationsCounter?.recordCreate(),r}destroy(n){y0(n[We],n),this.operationsCounter?.recordDestroy()}updateValue(n,e){this.getLView(n)[en].$implicit=e}reset(){this.needsIndexUpdate=!1,this.operationsCounter?.reset()}updateIndexes(){if(this.needsIndexUpdate)for(let n=0;n0){let o=i[ll];O7(o,r),Nd.delete(i),r.detachedLeaveAnimationFns=void 0}}function SG(t,n){if(t.length<=tn)return;let e=tn+n,i=t[e],r=i?i[Td]:void 0;r&&r.leave&&r.leave.size>0&&(r.detachedLeaveAnimationFns=[])}function kG(t,n){return Qp(t,n)}function TG(t,n){return XP(t,n)}function X1(t,n){return Np(t,n)}function v(t,n,e){let i=Le(),r=bs();if(jr(i,r,n)){let o=Di(),a=cm();VP(a,i,t,n,i[mi],e)}return v}function J1(t,n,e,i,r){M0(n,t,e,r?"class":"style",i)}function m(t,n,e,i){let r=Le(),o=r[We],a=t+vi,s=o.firstCreatePass?AM(a,r,2,n,kM,Ib(),e,i):o.data[a];if(w0(s,r,t,n,kF),rm(s)){let l=r[We];C0(l,r,s),lM(l,s,r)}return i!=null&&Cm(r,s),m}function h(){let t=Di(),n=sn(),e=D0(n);return t.firstCreatePass&&OM(t,e),YD(e)&&QD(),GD(),e.classesWithoutHost!=null&&cW(e)&&J1(t,e,Le(),e.classesWithoutHost,!0),e.stylesWithoutHost!=null&&dW(e)&&J1(t,e,Le(),e.stylesWithoutHost,!1),h}function M(t,n,e,i){return m(t,n,e,i),h(),M}function gt(t,n,e,i){let r=Le(),o=r[We],a=t+vi,s=o.firstCreatePass?oF(a,o,2,n,e,i):o.data[a];return w0(s,r,t,n,kF),i!=null&&Cm(r,s),gt}function yt(){let t=sn(),n=D0(t);return YD(n)&&QD(),GD(),yt}function ni(t,n,e,i){return gt(t,n,e,i),yt(),ni}var kF=(t,n,e,i,r)=>(jp(!0),gP(n[mi],i,GO()));function lt(t,n,e){let i=Le(),r=i[We],o=t+vi,a=r.firstCreatePass?AM(o,i,8,"ng-container",kM,Ib(),n,e):r.data[o];if(w0(a,i,t,"ng-container",IF),rm(a)){let s=i[We];C0(s,i,a),lM(s,a,i)}return e!=null&&Cm(i,a),lt}function ot(){let t=Di(),n=sn(),e=D0(n);return t.firstCreatePass&&OM(t,e),ot}function qe(t,n,e){return lt(t,n,e),ot(),qe}function UM(t,n,e){let i=Le(),r=i[We],o=t+vi,a=r.firstCreatePass?oF(o,r,8,"ng-container",n,e):r.data[o];return w0(a,i,t,"ng-container",IF),e!=null&&Cm(i,a),UM}function TF(){let t=sn(),n=D0(t);return ot}function df(t,n,e){return UM(t,n,e),TF(),df}var IF=(t,n,e,i,r)=>(jp(!0),i7(n[mi],""));function q(){return Le()}function pi(t,n,e){let i=Le(),r=bs();if(jr(i,r,n)){let o=Di(),a=cm();BP(a,i,t,n,i[mi],e)}return pi}var Hb=void 0;function IG(t){let n=Math.floor(Math.abs(t)),e=t.toString().replace(/^[^.]*\.?/,"").length;return n===1&&e===0?1:5}var AG=["en",[["a","p"],["AM","PM"]],[["AM","PM"]],[["S","M","T","W","T","F","S"],["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],["Su","Mo","Tu","We","Th","Fr","Sa"]],Hb,[["J","F","M","A","M","J","J","A","S","O","N","D"],["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["January","February","March","April","May","June","July","August","September","October","November","December"]],Hb,[["B","A"],["BC","AD"],["Before Christ","Anno Domini"]],0,[6,0],["M/d/yy","MMM d, y","MMMM d, y","EEEE, MMMM d, y"],["h:mm a","h:mm:ss a","h:mm:ss a z","h:mm:ss a zzzz"],["{1}, {0}",Hb,"{1} 'at' {0}",Hb],[".",",",";","%","+","-","E","\xD7","\u2030","\u221E","NaN",":"],["#,##0.###","#,##0%","\xA4#,##0.00","#E0"],"USD","$","US Dollar",{},"ltr",IG],p1={};function Hr(t){let n=OG(t),e=kR(n);if(e)return e;let i=n.split("-")[0];if(e=kR(i),e)return e;if(i==="en")return AG;throw new ue(701,!1)}function kR(t){return t in p1||(p1[t]=Uo.ng&&Uo.ng.common&&Uo.ng.common.locales&&Uo.ng.common.locales[t]),p1[t]}var Yi=(function(t){return t[t.LocaleId=0]="LocaleId",t[t.DayPeriodsFormat=1]="DayPeriodsFormat",t[t.DayPeriodsStandalone=2]="DayPeriodsStandalone",t[t.DaysFormat=3]="DaysFormat",t[t.DaysStandalone=4]="DaysStandalone",t[t.MonthsFormat=5]="MonthsFormat",t[t.MonthsStandalone=6]="MonthsStandalone",t[t.Eras=7]="Eras",t[t.FirstDayOfWeek=8]="FirstDayOfWeek",t[t.WeekendRange=9]="WeekendRange",t[t.DateFormat=10]="DateFormat",t[t.TimeFormat=11]="TimeFormat",t[t.DateTimeFormat=12]="DateTimeFormat",t[t.NumberSymbols=13]="NumberSymbols",t[t.NumberFormats=14]="NumberFormats",t[t.CurrencyCode=15]="CurrencyCode",t[t.CurrencySymbol=16]="CurrencySymbol",t[t.CurrencyName=17]="CurrencyName",t[t.Currencies=18]="Currencies",t[t.Directionality=19]="Directionality",t[t.PluralCase=20]="PluralCase",t[t.ExtraData=21]="ExtraData",t})(Yi||{});function OG(t){return t.toLowerCase().replace(/_/g,"-")}var uf="en-US",RG="USD";var PG=uf;function AF(t){typeof t=="string"&&(PG=t.toLowerCase().replace(/_/g,"-"))}function S(t,n,e){let i=Le(),r=Di(),o=sn();return OF(r,i,i[mi],o,t,n,e),S}function O0(t,n,e){let i=Le(),r=Di(),o=sn();return(o.type&3||e)&&cF(o,r,i,e,i[mi],t,n,qb(o,i,n)),O0}function OF(t,n,e,i,r,o,a){let s=!0,l=null;if((i.type&3||a)&&(l??=qb(i,n,o),cF(i,t,n,a,e,r,o,l)&&(s=!1)),s){let c=i.outputs?.[r],d=i.hostDirectiveOutputs?.[r];if(d&&d.length)for(let p=0;p>17&32767}function LG(t){return(t&2)==2}function VG(t,n){return t&131071|n<<17}function eM(t){return t|2}function _m(t){return(t&131068)>>2}function f1(t,n){return t&-131069|n<<2}function BG(t){return(t&1)===1}function tM(t){return t|1}function jG(t,n,e,i,r,o){let a=o?n.classBindings:n.styleBindings,s=Ld(a),l=_m(a);t[i]=e;let c=!1,d;if(Array.isArray(e)){let p=e;d=p[1],(d===null||em(p,d)>0)&&(c=!0)}else d=e;if(r)if(l!==0){let _=Ld(t[s+1]);t[i+1]=zb(_,s),_!==0&&(t[_+1]=f1(t[_+1],i)),t[s+1]=VG(t[s+1],i)}else t[i+1]=zb(s,0),s!==0&&(t[s+1]=f1(t[s+1],i)),s=i;else t[i+1]=zb(l,0),s===0?s=i:t[l+1]=f1(t[l+1],i),l=i;c&&(t[i+1]=eM(t[i+1])),TR(t,d,i,!0),TR(t,d,i,!1),HG(n,d,t,i,o),a=zb(s,l),o?n.classBindings=a:n.styleBindings=a}function HG(t,n,e,i,r){let o=r?t.residualClasses:t.residualStyles;o!=null&&typeof n=="string"&&em(o,n)>=0&&(e[i+1]=tM(e[i+1]))}function TR(t,n,e,i){let r=t[e+1],o=n===null,a=i?Ld(r):_m(r),s=!1;for(;a!==0&&(s===!1||o);){let l=t[a],c=t[a+1];zG(l,n)&&(s=!0,t[a+1]=i?tM(c):eM(c)),a=i?Ld(c):_m(c)}s&&(t[e+1]=i?eM(r):tM(r))}function zG(t,n){return t===null||n==null||(Array.isArray(t)?t[1]:t)===n?!0:Array.isArray(t)&&typeof n=="string"?em(t,n)>=0:!1}var Dn={textEnd:0,key:0,keyEnd:0,value:0,valueEnd:0};function RF(t){return t.substring(Dn.key,Dn.keyEnd)}function UG(t){return t.substring(Dn.value,Dn.valueEnd)}function $G(t){return NF(t),PF(t,bm(t,0,Dn.textEnd))}function PF(t,n){let e=Dn.textEnd;return e===n?-1:(n=Dn.keyEnd=GG(t,Dn.key=n,e),bm(t,n,e))}function WG(t){return NF(t),FF(t,bm(t,0,Dn.textEnd))}function FF(t,n){let e=Dn.textEnd,i=Dn.key=bm(t,n,e);return e===i?-1:(i=Dn.keyEnd=qG(t,i,e),i=IR(t,i,e,58),i=Dn.value=bm(t,i,e),i=Dn.valueEnd=YG(t,i,e),IR(t,i,e,59))}function NF(t){Dn.key=0,Dn.keyEnd=0,Dn.value=0,Dn.valueEnd=0,Dn.textEnd=t.length}function bm(t,n,e){for(;n32;)n++;return n}function qG(t,n,e){let i;for(;n=65&&(i&-33)<=90||i>=48&&i<=57);)n++;return n}function IR(t,n,e,i){return n=bm(t,n,e),n32&&(s=a),o=r,r=i,i=l&-33}return s}function AR(t,n,e,i){let r=-1,o=e;for(;o=0;e=FF(n,e))HF(t,RF(n),UG(n))}function at(t){VF(iq,KG,t,!0)}function KG(t,n){for(let e=$G(n);e>=0;e=PF(n,e))Op(t,RF(n),!0)}function LF(t,n,e,i){let r=Le(),o=Di(),a=Vp(2);if(o.firstUpdatePass&&jF(o,t,a,i),n!==jn&&jr(r,a,n)){let s=o.data[La()];zF(o,s,r,r[mi],t,r[a+1]=rq(n,e),i,a)}}function VF(t,n,e,i){let r=Di(),o=Vp(2);r.firstUpdatePass&&jF(r,null,o,i);let a=Le();if(e!==jn&&jr(a,o,e)){let s=r.data[La()];if(UF(s,i)&&!BF(r,o)){let l=i?s.classesWithoutHost:s.stylesWithoutHost;l!==null&&(e=yb(l,e||"")),J1(r,s,a,e,i)}else nq(r,s,a,a[mi],a[o+1],a[o+1]=tq(t,n,e),i,o)}}function BF(t,n){return n>=t.expandoStartIndex}function jF(t,n,e,i){let r=t.data;if(r[e+1]===null){let o=r[La()],a=BF(t,e);UF(o,i)&&n===null&&!a&&(n=!1),n=ZG(r,o,n,i),jG(r,o,n,e,a,i)}}function ZG(t,n,e,i){let r=HO(t),o=i?n.residualClasses:n.residualStyles;if(r===null)(i?n.classBindings:n.styleBindings)===0&&(e=g1(null,t,n,e,i),e=Xp(e,n.attrs,i),o=null);else{let a=n.directiveStylingLast;if(a===-1||t[a]!==r)if(e=g1(r,t,n,e,i),o===null){let l=XG(t,n,i);l!==void 0&&Array.isArray(l)&&(l=g1(null,t,n,l[1],i),l=Xp(l,n.attrs,i),JG(t,n,i,l))}else o=eq(t,n,i)}return o!==void 0&&(i?n.residualClasses=o:n.residualStyles=o),e}function XG(t,n,e){let i=e?n.classBindings:n.styleBindings;if(_m(i)!==0)return t[Ld(i)]}function JG(t,n,e,i){let r=e?n.classBindings:n.styleBindings;t[Ld(r)]=i}function eq(t,n,e){let i,r=n.directiveEnd;for(let o=1+n.directiveStylingLast;o0;){let l=t[r],c=Array.isArray(l),d=c?l[1]:l,p=d===null,_=e[r+1];_===jn&&(_=p?xr:void 0);let b=p?Db(_,i):d===i?_:void 0;if(c&&!d0(b)&&(b=Db(l,i)),d0(b)&&(s=b,a))return s;let y=t[r+1];r=a?Ld(y):_m(y)}if(n!==null){let l=o?n.residualClasses:n.residualStyles;l!=null&&(s=Db(l,i))}return s}function d0(t){return t!==void 0}function rq(t,n){return t==null||t===""||(typeof n=="string"?t=t+n:typeof t=="object"&&(t=sl(eo(t)))),t}function UF(t,n){return(t.flags&(n?8:16))!==0}function f(t,n=""){let e=Le(),i=Di(),r=t+vi,o=i.firstCreatePass?wm(i,r,1,n,null):i.data[r],a=oq(i,e,o,n,t);e[r]=a,Fb()&&EM(i,e,a,o),sm(o,!1)}var oq=(t,n,e,i,r)=>(jp(!0),e7(n[mi],i));function $F(t,n,e,i=""){return jr(t,bs(),e)?n+$o(e)+i:jn}function aq(t,n,e,i,r,o=""){let a=e1(),s=Kp(t,a,e,r);return Vp(2),s?n+$o(e)+i+$o(r)+o:jn}function sq(t,n,e,i,r,o,a,s=""){let l=e1(),c=lF(t,l,e,r,a);return Vp(3),c?n+$o(e)+i+$o(r)+o+$o(a)+s:jn}function N(t){return fe("",t),N}function fe(t,n,e){let i=Le(),r=$F(i,t,n,e);return r!==jn&&$M(i,La(),r),fe}function _l(t,n,e,i,r){let o=Le(),a=aq(o,t,n,e,i,r);return a!==jn&&$M(o,La(),a),_l}function Sm(t,n,e,i,r,o,a){let s=Le(),l=sq(s,t,n,e,i,r,o,a);return l!==jn&&$M(s,La(),l),Sm}function $M(t,n,e){let i=BD(n,t);t7(t[mi],i,e)}function pn(t,n,e){o1(n)&&(n=n());let i=Le(),r=bs();if(jr(i,r,n)){let o=Di(),a=cm();VP(a,i,t,n,i[mi],e)}return pn}function Mn(t,n){let e=o1(t);return e&&t.set(n),e}function fn(t,n){let e=Le(),i=Di(),r=sn();return OF(i,e,e[mi],r,t,n),fn}function WM(t){return jr(Le(),bs(),t)?$o(t):jn}function Zo(t,n,e=""){return $F(Le(),t,n,e)}function lq(t,n,e){let i=Di();if(i.firstCreatePass){let r=Na(t);iM(e,i.data,i.blueprint,r,!0),iM(n,i.data,i.blueprint,r,!1)}}function iM(t,n,e,i,r){if(t=Cn(t),Array.isArray(t))for(let o=0;o>20;if(Cd(t)||!t.multi){let b=new Fd(c,r,be,null),y=b1(l,n,r?d:d+_,p);y===-1?(y1(Xb(s,a),o,l),_1(o,t,n.length),n.push(l),s.directiveStart++,s.directiveEnd++,r&&(s.providerIndexes+=1048576),e.push(b),a.push(b)):(e[y]=b,a[y]=b)}else{let b=b1(l,n,d+_,p),y=b1(l,n,d,d+_),w=b>=0&&e[b],C=y>=0&&e[y];if(r&&!C||!r&&!w){y1(Xb(s,a),o,l);let D=uq(r?dq:cq,e.length,r,i,c,t);!r&&C&&(e[y].providerFactory=D),_1(o,t,n.length,0),n.push(l),s.directiveStart++,s.directiveEnd++,r&&(s.providerIndexes+=1048576),e.push(D),a.push(D)}else{let D=WF(e[r?y:b],c,!r&&i);_1(o,t,b>-1?b:y,D)}!r&&i&&C&&e[y].componentProviders++}}}function _1(t,n,e,i){let r=Cd(n),o=DO(n);if(r||o){let l=(o?Cn(n.useClass):n).prototype.ngOnDestroy;if(l){let c=t.destroyHooks||(t.destroyHooks=[]);if(!r&&n.multi){let d=c.indexOf(e);d===-1?c.push(e,[i,l]):c[d+1].push(i,l)}else c.push(e,l)}}}function WF(t,n,e){return e&&t.componentProviders++,t.multi.push(n)-1}function b1(t,n,e,i){for(let r=e;r{e.providersResolver=(i,r)=>lq(i,r?r(t):t,n)}}function dt(t,n,e){let i=_s()+t,r=Le();return r[i]===jn?lf(r,i,e?n.call(e):n()):N9(r,i)}function $t(t,n,e,i){return GF(Le(),_s(),t,n,e,i)}function ja(t,n,e,i,r){return qF(Le(),_s(),t,n,e,i,r)}function Hd(t,n,e,i,r,o){return YF(Le(),_s(),t,n,e,i,r,o)}function km(t,n,e,i,r,o,a){return QF(Le(),_s(),t,n,e,i,r,o,a)}function R0(t,n){let e=t[n];return e===jn?void 0:e}function GF(t,n,e,i,r,o){let a=n+e;return jr(t,a,r)?lf(t,a+1,o?i.call(o,r):i(r)):R0(t,a+1)}function qF(t,n,e,i,r,o,a){let s=n+e;return Kp(t,s,r,o)?lf(t,s+2,a?i.call(a,r,o):i(r,o)):R0(t,s+2)}function YF(t,n,e,i,r,o,a,s){let l=n+e;return lF(t,l,r,o,a)?lf(t,l+3,s?i.call(s,r,o,a):i(r,o,a)):R0(t,l+3)}function QF(t,n,e,i,r,o,a,s,l){let c=n+e;return L9(t,c,r,o,a,s)?lf(t,c+4,l?i.call(l,r,o,a,s):i(r,o,a,s)):R0(t,c+4)}function me(t,n){let e=Di(),i,r=t+vi;e.firstCreatePass?(i=mq(n,e.pipeRegistry),e.data[r]=i,i.onDestroy&&(e.destroyHooks??=[]).push(r,i.onDestroy)):i=e.data[r];let o=i.factory||(i.factory=ic(i.type,!0)),a,s=Vr(be);try{let l=Zb(!1),c=o();return Zb(l),jD(e,Le(),r,c),c}finally{Vr(s)}}function mq(t,n){if(n)for(let e=n.length-1;e>=0;e--){let i=n[e];if(t===i.name)return i}}function Re(t,n,e){let i=t+vi,r=Le(),o=am(r,i);return P0(r,i)?GF(r,_s(),n,o.transform,e,o):o.transform(e)}function Ui(t,n,e,i){let r=t+vi,o=Le(),a=am(o,r);return P0(o,r)?qF(o,_s(),n,a.transform,e,i,a):a.transform(e,i)}function GM(t,n,e,i,r){let o=t+vi,a=Le(),s=am(a,o);return P0(a,o)?YF(a,_s(),n,s.transform,e,i,r,s):s.transform(e,i,r)}function Tm(t,n,e,i,r,o){let a=t+vi,s=Le(),l=am(s,a);return P0(s,a)?QF(s,_s(),n,l.transform,e,i,r,o,l):l.transform(e,i,r,o)}function P0(t,n){return t[We].data[n].pure}function Mi(t,n){return E0(t,n)}var u0=class{ngModuleFactory;componentFactories;constructor(n,e){this.ngModuleFactory=n,this.componentFactories=e}},qM=(()=>{class t{compileModuleSync(e){return new s0(e)}compileModuleAsync(e){return Promise.resolve(this.compileModuleSync(e))}compileModuleAndAllComponentsSync(e){let i=this.compileModuleSync(e),r=ID(e),o=xP(r.declarations).reduce((a,s)=>{let l=hs(s);return l&&a.push(new cc(l)),a},[]);return new u0(i,o)}compileModuleAndAllComponentsAsync(e){return Promise.resolve(this.compileModuleAndAllComponentsSync(e))}clearCache(){}clearCacheFor(e){}getModuleId(e){}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var hq=(()=>{class t{zone=u(ae);changeDetectionScheduler=u(Ho);applicationRef=u(tr);applicationErrorHandler=u(wr);_onMicrotaskEmptySubscription;initialize(){this._onMicrotaskEmptySubscription||(this._onMicrotaskEmptySubscription=this.zone.onMicrotaskEmpty.subscribe({next:()=>{this.changeDetectionScheduler.runningTick||this.zone.run(()=>{try{this.applicationRef.dirtyFlags|=1,this.applicationRef._tick()}catch(e){this.applicationErrorHandler(e)}})}}))}ngOnDestroy(){this._onMicrotaskEmptySubscription?.unsubscribe()}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function KF({ngZoneFactory:t,ignoreChangesOutsideZone:n,scheduleInRootZone:e}){return t??=()=>new ae(Me(I({},ZF()),{scheduleInRootZone:e})),[{provide:ae,useFactory:t},{provide:ms,multi:!0,useFactory:()=>{let i=u(hq,{optional:!0});return()=>i.initialize()}},{provide:ms,multi:!0,useFactory:()=>{let i=u(pq);return()=>{i.initialize()}}},n===!0?{provide:a1,useValue:!0}:[],{provide:s1,useValue:e??SP},{provide:wr,useFactory:()=>{let i=u(ae),r=u(ti),o;return a=>{i.runOutsideAngular(()=>{r.destroyed&&!o?setTimeout(()=>{throw a}):(o??=r.get(Ln),o.handleError(a))})}}}]}function ZF(t){return{enableLongStackTrace:!1,shouldCoalesceEventChangeDetection:t?.eventCoalescing??!1,shouldCoalesceRunChangeDetection:t?.runCoalescing??!1}}var pq=(()=>{class t{subscription=new ke;initialized=!1;zone=u(ae);pendingTasks=u(ys);initialize(){if(this.initialized)return;this.initialized=!0;let e=null;!this.zone.isStable&&!this.zone.hasPendingMacrotasks&&!this.zone.hasPendingMicrotasks&&(e=this.pendingTasks.add()),this.zone.runOutsideAngular(()=>{this.subscription.add(this.zone.onStable.subscribe(()=>{ae.assertNotInAngularZone(),queueMicrotask(()=>{e!==null&&!this.zone.hasPendingMacrotasks&&!this.zone.hasPendingMicrotasks&&(this.pendingTasks.remove(e),e=null)})}))}),this.subscription.add(this.zone.onUnstable.subscribe(()=>{ae.assertInAngularZone(),e??=this.pendingTasks.add()}))}ngOnDestroy(){this.subscription.unsubscribe()}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var XF=(()=>{class t{applicationErrorHandler=u(wr);appRef=u(tr);taskService=u(ys);ngZone=u(ae);zonelessEnabled=u(Hp);tracing=u(mc,{optional:!0});disableScheduling=u(a1,{optional:!0})??!1;zoneIsDefined=typeof Zone<"u"&&!!Zone.root.run;schedulerTickApplyArgs=[{data:{__scheduler_tick__:!0}}];subscriptions=new ke;angularZoneId=this.zoneIsDefined?this.ngZone._inner?.get(e0):null;scheduleInRootZone=!this.zonelessEnabled&&this.zoneIsDefined&&(u(s1,{optional:!0})??!1);cancelScheduledCallback=null;useMicrotaskScheduler=!1;runningTick=!1;pendingRenderTaskId=null;constructor(){this.subscriptions.add(this.appRef.afterTick.subscribe(()=>{this.runningTick||this.cleanup()})),this.subscriptions.add(this.ngZone.onUnstable.subscribe(()=>{this.runningTick||this.cleanup()})),this.disableScheduling||=!this.zonelessEnabled&&(this.ngZone instanceof Gp||!this.zoneIsDefined)}notify(e){if(!this.zonelessEnabled&&e===5)return;let i=!1;switch(e){case 0:{this.appRef.dirtyFlags|=2;break}case 3:case 2:case 4:case 5:case 1:{this.appRef.dirtyFlags|=4;break}case 6:{this.appRef.dirtyFlags|=2,i=!0;break}case 12:{this.appRef.dirtyFlags|=16,i=!0;break}case 13:{this.appRef.dirtyFlags|=2,i=!0;break}case 11:{i=!0;break}case 9:case 8:case 7:case 10:default:this.appRef.dirtyFlags|=8}if(this.appRef.tracingSnapshot=this.tracing?.snapshot(this.appRef.tracingSnapshot)??null,!this.shouldScheduleTick(i))return;let r=this.useMicrotaskScheduler?dR:kP;this.pendingRenderTaskId=this.taskService.add(),this.scheduleInRootZone?this.cancelScheduledCallback=Zone.root.run(()=>r(()=>this.tick())):this.cancelScheduledCallback=this.ngZone.runOutsideAngular(()=>r(()=>this.tick()))}shouldScheduleTick(e){return!(this.disableScheduling&&!e||this.appRef.destroyed||this.pendingRenderTaskId!==null||this.runningTick||this.appRef._runningTick||!this.zonelessEnabled&&this.zoneIsDefined&&Zone.current.get(e0+this.angularZoneId))}tick(){if(this.runningTick||this.appRef.destroyed)return;if(this.appRef.dirtyFlags===0){this.cleanup();return}!this.zonelessEnabled&&this.appRef.dirtyFlags&7&&(this.appRef.dirtyFlags|=1);let e=this.taskService.add();try{this.ngZone.run(()=>{this.runningTick=!0,this.appRef._tick()},void 0,this.schedulerTickApplyArgs)}catch(i){this.taskService.remove(e),this.applicationErrorHandler(i)}finally{this.cleanup()}this.useMicrotaskScheduler=!0,dR(()=>{this.useMicrotaskScheduler=!1,this.taskService.remove(e)})}ngOnDestroy(){this.subscriptions.unsubscribe(),this.cleanup()}cleanup(){if(this.runningTick=!1,this.cancelScheduledCallback?.(),this.cancelScheduledCallback=null,this.pendingRenderTaskId!==null){let e=this.pendingRenderTaskId;this.pendingRenderTaskId=null,this.taskService.remove(e)}}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function fq(){return typeof $localize<"u"&&$localize.locale||uf}var bl=new O("",{providedIn:"root",factory:()=>u(bl,{optional:!0,skipSelf:!0})||fq()}),YM=new O("",{providedIn:"root",factory:()=>RG});var mf=class{destroyed=!1;listeners=null;errorHandler=u(Ln,{optional:!0});destroyRef=u(ln);constructor(){this.destroyRef.onDestroy(()=>{this.destroyed=!0,this.listeners=null})}subscribe(n){if(this.destroyed)throw new ue(953,!1);return(this.listeners??=[]).push(n),{unsubscribe:()=>{let e=this.listeners?.indexOf(n);e!==void 0&&e!==-1&&this.listeners?.splice(e,1)}}}emit(n){if(this.destroyed){console.warn(zo(953,!1));return}if(this.listeners===null)return;let e=tt(null);try{for(let i of this.listeners)try{i(n)}catch(r){this.errorHandler?.handleError(r)}}finally{tt(e)}}};function KM(t){return t.destroyRef}function Ni(t){return aO(t)}function ci(t,n){return yp(t,n?.equal)}var QM=class{[mn];constructor(n){this[mn]=n}destroy(){this[mn].destroy()}};function zr(t,n){let e=n?.injector??u(de),i=n?.manualCleanup!==!0?e.get(ln):null,r,o=e.get(Od,null,{optional:!0}),a=e.get(Ho);return o!==null?(r=bq(o.view,a,t),i instanceof kp&&i._lView===o.view&&(i=null)):r=vq(t,e.get(zp),a),r.injector=e,i!==null&&(r.onDestroyFn=i.onDestroy(()=>r.destroy())),new QM(r)}var JF=Me(I({},sO),{cleanupFns:void 0,zone:null,onDestroyFn:Rd,run(){let t=lm(!1);try{lO(this)}finally{lm(t)}},cleanup(){if(!this.cleanupFns?.length)return;let t=tt(null);try{for(;this.cleanupFns.length;)this.cleanupFns.pop()()}finally{this.cleanupFns=[],tt(t)}}}),gq=Me(I({},JF),{consumerMarkedDirty(){this.scheduler.schedule(this),this.notifier.notify(12)},destroy(){ql(this),this.onDestroyFn(),this.cleanup(),this.scheduler.remove(this)}}),_q=Me(I({},JF),{consumerMarkedDirty(){this.view[nt]|=8192,ac(this.view),this.notifier.notify(13)},destroy(){ql(this),this.onDestroyFn(),this.cleanup(),this.view[cl]?.delete(this)}});function bq(t,n,e){let i=Object.create(_q);return i.view=t,i.zone=typeof Zone<"u"?Zone.current:null,i.notifier=n,i.fn=e2(i,e),t[cl]??=new Set,t[cl].add(i),i.consumerMarkedDirty(i),i}function vq(t,n,e){let i=Object.create(gq);return i.fn=e2(i,t),i.scheduler=n,i.notifier=e,i.zone=typeof Zone<"u"?Zone.current:null,i.scheduler.add(i),i.notifier.notify(12),i}function e2(t,n){return()=>{n(e=>(t.cleanupFns??=[]).push(e))}}var L0=Symbol("InputSignalNode#UNSET"),c2=Me(I({},xp),{transformFn:void 0,applyValueToInputSignal(t,n){hd(t,n)}});function d2(t,n){let e=Object.create(c2);e.value=t,e.transformFn=n?.transform;function i(){if(Wl(e),e.value===L0){let r=null;throw new ue(-950,r)}return e.value}return i[mn]=e,i}var Li=class{attributeName;constructor(n){this.attributeName=n}__NG_ELEMENT_ID__=()=>Jp(this.attributeName);toString(){return`HostAttributeToken ${this.attributeName}`}},rE=new O("");rE.__NG_ELEMENT_ID__=t=>{let n=sn();if(n===null)throw new ue(204,!1);if(n.type&2)return n.value;if(t&8)return null;throw new ue(204,!1)};function Ei(t){return new mf}function t2(t,n){return d2(t,n)}function Rq(t){return d2(L0,t)}var re=(t2.required=Rq,t2);function i2(t,n){return LM(n)}function Pq(t,n){return VM(n)}var ir=(i2.required=Pq,i2);function n2(t,n){return LM(n)}function Fq(t,n){return VM(n)}var Mr=(n2.required=Fq,n2);function u2(t,n){return yF(n)}function m2(t,n){let e=Object.create(c2),i=new mf;e.value=t;function r(){return Wl(e),r2(e.value),e.value}return r[mn]=e,r.asReadonly=Nb.bind(r),r.set=o=>{e.equal(e.value,o)||(hd(e,o),i.emit(o))},r.update=o=>{r2(e.value),r.set(o(e.value))},r.subscribe=i.subscribe.bind(i),r.destroyRef=i.destroyRef,r}function r2(t){if(t===L0)throw new ue(952,!1)}function o2(t,n){return m2(t,n)}function Nq(t){return m2(L0,t)}var Im=(o2.required=Nq,o2);var XM=new O(""),Lq=new O("");function hf(t){return!t.moduleRef}function Vq(t){let n=hf(t)?t.r3Injector:t.moduleRef.injector,e=n.get(ae);return e.run(()=>{hf(t)?t.r3Injector.resolveInjectorInitializers():t.moduleRef.resolveInjectorInitializers();let i=n.get(wr),r;if(e.runOutsideAngular(()=>{r=e.onError.subscribe({next:i})}),hf(t)){let o=()=>n.destroy(),a=t.platformInjector.get(XM);a.add(o),n.onDestroy(()=>{r.unsubscribe(),a.delete(o)})}else{let o=()=>t.moduleRef.destroy(),a=t.platformInjector.get(XM);a.add(o),t.moduleRef.onDestroy(()=>{$p(t.allPlatformModules,t.moduleRef),r.unsubscribe(),a.delete(o)})}return jq(i,e,()=>{let o=n.get(ys),a=o.add(),s=n.get(HM);return s.runInitializers(),s.donePromise.then(()=>{let l=n.get(bl,uf);if(AF(l||uf),!n.get(Lq,!0))return hf(t)?n.get(tr):(t.allPlatformModules.push(t.moduleRef),t.moduleRef);if(hf(t)){let d=n.get(tr);return t.rootComponent!==void 0&&d.bootstrap(t.rootComponent),d}else return Bq?.(t.moduleRef,t.allPlatformModules),t.moduleRef}).finally(()=>void o.remove(a))})})}var Bq;function jq(t,n,e){try{let i=e();return fl(i)?i.catch(r=>{throw n.runOutsideAngular(()=>t(r)),r}):i}catch(i){throw n.runOutsideAngular(()=>t(i)),i}}var F0=null;function Hq(t=[],n){return de.create({name:n,providers:[{provide:Rp,useValue:"platform"},{provide:XM,useValue:new Set([()=>F0=null])},...t]})}function zq(t=[]){if(F0)return F0;let n=Hq(t);return F0=n,EF(),Uq(n),n}function Uq(t){let n=t.get(m0,null);Vn(t,()=>{n?.forEach(e=>e())})}var ye=(()=>{class t{static __NG_ELEMENT_ID__=$q}return t})();function $q(t){return Wq(sn(),Le(),(t&16)===16)}function Wq(t,n,e){if(dl(t)&&!e){let i=Yo(t.index,n);return new lc(i,i)}else if(t.type&175){let i=n[Cr];return new lc(i,n)}return null}var JM=class{constructor(){}supports(n){return RM(n)}create(n){return new eE(n)}},Gq=(t,n)=>n,eE=class{length=0;collection;_linkedRecords=null;_unlinkedRecords=null;_previousItHead=null;_itHead=null;_itTail=null;_additionsHead=null;_additionsTail=null;_movesHead=null;_movesTail=null;_removalsHead=null;_removalsTail=null;_identityChangesHead=null;_identityChangesTail=null;_trackByFn;constructor(n){this._trackByFn=n||Gq}forEachItem(n){let e;for(e=this._itHead;e!==null;e=e._next)n(e)}forEachOperation(n){let e=this._itHead,i=this._removalsHead,r=0,o=null;for(;e||i;){let a=!i||e&&e.currentIndex{a=this._trackByFn(r,s),e===null||!Object.is(e.trackById,a)?(e=this._mismatch(e,s,a,r),i=!0):(i&&(e=this._verifyReinsertion(e,s,a,r)),Object.is(e.item,s)||this._addIdentityChange(e,s)),e=e._next,r++}),this.length=r;return this._truncate(e),this.collection=n,this.isDirty}get isDirty(){return this._additionsHead!==null||this._movesHead!==null||this._removalsHead!==null||this._identityChangesHead!==null}_reset(){if(this.isDirty){let n;for(n=this._previousItHead=this._itHead;n!==null;n=n._next)n._nextPrevious=n._next;for(n=this._additionsHead;n!==null;n=n._nextAdded)n.previousIndex=n.currentIndex;for(this._additionsHead=this._additionsTail=null,n=this._movesHead;n!==null;n=n._nextMoved)n.previousIndex=n.currentIndex;this._movesHead=this._movesTail=null,this._removalsHead=this._removalsTail=null,this._identityChangesHead=this._identityChangesTail=null}}_mismatch(n,e,i,r){let o;return n===null?o=this._itTail:(o=n._prev,this._remove(n)),n=this._unlinkedRecords===null?null:this._unlinkedRecords.get(i,null),n!==null?(Object.is(n.item,e)||this._addIdentityChange(n,e),this._reinsertAfter(n,o,r)):(n=this._linkedRecords===null?null:this._linkedRecords.get(i,r),n!==null?(Object.is(n.item,e)||this._addIdentityChange(n,e),this._moveAfter(n,o,r)):n=this._addAfter(new tE(e,i),o,r)),n}_verifyReinsertion(n,e,i,r){let o=this._unlinkedRecords===null?null:this._unlinkedRecords.get(i,null);return o!==null?n=this._reinsertAfter(o,n._prev,r):n.currentIndex!=r&&(n.currentIndex=r,this._addToMoves(n,r)),n}_truncate(n){for(;n!==null;){let e=n._next;this._addToRemovals(this._unlink(n)),n=e}this._unlinkedRecords!==null&&this._unlinkedRecords.clear(),this._additionsTail!==null&&(this._additionsTail._nextAdded=null),this._movesTail!==null&&(this._movesTail._nextMoved=null),this._itTail!==null&&(this._itTail._next=null),this._removalsTail!==null&&(this._removalsTail._nextRemoved=null),this._identityChangesTail!==null&&(this._identityChangesTail._nextIdentityChange=null)}_reinsertAfter(n,e,i){this._unlinkedRecords!==null&&this._unlinkedRecords.remove(n);let r=n._prevRemoved,o=n._nextRemoved;return r===null?this._removalsHead=o:r._nextRemoved=o,o===null?this._removalsTail=r:o._prevRemoved=r,this._insertAfter(n,e,i),this._addToMoves(n,i),n}_moveAfter(n,e,i){return this._unlink(n),this._insertAfter(n,e,i),this._addToMoves(n,i),n}_addAfter(n,e,i){return this._insertAfter(n,e,i),this._additionsTail===null?this._additionsTail=this._additionsHead=n:this._additionsTail=this._additionsTail._nextAdded=n,n}_insertAfter(n,e,i){let r=e===null?this._itHead:e._next;return n._next=r,n._prev=e,r===null?this._itTail=n:r._prev=n,e===null?this._itHead=n:e._next=n,this._linkedRecords===null&&(this._linkedRecords=new N0),this._linkedRecords.put(n),n.currentIndex=i,n}_remove(n){return this._addToRemovals(this._unlink(n))}_unlink(n){this._linkedRecords!==null&&this._linkedRecords.remove(n);let e=n._prev,i=n._next;return e===null?this._itHead=i:e._next=i,i===null?this._itTail=e:i._prev=e,n}_addToMoves(n,e){return n.previousIndex===e||(this._movesTail===null?this._movesTail=this._movesHead=n:this._movesTail=this._movesTail._nextMoved=n),n}_addToRemovals(n){return this._unlinkedRecords===null&&(this._unlinkedRecords=new N0),this._unlinkedRecords.put(n),n.currentIndex=null,n._nextRemoved=null,this._removalsTail===null?(this._removalsTail=this._removalsHead=n,n._prevRemoved=null):(n._prevRemoved=this._removalsTail,this._removalsTail=this._removalsTail._nextRemoved=n),n}_addIdentityChange(n,e){return n.item=e,this._identityChangesTail===null?this._identityChangesTail=this._identityChangesHead=n:this._identityChangesTail=this._identityChangesTail._nextIdentityChange=n,n}},tE=class{item;trackById;currentIndex=null;previousIndex=null;_nextPrevious=null;_prev=null;_next=null;_prevDup=null;_nextDup=null;_prevRemoved=null;_nextRemoved=null;_nextAdded=null;_nextMoved=null;_nextIdentityChange=null;constructor(n,e){this.item=n,this.trackById=e}},iE=class{_head=null;_tail=null;add(n){this._head===null?(this._head=this._tail=n,n._nextDup=null,n._prevDup=null):(this._tail._nextDup=n,n._prevDup=this._tail,n._nextDup=null,this._tail=n)}get(n,e){let i;for(i=this._head;i!==null;i=i._nextDup)if((e===null||e<=i.currentIndex)&&Object.is(i.trackById,n))return i;return null}remove(n){let e=n._prevDup,i=n._nextDup;return e===null?this._head=i:e._nextDup=i,i===null?this._tail=e:i._prevDup=e,this._head===null}},N0=class{map=new Map;put(n){let e=n.trackById,i=this.map.get(e);i||(i=new iE,this.map.set(e,i)),i.add(n)}get(n,e){let i=n,r=this.map.get(i);return r?r.get(n,e):null}remove(n){let e=n.trackById;return this.map.get(e).remove(n)&&this.map.delete(e),n}get isEmpty(){return this.map.size===0}clear(){this.map.clear()}};function a2(t,n,e){let i=t.previousIndex;if(i===null)return i;let r=0;return e&&i{class t{factories;static \u0275prov=R({token:t,providedIn:"root",factory:s2});constructor(e){this.factories=e}static create(e,i){if(i!=null){let r=i.factories.slice();e=e.concat(r)}return new t(e)}static extend(e){return{provide:t,useFactory:()=>{let i=u(t,{optional:!0,skipSelf:!0});return t.create(e,i||s2())}}}find(e){let i=this.factories.find(r=>r.supports(e));if(i!=null)return i;throw new ue(901,!1)}}return t})();function h2(t){let{rootComponent:n,appProviders:e,platformProviders:i,platformRef:r}=t;hi(8);try{let o=r?.injector??zq(i),a=[KF({}),{provide:Ho,useExisting:XF},YO,...e||[]],s=new Zp({providers:a,parent:o,debugName:"",runEnvironmentInitializers:!1});return Vq({r3Injector:s.injector,platformInjector:o,rootComponent:n})}catch(o){return Promise.reject(o)}finally{hi(9)}}function B(t){return typeof t=="boolean"?t:t!=null&&t!=="false"}function ht(t,n=NaN){return!isNaN(parseFloat(t))&&!isNaN(Number(t))?Number(t):n}var ZM=Symbol("NOT_SET"),p2=new Set,qq=Me(I({},xp),{consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,value:ZM,cleanup:null,consumerMarkedDirty(){if(this.sequence.impl.executing){if(this.sequence.lastPhase===null||this.sequence.lastPhase(Wl(c),c.value),c.signal[mn]=c,c.registerCleanupFn=d=>(c.cleanup??=new Set).add(d),this.nodes[s]=c,this.hooks[s]=d=>c.phaseFn(d)}}afterRun(){super.afterRun(),this.lastPhase=null}destroy(){super.destroy();for(let n of this.nodes)if(n)try{for(let e of n.cleanup??p2)e()}finally{ql(n)}}};function f2(t,n){let e=n?.injector??u(de),i=e.get(Ho),r=e.get(v0),o=e.get(mc,null,{optional:!0});r.impl??=e.get(DM);let a=t;typeof a=="function"&&(a={mixedReadWrite:t});let s=e.get(Od,null,{optional:!0}),l=new nE(r.impl,[a.earlyRead,a.write,a.mixedReadWrite,a.read],s?.view,i,e,o?.snapshot(null));return r.impl.register(l),l}function Am(t,n){let e=hs(t),i=n.elementInjector||tm();return new cc(e).create(i,n.projectableNodes,n.hostElement,n.environmentInjector,n.directives,n.bindings)}function g2(t){let n=hs(t);if(!n)return null;let e=new cc(n);return{get selector(){return e.selector},get type(){return e.componentType},get inputs(){return e.inputs},get outputs(){return e.outputs},get ngContentSelectors(){return e.ngContentSelectors},get isStandalone(){return n.standalone},get isSignal(){return n.signals}}}var v2=null;function Xo(){return v2}function oE(t){v2??=t}var pf=class{},ff=(()=>{class t{historyGo(e){throw new Error("")}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:()=>u(y2),providedIn:"platform"})}return t})(),aE=new O(""),y2=(()=>{class t extends ff{_location;_history;_doc=u(_e);constructor(){super(),this._location=window.location,this._history=window.history}getBaseHrefFromDOM(){return Xo().getBaseHref(this._doc)}onPopState(e){let i=Xo().getGlobalEventTarget(this._doc,"window");return i.addEventListener("popstate",e,!1),()=>i.removeEventListener("popstate",e)}onHashChange(e){let i=Xo().getGlobalEventTarget(this._doc,"window");return i.addEventListener("hashchange",e,!1),()=>i.removeEventListener("hashchange",e)}get href(){return this._location.href}get protocol(){return this._location.protocol}get hostname(){return this._location.hostname}get port(){return this._location.port}get pathname(){return this._location.pathname}get search(){return this._location.search}get hash(){return this._location.hash}set pathname(e){this._location.pathname=e}pushState(e,i,r){this._history.pushState(e,i,r)}replaceState(e,i,r){this._history.replaceState(e,i,r)}forward(){this._history.forward()}back(){this._history.back()}historyGo(e=0){this._history.go(e)}getState(){return this._history.state}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:()=>new t,providedIn:"platform"})}return t})();function V0(t,n){return t?n?t.endsWith("/")?n.startsWith("/")?t+n.slice(1):t+n:n.startsWith("/")?t+n:`${t}/${n}`:t:n}function _2(t){let n=t.search(/#|\?|$/);return t[n-1]==="/"?t.slice(0,n-1)+t.slice(n):t}function Ha(t){return t&&t[0]!=="?"?`?${t}`:t}var za=(()=>{class t{historyGo(e){throw new Error("")}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:()=>u(j0),providedIn:"root"})}return t})(),B0=new O(""),j0=(()=>{class t extends za{_platformLocation;_baseHref;_removeListenerFns=[];constructor(e,i){super(),this._platformLocation=e,this._baseHref=i??this._platformLocation.getBaseHrefFromDOM()??u(_e).location?.origin??""}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(e){this._removeListenerFns.push(this._platformLocation.onPopState(e),this._platformLocation.onHashChange(e))}getBaseHref(){return this._baseHref}prepareExternalUrl(e){return V0(this._baseHref,e)}path(e=!1){let i=this._platformLocation.pathname+Ha(this._platformLocation.search),r=this._platformLocation.hash;return r&&e?`${i}${r}`:i}pushState(e,i,r,o){let a=this.prepareExternalUrl(r+Ha(o));this._platformLocation.pushState(e,i,a)}replaceState(e,i,r,o){let a=this.prepareExternalUrl(r+Ha(o));this._platformLocation.replaceState(e,i,a)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(e=0){this._platformLocation.historyGo?.(e)}static \u0275fac=function(i){return new(i||t)(pe(ff),pe(B0,8))};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),ks=(()=>{class t{_subject=new z;_basePath;_locationStrategy;_urlChangeListeners=[];_urlChangeSubscription=null;constructor(e){this._locationStrategy=e;let i=this._locationStrategy.getBaseHref();this._basePath=Kq(_2(b2(i))),this._locationStrategy.onPopState(r=>{this._subject.next({url:this.path(!0),pop:!0,state:r.state,type:r.type})})}ngOnDestroy(){this._urlChangeSubscription?.unsubscribe(),this._urlChangeListeners=[]}path(e=!1){return this.normalize(this._locationStrategy.path(e))}getState(){return this._locationStrategy.getState()}isCurrentPathEqualTo(e,i=""){return this.path()==this.normalize(e+Ha(i))}normalize(e){return t.stripTrailingSlash(Qq(this._basePath,b2(e)))}prepareExternalUrl(e){return e&&e[0]!=="/"&&(e="/"+e),this._locationStrategy.prepareExternalUrl(e)}go(e,i="",r=null){this._locationStrategy.pushState(r,"",e,i),this._notifyUrlChangeListeners(this.prepareExternalUrl(e+Ha(i)),r)}replaceState(e,i="",r=null){this._locationStrategy.replaceState(r,"",e,i),this._notifyUrlChangeListeners(this.prepareExternalUrl(e+Ha(i)),r)}forward(){this._locationStrategy.forward()}back(){this._locationStrategy.back()}historyGo(e=0){this._locationStrategy.historyGo?.(e)}onUrlChange(e){return this._urlChangeListeners.push(e),this._urlChangeSubscription??=this.subscribe(i=>{this._notifyUrlChangeListeners(i.url,i.state)}),()=>{let i=this._urlChangeListeners.indexOf(e);this._urlChangeListeners.splice(i,1),this._urlChangeListeners.length===0&&(this._urlChangeSubscription?.unsubscribe(),this._urlChangeSubscription=null)}}_notifyUrlChangeListeners(e="",i){this._urlChangeListeners.forEach(r=>r(e,i))}subscribe(e,i,r){return this._subject.subscribe({next:e,error:i??void 0,complete:r??void 0})}static normalizeQueryParams=Ha;static joinWithSlash=V0;static stripTrailingSlash=_2;static \u0275fac=function(i){return new(i||t)(pe(za))};static \u0275prov=R({token:t,factory:()=>Yq(),providedIn:"root"})}return t})();function Yq(){return new ks(pe(za))}function Qq(t,n){if(!t||!n.startsWith(t))return n;let e=n.substring(t.length);return e===""||["/",";","?","#"].includes(e[0])?e:n}function b2(t){return t.replace(/\/index.html$/,"")}function Kq(t){if(new RegExp("^(https?:)?//").test(t)){let[,e]=t.split(/\/\/[^\/]+/);return e}return t}var fE=(()=>{class t extends za{_platformLocation;_baseHref="";_removeListenerFns=[];constructor(e,i){super(),this._platformLocation=e,i!=null&&(this._baseHref=i)}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(e){this._removeListenerFns.push(this._platformLocation.onPopState(e),this._platformLocation.onHashChange(e))}getBaseHref(){return this._baseHref}path(e=!1){let i=this._platformLocation.hash??"#";return i.length>0?i.substring(1):i}prepareExternalUrl(e){let i=V0(this._baseHref,e);return i.length>0?"#"+i:i}pushState(e,i,r,o){let a=this.prepareExternalUrl(r+Ha(o))||this._platformLocation.pathname;this._platformLocation.pushState(e,i,a)}replaceState(e,i,r,o){let a=this.prepareExternalUrl(r+Ha(o))||this._platformLocation.pathname;this._platformLocation.replaceState(e,i,a)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(e=0){this._platformLocation.historyGo?.(e)}static \u0275fac=function(i){return new(i||t)(pe(ff),pe(B0,8))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),S2={ADP:[void 0,void 0,0],AFN:[void 0,"\u060B",0],ALL:[void 0,void 0,0],AMD:[void 0,"\u058F",2],AOA:[void 0,"Kz"],ARS:[void 0,"$"],AUD:["A$","$"],AZN:[void 0,"\u20BC"],BAM:[void 0,"KM"],BBD:[void 0,"$"],BDT:[void 0,"\u09F3"],BHD:[void 0,void 0,3],BIF:[void 0,void 0,0],BMD:[void 0,"$"],BND:[void 0,"$"],BOB:[void 0,"Bs"],BRL:["R$"],BSD:[void 0,"$"],BWP:[void 0,"P"],BYN:[void 0,void 0,2],BYR:[void 0,void 0,0],BZD:[void 0,"$"],CAD:["CA$","$",2],CHF:[void 0,void 0,2],CLF:[void 0,void 0,4],CLP:[void 0,"$",0],CNY:["CN\xA5","\xA5"],COP:[void 0,"$",2],CRC:[void 0,"\u20A1",2],CUC:[void 0,"$"],CUP:[void 0,"$"],CZK:[void 0,"K\u010D",2],DJF:[void 0,void 0,0],DKK:[void 0,"kr",2],DOP:[void 0,"$"],EGP:[void 0,"E\xA3"],ESP:[void 0,"\u20A7",0],EUR:["\u20AC"],FJD:[void 0,"$"],FKP:[void 0,"\xA3"],GBP:["\xA3"],GEL:[void 0,"\u20BE"],GHS:[void 0,"GH\u20B5"],GIP:[void 0,"\xA3"],GNF:[void 0,"FG",0],GTQ:[void 0,"Q"],GYD:[void 0,"$",2],HKD:["HK$","$"],HNL:[void 0,"L"],HRK:[void 0,"kn"],HUF:[void 0,"Ft",2],IDR:[void 0,"Rp",2],ILS:["\u20AA"],INR:["\u20B9"],IQD:[void 0,void 0,0],IRR:[void 0,void 0,0],ISK:[void 0,"kr",0],ITL:[void 0,void 0,0],JMD:[void 0,"$"],JOD:[void 0,void 0,3],JPY:["\xA5",void 0,0],KHR:[void 0,"\u17DB"],KMF:[void 0,"CF",0],KPW:[void 0,"\u20A9",0],KRW:["\u20A9",void 0,0],KWD:[void 0,void 0,3],KYD:[void 0,"$"],KZT:[void 0,"\u20B8"],LAK:[void 0,"\u20AD",0],LBP:[void 0,"L\xA3",0],LKR:[void 0,"Rs"],LRD:[void 0,"$"],LTL:[void 0,"Lt"],LUF:[void 0,void 0,0],LVL:[void 0,"Ls"],LYD:[void 0,void 0,3],MGA:[void 0,"Ar",0],MGF:[void 0,void 0,0],MMK:[void 0,"K",0],MNT:[void 0,"\u20AE",2],MRO:[void 0,void 0,0],MUR:[void 0,"Rs",2],MXN:["MX$","$"],MYR:[void 0,"RM"],NAD:[void 0,"$"],NGN:[void 0,"\u20A6"],NIO:[void 0,"C$"],NOK:[void 0,"kr",2],NPR:[void 0,"Rs"],NZD:["NZ$","$"],OMR:[void 0,void 0,3],PHP:["\u20B1"],PKR:[void 0,"Rs",2],PLN:[void 0,"z\u0142"],PYG:[void 0,"\u20B2",0],RON:[void 0,"lei"],RSD:[void 0,void 0,0],RUB:[void 0,"\u20BD"],RWF:[void 0,"RF",0],SBD:[void 0,"$"],SEK:[void 0,"kr",2],SGD:[void 0,"$"],SHP:[void 0,"\xA3"],SLE:[void 0,void 0,2],SLL:[void 0,void 0,0],SOS:[void 0,void 0,0],SRD:[void 0,"$"],SSP:[void 0,"\xA3"],STD:[void 0,void 0,0],STN:[void 0,"Db"],SYP:[void 0,"\xA3",0],THB:[void 0,"\u0E3F"],TMM:[void 0,void 0,0],TND:[void 0,void 0,3],TOP:[void 0,"T$"],TRL:[void 0,void 0,0],TRY:[void 0,"\u20BA"],TTD:[void 0,"$"],TWD:["NT$","$",2],TZS:[void 0,void 0,2],UAH:[void 0,"\u20B4"],UGX:[void 0,void 0,0],USD:["$"],UYI:[void 0,void 0,0],UYU:[void 0,"$"],UYW:[void 0,void 0,4],UZS:[void 0,void 0,2],VEF:[void 0,"Bs",2],VND:["\u20AB",void 0,0],VUV:[void 0,void 0,0],XAF:["FCFA",void 0,0],XCD:["EC$","$"],XOF:["F\u202FCFA",void 0,0],XPF:["CFPF",void 0,0],XXX:["\xA4"],YER:[void 0,void 0,0],ZAR:[void 0,"R"],ZMK:[void 0,void 0,0],ZMW:[void 0,"ZK"],ZWD:[void 0,void 0,0]},K0=(function(t){return t[t.Decimal=0]="Decimal",t[t.Percent=1]="Percent",t[t.Currency=2]="Currency",t[t.Scientific=3]="Scientific",t})(K0||{});var Er=(function(t){return t[t.Format=0]="Format",t[t.Standalone=1]="Standalone",t})(Er||{}),yi=(function(t){return t[t.Narrow=0]="Narrow",t[t.Abbreviated=1]="Abbreviated",t[t.Wide=2]="Wide",t[t.Short=3]="Short",t})(yi||{}),no=(function(t){return t[t.Short=0]="Short",t[t.Medium=1]="Medium",t[t.Long=2]="Long",t[t.Full=3]="Full",t})(no||{}),nr={Decimal:0,Group:1,List:2,PercentSign:3,PlusSign:4,MinusSign:5,Exponential:6,SuperscriptingExponent:7,PerMille:8,Infinity:9,NaN:10,TimeSeparator:11,CurrencyDecimal:12,CurrencyGroup:13};function k2(t){return Hr(t)[Yi.LocaleId]}function T2(t,n,e){let i=Hr(t),r=[i[Yi.DayPeriodsFormat],i[Yi.DayPeriodsStandalone]],o=Jo(r,n);return Jo(o,e)}function I2(t,n,e){let i=Hr(t),r=[i[Yi.DaysFormat],i[Yi.DaysStandalone]],o=Jo(r,n);return Jo(o,e)}function A2(t,n,e){let i=Hr(t),r=[i[Yi.MonthsFormat],i[Yi.MonthsStandalone]],o=Jo(r,n);return Jo(o,e)}function O2(t,n){let i=Hr(t)[Yi.Eras];return Jo(i,n)}function gf(t,n){let e=Hr(t);return Jo(e[Yi.DateFormat],n)}function _f(t,n){let e=Hr(t);return Jo(e[Yi.TimeFormat],n)}function bf(t,n){let i=Hr(t)[Yi.DateTimeFormat];return Jo(i,n)}function $a(t,n){let e=Hr(t),i=e[Yi.NumberSymbols][n];if(typeof i>"u"){if(n===nr.CurrencyDecimal)return e[Yi.NumberSymbols][nr.Decimal];if(n===nr.CurrencyGroup)return e[Yi.NumberSymbols][nr.Group]}return i}function gE(t,n){return Hr(t)[Yi.NumberFormats][n]}function Zq(t){return Hr(t)[Yi.Currencies]}function R2(t){if(!t[Yi.ExtraData])throw new ue(2303,!1)}function P2(t){let n=Hr(t);return R2(n),(n[Yi.ExtraData][2]||[]).map(i=>typeof i=="string"?sE(i):[sE(i[0]),sE(i[1])])}function F2(t,n,e){let i=Hr(t);R2(i);let r=[i[Yi.ExtraData][0],i[Yi.ExtraData][1]],o=Jo(r,n)||[];return Jo(o,e)||[]}function Jo(t,n){for(let e=n;e>-1;e--)if(typeof t[e]<"u")return t[e];throw new ue(2304,!1)}function sE(t){let[n,e]=t.split(":");return{hours:+n,minutes:+e}}function N2(t,n,e="en"){let i=Zq(e)[t]||S2[t]||[],r=i[1];return n==="narrow"&&typeof r=="string"?r:i[0]||t}var Xq=2;function L2(t){let n,e=S2[t];return e&&(n=e[2]),typeof n=="number"?n:Xq}var Jq=/^(\d{4,})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/,H0={},eY=/((?:[^BEGHLMOSWYZabcdhmswyz']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|Y{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|c{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/;function V2(t,n,e,i){let r=cY(t);n=vl(e,n)||n;let a=[],s;for(;n;)if(s=eY.exec(n),s){a=a.concat(s.slice(1));let d=a.pop();if(!d)break;n=d}else{a.push(n);break}let l=r.getTimezoneOffset();i&&(l=j2(i,l),r=lY(r,i));let c="";return a.forEach(d=>{let p=aY(d);c+=p?p(r,e,l):d==="''"?"'":d.replace(/(^'|'$)/g,"").replace(/''/g,"'")}),c}function G0(t,n,e){let i=new Date(0);return i.setFullYear(t,n,e),i.setHours(0,0,0),i}function vl(t,n){let e=k2(t);if(H0[e]??={},H0[e][n])return H0[e][n];let i="";switch(n){case"shortDate":i=gf(t,no.Short);break;case"mediumDate":i=gf(t,no.Medium);break;case"longDate":i=gf(t,no.Long);break;case"fullDate":i=gf(t,no.Full);break;case"shortTime":i=_f(t,no.Short);break;case"mediumTime":i=_f(t,no.Medium);break;case"longTime":i=_f(t,no.Long);break;case"fullTime":i=_f(t,no.Full);break;case"short":let r=vl(t,"shortTime"),o=vl(t,"shortDate");i=z0(bf(t,no.Short),[r,o]);break;case"medium":let a=vl(t,"mediumTime"),s=vl(t,"mediumDate");i=z0(bf(t,no.Medium),[a,s]);break;case"long":let l=vl(t,"longTime"),c=vl(t,"longDate");i=z0(bf(t,no.Long),[l,c]);break;case"full":let d=vl(t,"fullTime"),p=vl(t,"fullDate");i=z0(bf(t,no.Full),[d,p]);break}return i&&(H0[e][n]=i),i}function z0(t,n){return n&&(t=t.replace(/\{([^}]+)}/g,function(e,i){return n!=null&&i in n?n[i]:e})),t}function Ua(t,n,e="-",i,r){let o="";(t<0||r&&t<=0)&&(r?t=-t+1:(t=-t,o=e));let a=String(t);for(;a.length0||s>-e)&&(s+=e),t===3)s===0&&e===-12&&(s=12);else if(t===6)return tY(s,n);let l=$a(a,nr.MinusSign);return Ua(s,n,l,i,r)}}function iY(t,n){switch(t){case 0:return n.getFullYear();case 1:return n.getMonth();case 2:return n.getDate();case 3:return n.getHours();case 4:return n.getMinutes();case 5:return n.getSeconds();case 6:return n.getMilliseconds();case 7:return n.getDay();default:throw new ue(2301,!1)}}function Ii(t,n,e=Er.Format,i=!1){return function(r,o){return nY(r,o,t,n,e,i)}}function nY(t,n,e,i,r,o){switch(e){case 2:return A2(n,r,i)[t.getMonth()];case 1:return I2(n,r,i)[t.getDay()];case 0:let a=t.getHours(),s=t.getMinutes();if(o){let c=P2(n),d=F2(n,r,i),p=c.findIndex(_=>{if(Array.isArray(_)){let[b,y]=_,w=a>=b.hours&&s>=b.minutes,C=a0?Math.floor(r/60):Math.ceil(r/60);switch(t){case 0:return(r>=0?"+":"")+Ua(a,2,o)+Ua(Math.abs(r%60),2,o);case 1:return"GMT"+(r>=0?"+":"")+Ua(a,1,o);case 2:return"GMT"+(r>=0?"+":"")+Ua(a,2,o)+":"+Ua(Math.abs(r%60),2,o);case 3:return i===0?"Z":(r>=0?"+":"")+Ua(a,2,o)+":"+Ua(Math.abs(r%60),2,o);default:throw new ue(2310,!1)}}}var rY=0,W0=4;function oY(t){let n=G0(t,rY,1).getDay();return G0(t,0,1+(n<=W0?W0:W0+7)-n)}function B2(t){let n=t.getDay(),e=n===0?-3:W0-n;return G0(t.getFullYear(),t.getMonth(),t.getDate()+e)}function lE(t,n=!1){return function(e,i){let r;if(n){let o=new Date(e.getFullYear(),e.getMonth(),1).getDay()-1,a=e.getDate();r=1+Math.floor((a+o)/7)}else{let o=B2(e),a=oY(o.getFullYear()),s=o.getTime()-a.getTime();r=1+Math.round(s/6048e5)}return Ua(r,t,$a(i,nr.MinusSign))}}function $0(t,n=!1){return function(e,i){let o=B2(e).getFullYear();return Ua(o,t,$a(i,nr.MinusSign),n)}}var cE={};function aY(t){if(cE[t])return cE[t];let n;switch(t){case"G":case"GG":case"GGG":n=Ii(3,yi.Abbreviated);break;case"GGGG":n=Ii(3,yi.Wide);break;case"GGGGG":n=Ii(3,yi.Narrow);break;case"y":n=gn(0,1,0,!1,!0);break;case"yy":n=gn(0,2,0,!0,!0);break;case"yyy":n=gn(0,3,0,!1,!0);break;case"yyyy":n=gn(0,4,0,!1,!0);break;case"Y":n=$0(1);break;case"YY":n=$0(2,!0);break;case"YYY":n=$0(3);break;case"YYYY":n=$0(4);break;case"M":case"L":n=gn(1,1,1);break;case"MM":case"LL":n=gn(1,2,1);break;case"MMM":n=Ii(2,yi.Abbreviated);break;case"MMMM":n=Ii(2,yi.Wide);break;case"MMMMM":n=Ii(2,yi.Narrow);break;case"LLL":n=Ii(2,yi.Abbreviated,Er.Standalone);break;case"LLLL":n=Ii(2,yi.Wide,Er.Standalone);break;case"LLLLL":n=Ii(2,yi.Narrow,Er.Standalone);break;case"w":n=lE(1);break;case"ww":n=lE(2);break;case"W":n=lE(1,!0);break;case"d":n=gn(2,1);break;case"dd":n=gn(2,2);break;case"c":case"cc":n=gn(7,1);break;case"ccc":n=Ii(1,yi.Abbreviated,Er.Standalone);break;case"cccc":n=Ii(1,yi.Wide,Er.Standalone);break;case"ccccc":n=Ii(1,yi.Narrow,Er.Standalone);break;case"cccccc":n=Ii(1,yi.Short,Er.Standalone);break;case"E":case"EE":case"EEE":n=Ii(1,yi.Abbreviated);break;case"EEEE":n=Ii(1,yi.Wide);break;case"EEEEE":n=Ii(1,yi.Narrow);break;case"EEEEEE":n=Ii(1,yi.Short);break;case"a":case"aa":case"aaa":n=Ii(0,yi.Abbreviated);break;case"aaaa":n=Ii(0,yi.Wide);break;case"aaaaa":n=Ii(0,yi.Narrow);break;case"b":case"bb":case"bbb":n=Ii(0,yi.Abbreviated,Er.Standalone,!0);break;case"bbbb":n=Ii(0,yi.Wide,Er.Standalone,!0);break;case"bbbbb":n=Ii(0,yi.Narrow,Er.Standalone,!0);break;case"B":case"BB":case"BBB":n=Ii(0,yi.Abbreviated,Er.Format,!0);break;case"BBBB":n=Ii(0,yi.Wide,Er.Format,!0);break;case"BBBBB":n=Ii(0,yi.Narrow,Er.Format,!0);break;case"h":n=gn(3,1,-12);break;case"hh":n=gn(3,2,-12);break;case"H":n=gn(3,1);break;case"HH":n=gn(3,2);break;case"m":n=gn(4,1);break;case"mm":n=gn(4,2);break;case"s":n=gn(5,1);break;case"ss":n=gn(5,2);break;case"S":n=gn(6,1);break;case"SS":n=gn(6,2);break;case"SSS":n=gn(6,3);break;case"Z":case"ZZ":case"ZZZ":n=U0(0);break;case"ZZZZZ":n=U0(3);break;case"O":case"OO":case"OOO":case"z":case"zz":case"zzz":n=U0(1);break;case"OOOO":case"ZZZZ":case"zzzz":n=U0(2);break;default:return null}return cE[t]=n,n}function j2(t,n){t=t.replace(/:/g,"");let e=Date.parse("Jan 01, 1970 00:00:00 "+t)/6e4;return isNaN(e)?n:e}function sY(t,n){return t=new Date(t.getTime()),t.setMinutes(t.getMinutes()+n),t}function lY(t,n,e){let r=t.getTimezoneOffset(),o=j2(n,r);return sY(t,-1*(o-r))}function cY(t){if(x2(t))return t;if(typeof t=="number"&&!isNaN(t))return new Date(t);if(typeof t=="string"){if(t=t.trim(),/^(\d{4}(-\d{1,2}(-\d{1,2})?)?)$/.test(t)){let[r,o=1,a=1]=t.split("-").map(s=>+s);return G0(r,o-1,a)}let e=parseFloat(t);if(!isNaN(t-e))return new Date(e);let i;if(i=t.match(Jq))return dY(i)}let n=new Date(t);if(!x2(n))throw new ue(2311,!1);return n}function dY(t){let n=new Date(0),e=0,i=0,r=t[8]?n.setUTCFullYear:n.setFullYear,o=t[8]?n.setUTCHours:n.setHours;t[9]&&(e=Number(t[9]+t[10]),i=Number(t[9]+t[11])),r.call(n,Number(t[1]),Number(t[2])-1,Number(t[3]));let a=Number(t[4]||0)-e,s=Number(t[5]||0)-i,l=Number(t[6]||0),c=Math.floor(parseFloat("0."+(t[7]||0))*1e3);return o.call(n,a,s,l,c),n}function x2(t){return t instanceof Date&&!isNaN(t.valueOf())}var uY=/^(\d+)?\.((\d+)(-(\d+))?)?$/,C2=22,q0=".",vf="0",mY=";",hY=",",dE="#",w2="\xA4";function H2(t,n,e,i,r,o,a=!1){let s="",l=!1;if(!isFinite(t))s=$a(e,nr.Infinity);else{let c=fY(t);a&&(c=pY(c));let d=n.minInt,p=n.minFrac,_=n.maxFrac;if(o){let F=o.match(uY);if(F===null)throw new ue(2306,!1);let W=F[1],Z=F[3],K=F[5];W!=null&&(d=uE(W)),Z!=null&&(p=uE(Z)),K!=null?_=uE(K):Z!=null&&p>_&&(_=p)}gY(c,p,_);let b=c.digits,y=c.integerLen,w=c.exponent,C=[];for(l=b.every(F=>!F);y0?C=b.splice(y,b.length):(C=b,b=[0]);let D=[];for(b.length>=n.lgSize&&D.unshift(b.splice(-n.lgSize,b.length).join(""));b.length>n.gSize;)D.unshift(b.splice(-n.gSize,b.length).join(""));b.length&&D.unshift(b.join("")),s=D.join($a(e,i)),C.length&&(s+=$a(e,r)+C.join("")),w&&(s+=$a(e,nr.Exponential)+"+"+w)}return t<0&&!l?s=n.negPre+s+n.negSuf:s=n.posPre+s+n.posSuf,s}function z2(t,n,e,i,r){let o=gE(n,K0.Currency),a=$2(o,$a(n,nr.MinusSign));return a.minFrac=L2(i),a.maxFrac=a.minFrac,H2(t,a,n,nr.CurrencyGroup,nr.CurrencyDecimal,r).replace(w2,e).replace(w2,"").trim()}function U2(t,n,e){let i=gE(n,K0.Decimal),r=$2(i,$a(n,nr.MinusSign));return H2(t,r,n,nr.Group,nr.Decimal,e)}function $2(t,n="-"){let e={minInt:1,minFrac:0,maxFrac:0,posPre:"",posSuf:"",negPre:"",negSuf:"",gSize:0,lgSize:0},i=t.split(mY),r=i[0],o=i[1],a=r.indexOf(q0)!==-1?r.split(q0):[r.substring(0,r.lastIndexOf(vf)+1),r.substring(r.lastIndexOf(vf)+1)],s=a[0],l=a[1]||"";e.posPre=s.substring(0,s.indexOf(dE));for(let d=0;d-1&&(n=n.replace(q0,"")),(o=n.search(/e/i))>0?(r<0&&(r=o),r+=+n.slice(o+1),n=n.substring(0,o)):r<0&&(r=n.length),o=0;n.charAt(o)===vf;o++);if(o===(s=n.length))i=[0],r=1;else{for(s--;n.charAt(s)===vf;)s--;for(r-=o,i=[],a=0;o<=s;o++,a++)i[a]=Number(n.charAt(o))}return r>C2&&(i=i.splice(0,C2-1),e=r-1,r=1),{digits:i,exponent:e,integerLen:r}}function gY(t,n,e){if(n>e)throw new ue(2307,!1);let i=t.digits,r=i.length-t.integerLen,o=Math.min(Math.max(n,r),e),a=o+t.integerLen,s=i[a];if(a>0){i.splice(Math.max(t.integerLen,a));for(let p=a;p=5)if(a-1<0){for(let p=0;p>a;p--)i.unshift(0),t.integerLen++;i.unshift(1),t.integerLen++}else i[a-1]++;for(;r=c?y.pop():l=!1),_>=10?1:0},0);d&&(i.unshift(d),t.integerLen++)}function uE(t){let n=parseInt(t);if(isNaN(n))throw new ue(2305,!1);return n}var mE=/\s+/,D2=[],zd=(()=>{class t{_ngEl;_renderer;initialClasses=D2;rawClass;stateMap=new Map;constructor(e,i){this._ngEl=e,this._renderer=i}set klass(e){this.initialClasses=e!=null?e.trim().split(mE):D2}set ngClass(e){this.rawClass=typeof e=="string"?e.trim().split(mE):e}ngDoCheck(){for(let i of this.initialClasses)this._updateState(i,!0);let e=this.rawClass;if(Array.isArray(e)||e instanceof Set)for(let i of e)this._updateState(i,!0);else if(e!=null)for(let i of Object.keys(e))this._updateState(i,!!e[i]);this._applyStateDiff()}_updateState(e,i){let r=this.stateMap.get(e);r!==void 0?(r.enabled!==i&&(r.changed=!0,r.enabled=i),r.touched=!0):this.stateMap.set(e,{enabled:i,changed:!0,touched:!0})}_applyStateDiff(){for(let e of this.stateMap){let i=e[0],r=e[1];r.changed?(this._toggleClass(i,r.enabled),r.changed=!1):r.touched||(r.enabled&&this._toggleClass(i,!1),this.stateMap.delete(i)),r.touched=!1}}_toggleClass(e,i){e=e.trim(),e.length>0&&e.split(mE).forEach(r=>{i?this._renderer.addClass(this._ngEl.nativeElement,r):this._renderer.removeClass(this._ngEl.nativeElement,r)})}static \u0275fac=function(i){return new(i||t)(be(Y),be(ze))};static \u0275dir=P({type:t,selectors:[["","ngClass",""]],inputs:{klass:[0,"class","klass"],ngClass:"ngClass"}})}return t})();var Y0=class{$implicit;ngForOf;index;count;constructor(n,e,i,r){this.$implicit=n,this.ngForOf=e,this.index=i,this.count=r}get first(){return this.index===0}get last(){return this.index===this.count-1}get even(){return this.index%2===0}get odd(){return!this.even}},Un=(()=>{class t{_viewContainer;_template;_differs;set ngForOf(e){this._ngForOf=e,this._ngForOfDirty=!0}set ngForTrackBy(e){this._trackByFn=e}get ngForTrackBy(){return this._trackByFn}_ngForOf=null;_ngForOfDirty=!0;_differ=null;_trackByFn;constructor(e,i,r){this._viewContainer=e,this._template=i,this._differs=r}set ngForTemplate(e){e&&(this._template=e)}ngDoCheck(){if(this._ngForOfDirty){this._ngForOfDirty=!1;let e=this._ngForOf;!this._differ&&e&&(this._differ=this._differs.find(e).create(this.ngForTrackBy))}if(this._differ){let e=this._differ.diff(this._ngForOf);e&&this._applyChanges(e)}}_applyChanges(e){let i=this._viewContainer;e.forEachOperation((r,o,a)=>{if(r.previousIndex==null)i.createEmbeddedView(this._template,new Y0(r.item,this._ngForOf,-1,-1),a===null?void 0:a);else if(a==null)i.remove(o===null?void 0:o);else if(o!==null){let s=i.get(o);i.move(s,a),M2(s,r)}});for(let r=0,o=i.length;r{let o=i.get(r.currentIndex);M2(o,r)})}static ngTemplateContextGuard(e,i){return!0}static \u0275fac=function(i){return new(i||t)(be(st),be(te),be(Ss))};static \u0275dir=P({type:t,selectors:[["","ngFor","","ngForOf",""]],inputs:{ngForOf:"ngForOf",ngForTrackBy:"ngForTrackBy",ngForTemplate:"ngForTemplate"}})}return t})();function M2(t,n){t.context.$implicit=n.item}var Wt=(()=>{class t{_viewContainer;_context=new Q0;_thenTemplateRef=null;_elseTemplateRef=null;_thenViewRef=null;_elseViewRef=null;constructor(e,i){this._viewContainer=e,this._thenTemplateRef=i}set ngIf(e){this._context.$implicit=this._context.ngIf=e,this._updateView()}set ngIfThen(e){E2(e,!1),this._thenTemplateRef=e,this._thenViewRef=null,this._updateView()}set ngIfElse(e){E2(e,!1),this._elseTemplateRef=e,this._elseViewRef=null,this._updateView()}_updateView(){this._context.$implicit?this._thenViewRef||(this._viewContainer.clear(),this._elseViewRef=null,this._thenTemplateRef&&(this._thenViewRef=this._viewContainer.createEmbeddedView(this._thenTemplateRef,this._context))):this._elseViewRef||(this._viewContainer.clear(),this._thenViewRef=null,this._elseTemplateRef&&(this._elseViewRef=this._viewContainer.createEmbeddedView(this._elseTemplateRef,this._context)))}static ngIfUseIfTypeGuard;static ngTemplateGuard_ngIf;static ngTemplateContextGuard(e,i){return!0}static \u0275fac=function(i){return new(i||t)(be(st),be(te))};static \u0275dir=P({type:t,selectors:[["","ngIf",""]],inputs:{ngIf:"ngIf",ngIfThen:"ngIfThen",ngIfElse:"ngIfElse"}})}return t})(),Q0=class{$implicit=null;ngIf=null};function E2(t,n){if(t&&!t.createEmbeddedView)throw new ue(2020,!1)}var $n=(()=>{class t{_viewContainerRef;_viewRef=null;ngTemplateOutletContext=null;ngTemplateOutlet=null;ngTemplateOutletInjector=null;constructor(e){this._viewContainerRef=e}ngOnChanges(e){if(this._shouldRecreateView(e)){let i=this._viewContainerRef;if(this._viewRef&&i.remove(i.indexOf(this._viewRef)),!this.ngTemplateOutlet){this._viewRef=null;return}let r=this._createContextForwardProxy();this._viewRef=i.createEmbeddedView(this.ngTemplateOutlet,r,{injector:this.ngTemplateOutletInjector??void 0})}}_shouldRecreateView(e){return!!e.ngTemplateOutlet||!!e.ngTemplateOutletInjector}_createContextForwardProxy(){return new Proxy({},{set:(e,i,r)=>this.ngTemplateOutletContext?Reflect.set(this.ngTemplateOutletContext,i,r):!1,get:(e,i,r)=>{if(this.ngTemplateOutletContext)return Reflect.get(this.ngTemplateOutletContext,i,r)}})}static \u0275fac=function(i){return new(i||t)(be(st))};static \u0275dir=P({type:t,selectors:[["","ngTemplateOutlet",""]],inputs:{ngTemplateOutletContext:"ngTemplateOutletContext",ngTemplateOutlet:"ngTemplateOutlet",ngTemplateOutletInjector:"ngTemplateOutletInjector"},features:[Oe]})}return t})();function yf(t,n){return new ue(2100,!1)}var hE=class{createSubscription(n,e,i){return Ni(()=>n.subscribe({next:e,error:i}))}dispose(n){Ni(()=>n.unsubscribe())}},pE=class{createSubscription(n,e,i){return n.then(r=>e?.(r),r=>i?.(r)),{unsubscribe:()=>{e=null,i=null}}}dispose(n){n.unsubscribe()}},_Y=new pE,bY=new hE,cn=(()=>{class t{_ref;_latestValue=null;markForCheckOnValueUpdate=!0;_subscription=null;_obj=null;_strategy=null;applicationErrorHandler=u(wr);constructor(e){this._ref=e}ngOnDestroy(){this._subscription&&this._dispose(),this._ref=null}transform(e){if(!this._obj){if(e)try{this.markForCheckOnValueUpdate=!1,this._subscribe(e)}finally{this.markForCheckOnValueUpdate=!0}return this._latestValue}return e!==this._obj?(this._dispose(),this.transform(e)):this._latestValue}_subscribe(e){this._obj=e,this._strategy=this._selectStrategy(e),this._subscription=this._strategy.createSubscription(e,i=>this._updateLatestValue(e,i),i=>this.applicationErrorHandler(i))}_selectStrategy(e){if(fl(e))return _Y;if(I0(e))return bY;throw yf(t,e)}_dispose(){this._strategy.dispose(this._subscription),this._latestValue=null,this._subscription=null,this._obj=null}_updateLatestValue(e,i){e===this._obj&&(this._latestValue=i,this.markForCheckOnValueUpdate&&this._ref?.markForCheck())}static \u0275fac=function(i){return new(i||t)(be(ye,16))};static \u0275pipe=io({name:"async",type:t,pure:!1})}return t})();var vY="mediumDate",W2=new O(""),G2=new O(""),Wa=(()=>{class t{locale;defaultTimezone;defaultOptions;constructor(e,i,r){this.locale=e,this.defaultTimezone=i,this.defaultOptions=r}transform(e,i,r,o){if(e==null||e===""||e!==e)return null;try{let a=i??this.defaultOptions?.dateFormat??vY,s=r??this.defaultOptions?.timezone??this.defaultTimezone??void 0;return V2(e,a,o||this.locale,s)}catch(a){throw yf(t,a.message)}}static \u0275fac=function(i){return new(i||t)(be(bl,16),be(W2,24),be(G2,24))};static \u0275pipe=io({name:"date",type:t,pure:!0})}return t})();var xf=(()=>{class t{_locale;constructor(e){this._locale=e}transform(e,i,r){if(!q2(e))return null;r||=this._locale;try{let o=Y2(e);return U2(o,r,i)}catch(o){throw yf(t,o.message)}}static \u0275fac=function(i){return new(i||t)(be(bl,16))};static \u0275pipe=io({name:"number",type:t,pure:!0})}return t})();var yl=(()=>{class t{_locale;_defaultCurrencyCode;constructor(e,i="USD"){this._locale=e,this._defaultCurrencyCode=i}transform(e,i=this._defaultCurrencyCode,r="symbol",o,a){if(!q2(e))return null;a||=this._locale,typeof r=="boolean"&&(r=r?"symbol":"code");let s=i||this._defaultCurrencyCode;r!=="code"&&(r==="symbol"||r==="symbol-narrow"?s=N2(s,r==="symbol"?"wide":"narrow",a):s=r);try{let l=Y2(e);return z2(l,a,s,i,o)}catch(l){throw yf(t,l.message)}}static \u0275fac=function(i){return new(i||t)(be(bl,16),be(YM,16))};static \u0275pipe=io({name:"currency",type:t,pure:!0})}return t})();function q2(t){return!(t==null||t===""||t!==t)}function Y2(t){if(typeof t=="string"&&!isNaN(Number(t)-parseFloat(t)))return Number(t);if(typeof t!="number")throw new ue(2309,!1);return t}var _E=(()=>{class t{transform(e,i,r){if(e==null)return null;if(!(typeof e=="string"||Array.isArray(e)))throw yf(t,e);return e.slice(i,r)}static \u0275fac=function(i){return new(i||t)};static \u0275pipe=io({name:"slice",type:t,pure:!1})}return t})();var Je=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})();function Cf(t,n){n=encodeURIComponent(n);for(let e of t.split(";")){let i=e.indexOf("="),[r,o]=i==-1?[e,""]:[e.slice(0,i),e.slice(i+1)];if(r.trim()===n)return decodeURIComponent(o)}return null}var Ud=class{};var vE="browser",yY="server";function Q2(t){return t===vE}function K2(t){return t===yY}var yE=(()=>{class t{static \u0275prov=R({token:t,providedIn:"root",factory:()=>new bE(u(_e),window)})}return t})(),bE=class{document;window;offset=()=>[0,0];constructor(n,e){this.document=n,this.window=e}setOffset(n){Array.isArray(n)?this.offset=()=>n:this.offset=n}getScrollPosition(){return[this.window.scrollX,this.window.scrollY]}scrollToPosition(n,e){this.window.scrollTo(Me(I({},e),{left:n[0],top:n[1]}))}scrollToAnchor(n,e){let i=xY(this.document,n);i&&(this.scrollToElement(i,e),i.focus())}setHistoryScrollRestoration(n){try{this.window.history.scrollRestoration=n}catch{console.warn(zo(2400,!1))}}scrollToElement(n,e){let i=n.getBoundingClientRect(),r=i.left+this.window.pageXOffset,o=i.top+this.window.pageYOffset,a=this.offset();this.window.scrollTo(Me(I({},e),{left:r-a[0],top:o-a[1]}))}};function xY(t,n){let e=t.getElementById(n)||t.getElementsByName(n)[0];if(e)return e;if(typeof t.createTreeWalker=="function"&&t.body&&typeof t.body.attachShadow=="function"){let i=t.createTreeWalker(t.body,NodeFilter.SHOW_ELEMENT),r=i.currentNode;for(;r;){let o=r.shadowRoot;if(o){let a=o.getElementById(n)||o.querySelector(`[name="${n}"]`);if(a)return a}r=i.nextNode()}}return null}var wf=class{_doc;constructor(n){this._doc=n}manager},Z0=(()=>{class t extends wf{constructor(e){super(e)}supports(e){return!0}addEventListener(e,i,r,o){return e.addEventListener(i,r,o),()=>this.removeEventListener(e,i,r,o)}removeEventListener(e,i,r,o){return e.removeEventListener(i,r,o)}static \u0275fac=function(i){return new(i||t)(pe(_e))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),J0=new O(""),ME=(()=>{class t{_zone;_plugins;_eventNameToPlugin=new Map;constructor(e,i){this._zone=i,e.forEach(a=>{a.manager=this});let r=e.filter(a=>!(a instanceof Z0));this._plugins=r.slice().reverse();let o=e.find(a=>a instanceof Z0);o&&this._plugins.push(o)}addEventListener(e,i,r,o){return this._findPluginFor(i).addEventListener(e,i,r,o)}getZone(){return this._zone}_findPluginFor(e){let i=this._eventNameToPlugin.get(e);if(i)return i;if(i=this._plugins.find(o=>o.supports(e)),!i)throw new ue(5101,!1);return this._eventNameToPlugin.set(e,i),i}static \u0275fac=function(i){return new(i||t)(pe(J0),pe(ae))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),xE="ng-app-id";function Z2(t){for(let n of t)n.remove()}function X2(t,n){let e=n.createElement("style");return e.textContent=t,e}function CY(t,n,e,i){let r=t.head?.querySelectorAll(`style[${xE}="${n}"],link[${xE}="${n}"]`);if(r)for(let o of r)o.removeAttribute(xE),o instanceof HTMLLinkElement?i.set(o.href.slice(o.href.lastIndexOf("/")+1),{usage:0,elements:[o]}):o.textContent&&e.set(o.textContent,{usage:0,elements:[o]})}function wE(t,n){let e=n.createElement("link");return e.setAttribute("rel","stylesheet"),e.setAttribute("href",t),e}var EE=(()=>{class t{doc;appId;nonce;inline=new Map;external=new Map;hosts=new Set;constructor(e,i,r,o={}){this.doc=e,this.appId=i,this.nonce=r,CY(e,i,this.inline,this.external),this.hosts.add(e.head)}addStyles(e,i){for(let r of e)this.addUsage(r,this.inline,X2);i?.forEach(r=>this.addUsage(r,this.external,wE))}removeStyles(e,i){for(let r of e)this.removeUsage(r,this.inline);i?.forEach(r=>this.removeUsage(r,this.external))}addUsage(e,i,r){let o=i.get(e);o?o.usage++:i.set(e,{usage:1,elements:[...this.hosts].map(a=>this.addElement(a,r(e,this.doc)))})}removeUsage(e,i){let r=i.get(e);r&&(r.usage--,r.usage<=0&&(Z2(r.elements),i.delete(e)))}ngOnDestroy(){for(let[,{elements:e}]of[...this.inline,...this.external])Z2(e);this.hosts.clear()}addHost(e){this.hosts.add(e);for(let[i,{elements:r}]of this.inline)r.push(this.addElement(e,X2(i,this.doc)));for(let[i,{elements:r}]of this.external)r.push(this.addElement(e,wE(i,this.doc)))}removeHost(e){this.hosts.delete(e)}addElement(e,i){return this.nonce&&i.setAttribute("nonce",this.nonce),e.appendChild(i)}static \u0275fac=function(i){return new(i||t)(pe(_e),pe(uc),pe(xm,8),pe(hl))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),CE={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/",math:"http://www.w3.org/1998/Math/MathML"},SE=/%COMP%/g;var eN="%COMP%",wY=`_nghost-${eN}`,DY=`_ngcontent-${eN}`,MY=!0,EY=new O("",{providedIn:"root",factory:()=>MY});function SY(t){return DY.replace(SE,t)}function kY(t){return wY.replace(SE,t)}function tN(t,n){return n.map(e=>e.replace(SE,t))}var kE=(()=>{class t{eventManager;sharedStylesHost;appId;removeStylesOnCompDestroy;doc;ngZone;nonce;tracingService;rendererByCompId=new Map;defaultRenderer;platformIsServer;constructor(e,i,r,o,a,s,l=null,c=null){this.eventManager=e,this.sharedStylesHost=i,this.appId=r,this.removeStylesOnCompDestroy=o,this.doc=a,this.ngZone=s,this.nonce=l,this.tracingService=c,this.platformIsServer=!1,this.defaultRenderer=new Df(e,a,s,this.platformIsServer,this.tracingService)}createRenderer(e,i){if(!e||!i)return this.defaultRenderer;let r=this.getOrCreateRenderer(e,i);return r instanceof X0?r.applyToHost(e):r instanceof Mf&&r.applyStyles(),r}getOrCreateRenderer(e,i){let r=this.rendererByCompId,o=r.get(i.id);if(!o){let a=this.doc,s=this.ngZone,l=this.eventManager,c=this.sharedStylesHost,d=this.removeStylesOnCompDestroy,p=this.platformIsServer,_=this.tracingService;switch(i.encapsulation){case ul.Emulated:o=new X0(l,c,i,this.appId,d,a,s,p,_);break;case ul.ShadowDom:return new DE(l,c,e,i,a,s,this.nonce,p,_);default:o=new Mf(l,c,i,d,a,s,p,_);break}r.set(i.id,o)}return o}ngOnDestroy(){this.rendererByCompId.clear()}componentReplaced(e){this.rendererByCompId.delete(e)}static \u0275fac=function(i){return new(i||t)(pe(ME),pe(EE),pe(uc),pe(EY),pe(_e),pe(ae),pe(xm),pe(mc,8))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),Df=class{eventManager;doc;ngZone;platformIsServer;tracingService;data=Object.create(null);throwOnSyntheticProps=!0;constructor(n,e,i,r,o){this.eventManager=n,this.doc=e,this.ngZone=i,this.platformIsServer=r,this.tracingService=o}destroy(){}destroyNode=null;createElement(n,e){return e?this.doc.createElementNS(CE[e]||e,n):this.doc.createElement(n)}createComment(n){return this.doc.createComment(n)}createText(n){return this.doc.createTextNode(n)}appendChild(n,e){(J2(n)?n.content:n).appendChild(e)}insertBefore(n,e,i){n&&(J2(n)?n.content:n).insertBefore(e,i)}removeChild(n,e){e.remove()}selectRootElement(n,e){let i=typeof n=="string"?this.doc.querySelector(n):n;if(!i)throw new ue(-5104,!1);return e||(i.textContent=""),i}parentNode(n){return n.parentNode}nextSibling(n){return n.nextSibling}setAttribute(n,e,i,r){if(r){e=r+":"+e;let o=CE[r];o?n.setAttributeNS(o,e,i):n.setAttribute(e,i)}else n.setAttribute(e,i)}removeAttribute(n,e,i){if(i){let r=CE[i];r?n.removeAttributeNS(r,e):n.removeAttribute(`${i}:${e}`)}else n.removeAttribute(e)}addClass(n,e){n.classList.add(e)}removeClass(n,e){n.classList.remove(e)}setStyle(n,e,i,r){r&(Cs.DashCase|Cs.Important)?n.style.setProperty(e,i,r&Cs.Important?"important":""):n.style[e]=i}removeStyle(n,e,i){i&Cs.DashCase?n.style.removeProperty(e):n.style[e]=""}setProperty(n,e,i){n!=null&&(n[e]=i)}setValue(n,e){n.nodeValue=e}listen(n,e,i,r){if(typeof n=="string"&&(n=Xo().getGlobalEventTarget(this.doc,n),!n))throw new ue(5102,!1);let o=this.decoratePreventDefault(i);return this.tracingService?.wrapEventListener&&(o=this.tracingService.wrapEventListener(n,e,o)),this.eventManager.addEventListener(n,e,o,r)}decoratePreventDefault(n){return e=>{if(e==="__ngUnwrap__")return n;n(e)===!1&&e.preventDefault()}}};function J2(t){return t.tagName==="TEMPLATE"&&t.content!==void 0}var DE=class extends Df{sharedStylesHost;hostEl;shadowRoot;constructor(n,e,i,r,o,a,s,l,c){super(n,o,a,l,c),this.sharedStylesHost=e,this.hostEl=i,this.shadowRoot=i.attachShadow({mode:"open"}),this.sharedStylesHost.addHost(this.shadowRoot);let d=r.styles;d=tN(r.id,d);for(let _ of d){let b=document.createElement("style");s&&b.setAttribute("nonce",s),b.textContent=_,this.shadowRoot.appendChild(b)}let p=r.getExternalStyles?.();if(p)for(let _ of p){let b=wE(_,o);s&&b.setAttribute("nonce",s),this.shadowRoot.appendChild(b)}}nodeOrShadowRoot(n){return n===this.hostEl?this.shadowRoot:n}appendChild(n,e){return super.appendChild(this.nodeOrShadowRoot(n),e)}insertBefore(n,e,i){return super.insertBefore(this.nodeOrShadowRoot(n),e,i)}removeChild(n,e){return super.removeChild(null,e)}parentNode(n){return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(n)))}destroy(){this.sharedStylesHost.removeHost(this.shadowRoot)}},Mf=class extends Df{sharedStylesHost;removeStylesOnCompDestroy;styles;styleUrls;constructor(n,e,i,r,o,a,s,l,c){super(n,o,a,s,l),this.sharedStylesHost=e,this.removeStylesOnCompDestroy=r;let d=i.styles;this.styles=c?tN(c,d):d,this.styleUrls=i.getExternalStyles?.(c)}applyStyles(){this.sharedStylesHost.addStyles(this.styles,this.styleUrls)}destroy(){this.removeStylesOnCompDestroy&&Nd.size===0&&this.sharedStylesHost.removeStyles(this.styles,this.styleUrls)}},X0=class extends Mf{contentAttr;hostAttr;constructor(n,e,i,r,o,a,s,l,c){let d=r+"-"+i.id;super(n,e,i,o,a,s,l,c,d),this.contentAttr=SY(d),this.hostAttr=kY(d)}applyToHost(n){this.applyStyles(),this.setAttribute(n,this.hostAttr,"")}createElement(n,e){let i=super.createElement(n,e);return super.setAttribute(i,this.contentAttr,""),i}};var ev=class t extends pf{supportsDOMEvents=!0;static makeCurrent(){oE(new t)}onAndCancel(n,e,i,r){return n.addEventListener(e,i,r),()=>{n.removeEventListener(e,i,r)}}dispatchEvent(n,e){n.dispatchEvent(e)}remove(n){n.remove()}createElement(n,e){return e=e||this.getDefaultDocument(),e.createElement(n)}createHtmlDocument(){return document.implementation.createHTMLDocument("fakeTitle")}getDefaultDocument(){return document}isElementNode(n){return n.nodeType===Node.ELEMENT_NODE}isShadowRoot(n){return n instanceof DocumentFragment}getGlobalEventTarget(n,e){return e==="window"?window:e==="document"?n:e==="body"?n.body:null}getBaseHref(n){let e=TY();return e==null?null:IY(e)}resetBaseElement(){Ef=null}getUserAgent(){return window.navigator.userAgent}getCookie(n){return Cf(document.cookie,n)}},Ef=null;function TY(){return Ef=Ef||document.head.querySelector("base"),Ef?Ef.getAttribute("href"):null}function IY(t){return new URL(t,document.baseURI).pathname}var AY=(()=>{class t{build(){return new XMLHttpRequest}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),iN=["alt","control","meta","shift"],OY={"\b":"Backspace"," ":"Tab","\x7F":"Delete","\x1B":"Escape",Del:"Delete",Esc:"Escape",Left:"ArrowLeft",Right:"ArrowRight",Up:"ArrowUp",Down:"ArrowDown",Menu:"ContextMenu",Scroll:"ScrollLock",Win:"OS"},RY={alt:t=>t.altKey,control:t=>t.ctrlKey,meta:t=>t.metaKey,shift:t=>t.shiftKey},nN=(()=>{class t extends wf{constructor(e){super(e)}supports(e){return t.parseEventName(e)!=null}addEventListener(e,i,r,o){let a=t.parseEventName(i),s=t.eventCallback(a.fullKey,r,this.manager.getZone());return this.manager.getZone().runOutsideAngular(()=>Xo().onAndCancel(e,a.domEventName,s,o))}static parseEventName(e){let i=e.toLowerCase().split("."),r=i.shift();if(i.length===0||!(r==="keydown"||r==="keyup"))return null;let o=t._normalizeKey(i.pop()),a="",s=i.indexOf("code");if(s>-1&&(i.splice(s,1),a="code."),iN.forEach(c=>{let d=i.indexOf(c);d>-1&&(i.splice(d,1),a+=c+".")}),a+=o,i.length!=0||o.length===0)return null;let l={};return l.domEventName=r,l.fullKey=a,l}static matchEventFullKeyCode(e,i){let r=OY[e.key]||e.key,o="";return i.indexOf("code.")>-1&&(r=e.code,o="code."),r==null||!r?!1:(r=r.toLowerCase(),r===" "?r="space":r==="."&&(r="dot"),iN.forEach(a=>{if(a!==r){let s=RY[a];s(e)&&(o+=a+".")}}),o+=r,o===i)}static eventCallback(e,i,r){return o=>{t.matchEventFullKeyCode(o,e)&&r.runGuarded(()=>i(o))}}static _normalizeKey(e){return e==="esc"?"escape":e}static \u0275fac=function(i){return new(i||t)(pe(_e))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();function TE(t,n,e){let i=I({rootComponent:t,platformRef:e?.platformRef},PY(n));return h2(i)}function PY(t){return{appProviders:[...BY,...t?.providers??[]],platformProviders:VY}}function FY(){ev.makeCurrent()}function NY(){return new Ln}function LY(){return aM(document),document}var VY=[{provide:hl,useValue:vE},{provide:m0,useValue:FY,multi:!0},{provide:_e,useFactory:LY}];var BY=[{provide:Rp,useValue:"root"},{provide:Ln,useFactory:NY},{provide:J0,useClass:Z0,multi:!0,deps:[_e]},{provide:J0,useClass:nN,multi:!0,deps:[_e]},kE,EE,ME,{provide:hn,useExisting:kE},{provide:Ud,useClass:AY},[]];var Pm=class{},fc=class{},Sr=class t{headers;normalizedNames=new Map;lazyInit;lazyUpdate=null;constructor(n){n?typeof n=="string"?this.lazyInit=()=>{this.headers=new Map,n.split(` +`).forEach(e=>{let i=e.indexOf(":");if(i>0){let r=e.slice(0,i),o=e.slice(i+1).trim();this.addHeaderEntry(r,o)}})}:typeof Headers<"u"&&n instanceof Headers?(this.headers=new Map,n.forEach((e,i)=>{this.addHeaderEntry(i,e)})):this.lazyInit=()=>{this.headers=new Map,Object.entries(n).forEach(([e,i])=>{this.setHeaderEntries(e,i)})}:this.headers=new Map}has(n){return this.init(),this.headers.has(n.toLowerCase())}get(n){this.init();let e=this.headers.get(n.toLowerCase());return e&&e.length>0?e[0]:null}keys(){return this.init(),Array.from(this.normalizedNames.values())}getAll(n){return this.init(),this.headers.get(n.toLowerCase())||null}append(n,e){return this.clone({name:n,value:e,op:"a"})}set(n,e){return this.clone({name:n,value:e,op:"s"})}delete(n,e){return this.clone({name:n,value:e,op:"d"})}maybeSetNormalizedName(n,e){this.normalizedNames.has(e)||this.normalizedNames.set(e,n)}init(){this.lazyInit&&(this.lazyInit instanceof t?this.copyFrom(this.lazyInit):this.lazyInit(),this.lazyInit=null,this.lazyUpdate&&(this.lazyUpdate.forEach(n=>this.applyUpdate(n)),this.lazyUpdate=null))}copyFrom(n){n.init(),Array.from(n.headers.keys()).forEach(e=>{this.headers.set(e,n.headers.get(e)),this.normalizedNames.set(e,n.normalizedNames.get(e))})}clone(n){let e=new t;return e.lazyInit=this.lazyInit&&this.lazyInit instanceof t?this.lazyInit:this,e.lazyUpdate=(this.lazyUpdate||[]).concat([n]),e}applyUpdate(n){let e=n.name.toLowerCase();switch(n.op){case"a":case"s":let i=n.value;if(typeof i=="string"&&(i=[i]),i.length===0)return;this.maybeSetNormalizedName(n.name,e);let r=(n.op==="a"?this.headers.get(e):void 0)||[];r.push(...i),this.headers.set(e,r);break;case"d":let o=n.value;if(!o)this.headers.delete(e),this.normalizedNames.delete(e);else{let a=this.headers.get(e);if(!a)return;a=a.filter(s=>o.indexOf(s)===-1),a.length===0?(this.headers.delete(e),this.normalizedNames.delete(e)):this.headers.set(e,a)}break}}addHeaderEntry(n,e){let i=n.toLowerCase();this.maybeSetNormalizedName(n,i),this.headers.has(i)?this.headers.get(i).push(e):this.headers.set(i,[e])}setHeaderEntries(n,e){let i=(Array.isArray(e)?e:[e]).map(o=>o.toString()),r=n.toLowerCase();this.headers.set(r,i),this.maybeSetNormalizedName(n,r)}forEach(n){this.init(),Array.from(this.normalizedNames.keys()).forEach(e=>n(this.normalizedNames.get(e),this.headers.get(e)))}};var iv=class{encodeKey(n){return rN(n)}encodeValue(n){return rN(n)}decodeKey(n){return decodeURIComponent(n)}decodeValue(n){return decodeURIComponent(n)}};function jY(t,n){let e=new Map;return t.length>0&&t.replace(/^\?/,"").split("&").forEach(r=>{let o=r.indexOf("="),[a,s]=o==-1?[n.decodeKey(r),""]:[n.decodeKey(r.slice(0,o)),n.decodeValue(r.slice(o+1))],l=e.get(a)||[];l.push(s),e.set(a,l)}),e}var HY=/%(\d[a-f0-9])/gi,zY={40:"@","3A":":",24:"$","2C":",","3B":";","3D":"=","3F":"?","2F":"/"};function rN(t){return encodeURIComponent(t).replace(HY,(n,e)=>zY[e]??n)}function tv(t){return`${t}`}var rr=class t{map;encoder;updates=null;cloneFrom=null;constructor(n={}){if(this.encoder=n.encoder||new iv,n.fromString){if(n.fromObject)throw new ue(2805,!1);this.map=jY(n.fromString,this.encoder)}else n.fromObject?(this.map=new Map,Object.keys(n.fromObject).forEach(e=>{let i=n.fromObject[e],r=Array.isArray(i)?i.map(tv):[tv(i)];this.map.set(e,r)})):this.map=null}has(n){return this.init(),this.map.has(n)}get(n){this.init();let e=this.map.get(n);return e?e[0]:null}getAll(n){return this.init(),this.map.get(n)||null}keys(){return this.init(),Array.from(this.map.keys())}append(n,e){return this.clone({param:n,value:e,op:"a"})}appendAll(n){let e=[];return Object.keys(n).forEach(i=>{let r=n[i];Array.isArray(r)?r.forEach(o=>{e.push({param:i,value:o,op:"a"})}):e.push({param:i,value:r,op:"a"})}),this.clone(e)}set(n,e){return this.clone({param:n,value:e,op:"s"})}delete(n,e){return this.clone({param:n,value:e,op:"d"})}toString(){return this.init(),this.keys().map(n=>{let e=this.encoder.encodeKey(n);return this.map.get(n).map(i=>e+"="+this.encoder.encodeValue(i)).join("&")}).filter(n=>n!=="").join("&")}clone(n){let e=new t({encoder:this.encoder});return e.cloneFrom=this.cloneFrom||this,e.updates=(this.updates||[]).concat(n),e}init(){this.map===null&&(this.map=new Map),this.cloneFrom!==null&&(this.cloneFrom.init(),this.cloneFrom.keys().forEach(n=>this.map.set(n,this.cloneFrom.map.get(n))),this.updates.forEach(n=>{switch(n.op){case"a":case"s":let e=(n.op==="a"?this.map.get(n.param):void 0)||[];e.push(tv(n.value)),this.map.set(n.param,e);break;case"d":if(n.value!==void 0){let i=this.map.get(n.param)||[],r=i.indexOf(tv(n.value));r!==-1&&i.splice(r,1),i.length>0?this.map.set(n.param,i):this.map.delete(n.param)}else{this.map.delete(n.param);break}}}),this.cloneFrom=this.updates=null)}};var nv=class{map=new Map;set(n,e){return this.map.set(n,e),this}get(n){return this.map.has(n)||this.map.set(n,n.defaultValue()),this.map.get(n)}delete(n){return this.map.delete(n),this}has(n){return this.map.has(n)}keys(){return this.map.keys()}};function UY(t){switch(t){case"DELETE":case"GET":case"HEAD":case"OPTIONS":case"JSONP":return!1;default:return!0}}function oN(t){return typeof ArrayBuffer<"u"&&t instanceof ArrayBuffer}function aN(t){return typeof Blob<"u"&&t instanceof Blob}function sN(t){return typeof FormData<"u"&&t instanceof FormData}function $Y(t){return typeof URLSearchParams<"u"&&t instanceof URLSearchParams}var lN="Content-Type",cN="Accept",dN="X-Request-URL",uN="text/plain",mN="application/json",WY=`${mN}, ${uN}, */*`,Om=class t{url;body=null;headers;context;reportProgress=!1;withCredentials=!1;credentials;keepalive=!1;cache;priority;mode;redirect;referrer;integrity;responseType="json";method;params;urlWithParams;transferCache;timeout;constructor(n,e,i,r){this.url=e,this.method=n.toUpperCase();let o;if(UY(this.method)||r?(this.body=i!==void 0?i:null,o=r):o=i,o){if(this.reportProgress=!!o.reportProgress,this.withCredentials=!!o.withCredentials,this.keepalive=!!o.keepalive,o.responseType&&(this.responseType=o.responseType),o.headers&&(this.headers=o.headers),o.context&&(this.context=o.context),o.params&&(this.params=o.params),o.priority&&(this.priority=o.priority),o.cache&&(this.cache=o.cache),o.credentials&&(this.credentials=o.credentials),typeof o.timeout=="number"){if(o.timeout<1||!Number.isInteger(o.timeout))throw new ue(2822,"");this.timeout=o.timeout}o.mode&&(this.mode=o.mode),o.redirect&&(this.redirect=o.redirect),o.integrity&&(this.integrity=o.integrity),o.referrer&&(this.referrer=o.referrer),this.transferCache=o.transferCache}if(this.headers??=new Sr,this.context??=new nv,!this.params)this.params=new rr,this.urlWithParams=e;else{let a=this.params.toString();if(a.length===0)this.urlWithParams=e;else{let s=e.indexOf("?"),l=s===-1?"?":sK.set(oe,n.setHeaders[oe]),F)),n.setParams&&(W=Object.keys(n.setParams).reduce((K,oe)=>K.set(oe,n.setParams[oe]),W)),new t(e,i,w,{params:W,headers:F,context:Z,reportProgress:D,responseType:r,withCredentials:C,transferCache:b,keepalive:o,cache:s,priority:a,timeout:y,mode:l,redirect:c,credentials:d,referrer:p,integrity:_})}},$d=(function(t){return t[t.Sent=0]="Sent",t[t.UploadProgress=1]="UploadProgress",t[t.ResponseHeader=2]="ResponseHeader",t[t.DownloadProgress=3]="DownloadProgress",t[t.Response=4]="Response",t[t.User=5]="User",t})($d||{}),Fm=class{headers;status;statusText;url;ok;type;redirected;constructor(n,e=200,i="OK"){this.headers=n.headers||new Sr,this.status=n.status!==void 0?n.status:e,this.statusText=n.statusText||i,this.url=n.url||null,this.redirected=n.redirected,this.ok=this.status>=200&&this.status<300}},rv=class t extends Fm{constructor(n={}){super(n)}type=$d.ResponseHeader;clone(n={}){return new t({headers:n.headers||this.headers,status:n.status!==void 0?n.status:this.status,statusText:n.statusText||this.statusText,url:n.url||this.url||void 0})}},xl=class t extends Fm{body;constructor(n={}){super(n),this.body=n.body!==void 0?n.body:null}type=$d.Response;clone(n={}){return new t({body:n.body!==void 0?n.body:this.body,headers:n.headers||this.headers,status:n.status!==void 0?n.status:this.status,statusText:n.statusText||this.statusText,url:n.url||this.url||void 0,redirected:n.redirected??this.redirected})}},Rm=class extends Fm{name="HttpErrorResponse";message;error;ok=!1;constructor(n){super(n,0,"Unknown Error"),this.status>=200&&this.status<300?this.message=`Http failure during parsing for ${n.url||"(unknown url)"}`:this.message=`Http failure response for ${n.url||"(unknown url)"}: ${n.status} ${n.statusText}`,this.error=n.error||null}},GY=200,qY=204;function IE(t,n){return{body:n,headers:t.headers,context:t.context,observe:t.observe,params:t.params,reportProgress:t.reportProgress,responseType:t.responseType,withCredentials:t.withCredentials,credentials:t.credentials,transferCache:t.transferCache,timeout:t.timeout,keepalive:t.keepalive,priority:t.priority,cache:t.cache,mode:t.mode,redirect:t.redirect,integrity:t.integrity,referrer:t.referrer}}var kr=(()=>{class t{handler;constructor(e){this.handler=e}request(e,i,r={}){let o;if(e instanceof Om)o=e;else{let l;r.headers instanceof Sr?l=r.headers:l=new Sr(r.headers);let c;r.params&&(r.params instanceof rr?c=r.params:c=new rr({fromObject:r.params})),o=new Om(e,i,r.body!==void 0?r.body:null,{headers:l,context:r.context,params:c,reportProgress:r.reportProgress,responseType:r.responseType||"json",withCredentials:r.withCredentials,transferCache:r.transferCache,keepalive:r.keepalive,priority:r.priority,cache:r.cache,mode:r.mode,redirect:r.redirect,credentials:r.credentials,referrer:r.referrer,integrity:r.integrity,timeout:r.timeout})}let a=Q(o).pipe(jo(l=>this.handler.handle(l)));if(e instanceof Om||r.observe==="events")return a;let s=a.pipe(ce(l=>l instanceof xl));switch(r.observe||"body"){case"body":switch(o.responseType){case"arraybuffer":return s.pipe(se(l=>{if(l.body!==null&&!(l.body instanceof ArrayBuffer))throw new ue(2806,!1);return l.body}));case"blob":return s.pipe(se(l=>{if(l.body!==null&&!(l.body instanceof Blob))throw new ue(2807,!1);return l.body}));case"text":return s.pipe(se(l=>{if(l.body!==null&&typeof l.body!="string")throw new ue(2808,!1);return l.body}));case"json":default:return s.pipe(se(l=>l.body))}case"response":return s;default:throw new ue(2809,!1)}}delete(e,i={}){return this.request("DELETE",e,i)}get(e,i={}){return this.request("GET",e,i)}head(e,i={}){return this.request("HEAD",e,i)}jsonp(e,i){return this.request("JSONP",e,{params:new rr().append(i,"JSONP_CALLBACK"),observe:"body",responseType:"json"})}options(e,i={}){return this.request("OPTIONS",e,i)}patch(e,i,r={}){return this.request("PATCH",e,IE(r,i))}post(e,i,r={}){return this.request("POST",e,IE(r,i))}put(e,i,r={}){return this.request("PUT",e,IE(r,i))}static \u0275fac=function(i){return new(i||t)(pe(Pm))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();var YY=new O("");function QY(t,n){return n(t)}function KY(t,n,e){return(i,r)=>Vn(e,()=>n(i,o=>t(o,r)))}var OE=new O(""),RE=new O(""),hN=new O(""),pN=new O("",{providedIn:"root",factory:()=>!0});var ov=(()=>{class t extends Pm{backend;injector;chain=null;pendingTasks=u(dm);contributeToStability=u(pN);constructor(e,i){super(),this.backend=e,this.injector=i}handle(e){if(this.chain===null){let i=Array.from(new Set([...this.injector.get(RE),...this.injector.get(hN,[])]));this.chain=i.reduceRight((r,o)=>KY(r,o,this.injector),QY)}if(this.contributeToStability){let i=this.pendingTasks.add();return this.chain(e,r=>this.backend.handle(r)).pipe(Xr(i))}else return this.chain(e,i=>this.backend.handle(i))}static \u0275fac=function(i){return new(i||t)(pe(fc),pe(ti))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();var ZY=/^\)\]\}',?\n/,XY=RegExp(`^${dN}:`,"m");function JY(t){return"responseURL"in t&&t.responseURL?t.responseURL:XY.test(t.getAllResponseHeaders())?t.getResponseHeader(dN):null}var AE=(()=>{class t{xhrFactory;constructor(e){this.xhrFactory=e}handle(e){if(e.method==="JSONP")throw new ue(-2800,!1);let i=this.xhrFactory;return Q(null).pipe(je(()=>new Ne(o=>{let a=i.build();if(a.open(e.method,e.urlWithParams),e.withCredentials&&(a.withCredentials=!0),e.headers.forEach((C,D)=>a.setRequestHeader(C,D.join(","))),e.headers.has(cN)||a.setRequestHeader(cN,WY),!e.headers.has(lN)){let C=e.detectContentTypeHeader();C!==null&&a.setRequestHeader(lN,C)}if(e.timeout&&(a.timeout=e.timeout),e.responseType){let C=e.responseType.toLowerCase();a.responseType=C!=="json"?C:"text"}let s=e.serializeBody(),l=null,c=()=>{if(l!==null)return l;let C=a.statusText||"OK",D=new Sr(a.getAllResponseHeaders()),F=JY(a)||e.url;return l=new rv({headers:D,status:a.status,statusText:C,url:F}),l},d=()=>{let{headers:C,status:D,statusText:F,url:W}=c(),Z=null;D!==qY&&(Z=typeof a.response>"u"?a.responseText:a.response),D===0&&(D=Z?GY:0);let K=D>=200&&D<300;if(e.responseType==="json"&&typeof Z=="string"){let oe=Z;Z=Z.replace(ZY,"");try{Z=Z!==""?JSON.parse(Z):null}catch(Se){Z=oe,K&&(K=!1,Z={error:Se,text:Z})}}K?(o.next(new xl({body:Z,headers:C,status:D,statusText:F,url:W||void 0})),o.complete()):o.error(new Rm({error:Z,headers:C,status:D,statusText:F,url:W||void 0}))},p=C=>{let{url:D}=c(),F=new Rm({error:C,status:a.status||0,statusText:a.statusText||"Unknown Error",url:D||void 0});o.error(F)},_=p;e.timeout&&(_=C=>{let{url:D}=c(),F=new Rm({error:new DOMException("Request timed out","TimeoutError"),status:a.status||0,statusText:a.statusText||"Request timeout",url:D||void 0});o.error(F)});let b=!1,y=C=>{b||(o.next(c()),b=!0);let D={type:$d.DownloadProgress,loaded:C.loaded};C.lengthComputable&&(D.total=C.total),e.responseType==="text"&&a.responseText&&(D.partialText=a.responseText),o.next(D)},w=C=>{let D={type:$d.UploadProgress,loaded:C.loaded};C.lengthComputable&&(D.total=C.total),o.next(D)};return a.addEventListener("load",d),a.addEventListener("error",p),a.addEventListener("timeout",_),a.addEventListener("abort",p),e.reportProgress&&(a.addEventListener("progress",y),s!==null&&a.upload&&a.upload.addEventListener("progress",w)),a.send(s),o.next({type:$d.Sent}),()=>{a.removeEventListener("error",p),a.removeEventListener("abort",p),a.removeEventListener("load",d),a.removeEventListener("timeout",_),e.reportProgress&&(a.removeEventListener("progress",y),s!==null&&a.upload&&a.upload.removeEventListener("progress",w)),a.readyState!==a.DONE&&a.abort()}})))}static \u0275fac=function(i){return new(i||t)(pe(Ud))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),fN=new O(""),eQ="XSRF-TOKEN",tQ=new O("",{providedIn:"root",factory:()=>eQ}),iQ="X-XSRF-TOKEN",nQ=new O("",{providedIn:"root",factory:()=>iQ}),Sf=class{},rQ=(()=>{class t{doc;cookieName;lastCookieString="";lastToken=null;parseCount=0;constructor(e,i){this.doc=e,this.cookieName=i}getToken(){let e=this.doc.cookie||"";return e!==this.lastCookieString&&(this.parseCount++,this.lastToken=Cf(e,this.cookieName),this.lastCookieString=e),this.lastToken}static \u0275fac=function(i){return new(i||t)(pe(_e),pe(tQ))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),oQ=/^(?:https?:)?\/\//i;function aQ(t,n){if(!u(fN)||t.method==="GET"||t.method==="HEAD"||oQ.test(t.url))return n(t);let e=u(Sf).getToken(),i=u(nQ);return e!=null&&!t.headers.has(i)&&(t=t.clone({headers:t.headers.set(i,e)})),n(t)}var PE=(function(t){return t[t.Interceptors=0]="Interceptors",t[t.LegacyInterceptors=1]="LegacyInterceptors",t[t.CustomXsrfConfiguration=2]="CustomXsrfConfiguration",t[t.NoXsrfProtection=3]="NoXsrfProtection",t[t.JsonpSupport=4]="JsonpSupport",t[t.RequestsMadeViaParent=5]="RequestsMadeViaParent",t[t.Fetch=6]="Fetch",t})(PE||{});function sQ(t,n){return{\u0275kind:t,\u0275providers:n}}function FE(...t){let n=[kr,AE,ov,{provide:Pm,useExisting:ov},{provide:fc,useFactory:()=>u(YY,{optional:!0})??u(AE)},{provide:RE,useValue:aQ,multi:!0},{provide:fN,useValue:!0},{provide:Sf,useClass:rQ}];for(let e of t)n.push(...e.\u0275providers);return Jr(n)}function NE(t){return sQ(PE.Interceptors,t.map(n=>({provide:RE,useValue:n,multi:!0})))}var _N=(()=>{class t{_doc;constructor(e){this._doc=e}getTitle(){return this._doc.title}setTitle(e){this._doc.title=e||""}static \u0275fac=function(i){return new(i||t)(pe(_e))};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var kf=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:function(i){let r=null;return i?r=new(i||t):r=pe(lQ),r},providedIn:"root"})}return t})(),lQ=(()=>{class t extends kf{_doc;constructor(e){super(),this._doc=e}sanitize(e,i){if(i==null)return null;switch(e){case Bn.NONE:return i;case Bn.HTML:return Ms(i,"HTML")?eo(i):g0(this._doc,String(i)).toString();case Bn.STYLE:return Ms(i,"Style")?eo(i):i;case Bn.SCRIPT:if(Ms(i,"Script"))return eo(i);throw new ue(5200,!1);case Bn.URL:return Ms(i,"URL")?eo(i):tf(String(i));case Bn.RESOURCE_URL:if(Ms(i,"ResourceURL"))return eo(i);throw new ue(5201,!1);default:throw new ue(5202,!1)}}bypassSecurityTrustHtml(e){return cM(e)}bypassSecurityTrustStyle(e){return dM(e)}bypassSecurityTrustScript(e){return uM(e)}bypassSecurityTrustUrl(e){return mM(e)}bypassSecurityTrustResourceUrl(e){return hM(e)}static \u0275fac=function(i){return new(i||t)(pe(_e))};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var pt="primary",Hf=Symbol("RouteTitle"),zE=class{params;constructor(n){this.params=n||{}}has(n){return Object.prototype.hasOwnProperty.call(this.params,n)}get(n){if(this.has(n)){let e=this.params[n];return Array.isArray(e)?e[0]:e}return null}getAll(n){if(this.has(n)){let e=this.params[n];return Array.isArray(e)?e:[e]}return[]}get keys(){return Object.keys(this.params)}};function qd(t){return new zE(t)}function MN(t,n,e){let i=e.path.split("/");if(i.length>t.length||e.pathMatch==="full"&&(n.hasChildren()||i.lengthi[o]===r)}else return t===n}function SN(t){return t.length>0?t[t.length-1]:null}function Cl(t){return Gi(t)?t:fl(t)?Ut(Promise.resolve(t)):Q(t)}var dQ={exact:TN,subset:IN},kN={exact:uQ,subset:mQ,ignored:()=>!0};function bN(t,n,e){return dQ[e.paths](t.root,n.root,e.matrixParams)&&kN[e.queryParams](t.queryParams,n.queryParams)&&!(e.fragment==="exact"&&t.fragment!==n.fragment)}function uQ(t,n){return Ts(t,n)}function TN(t,n,e){if(!Wd(t.segments,n.segments)||!lv(t.segments,n.segments,e)||t.numberOfChildren!==n.numberOfChildren)return!1;for(let i in n.children)if(!t.children[i]||!TN(t.children[i],n.children[i],e))return!1;return!0}function mQ(t,n){return Object.keys(n).length<=Object.keys(t).length&&Object.keys(n).every(e=>EN(t[e],n[e]))}function IN(t,n,e){return AN(t,n,n.segments,e)}function AN(t,n,e,i){if(t.segments.length>e.length){let r=t.segments.slice(0,e.length);return!(!Wd(r,e)||n.hasChildren()||!lv(r,e,i))}else if(t.segments.length===e.length){if(!Wd(t.segments,e)||!lv(t.segments,e,i))return!1;for(let r in n.children)if(!t.children[r]||!IN(t.children[r],n.children[r],i))return!1;return!0}else{let r=e.slice(0,t.segments.length),o=e.slice(t.segments.length);return!Wd(t.segments,r)||!lv(t.segments,r,i)||!t.children[pt]?!1:AN(t.children[pt],n,o,i)}}function lv(t,n,e){return n.every((i,r)=>kN[e](t[r].parameters,i.parameters))}var Is=class{root;queryParams;fragment;_queryParamMap;constructor(n=new ri([],{}),e={},i=null){this.root=n,this.queryParams=e,this.fragment=i}get queryParamMap(){return this._queryParamMap??=qd(this.queryParams),this._queryParamMap}toString(){return fQ.serialize(this)}},ri=class{segments;children;parent=null;constructor(n,e){this.segments=n,this.children=e,Object.values(e).forEach(i=>i.parent=this)}hasChildren(){return this.numberOfChildren>0}get numberOfChildren(){return Object.keys(this.children).length}toString(){return cv(this)}},gc=class{path;parameters;_parameterMap;constructor(n,e){this.path=n,this.parameters=e}get parameterMap(){return this._parameterMap??=qd(this.parameters),this._parameterMap}toString(){return RN(this)}};function hQ(t,n){return Wd(t,n)&&t.every((e,i)=>Ts(e.parameters,n[i].parameters))}function Wd(t,n){return t.length!==n.length?!1:t.every((e,i)=>e.path===n[i].path)}function pQ(t,n){let e=[];return Object.entries(t.children).forEach(([i,r])=>{i===pt&&(e=e.concat(n(r,i)))}),Object.entries(t.children).forEach(([i,r])=>{i!==pt&&(e=e.concat(n(r,i)))}),e}var yc=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:()=>new _c,providedIn:"root"})}return t})(),_c=class{parse(n){let e=new WE(n);return new Is(e.parseRootSegment(),e.parseQueryParams(),e.parseFragment())}serialize(n){let e=`/${Tf(n.root,!0)}`,i=bQ(n.queryParams),r=typeof n.fragment=="string"?`#${gQ(n.fragment)}`:"";return`${e}${i}${r}`}},fQ=new _c;function cv(t){return t.segments.map(n=>RN(n)).join("/")}function Tf(t,n){if(!t.hasChildren())return cv(t);if(n){let e=t.children[pt]?Tf(t.children[pt],!1):"",i=[];return Object.entries(t.children).forEach(([r,o])=>{r!==pt&&i.push(`${r}:${Tf(o,!1)}`)}),i.length>0?`${e}(${i.join("//")})`:e}else{let e=pQ(t,(i,r)=>r===pt?[Tf(t.children[pt],!1)]:[`${r}:${Tf(i,!1)}`]);return Object.keys(t.children).length===1&&t.children[pt]!=null?`${cv(t)}/${e[0]}`:`${cv(t)}/(${e.join("//")})`}}function ON(t){return encodeURIComponent(t).replace(/%40/g,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",")}function av(t){return ON(t).replace(/%3B/gi,";")}function gQ(t){return encodeURI(t)}function $E(t){return ON(t).replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/%26/gi,"&")}function dv(t){return decodeURIComponent(t)}function vN(t){return dv(t.replace(/\+/g,"%20"))}function RN(t){return`${$E(t.path)}${_Q(t.parameters)}`}function _Q(t){return Object.entries(t).map(([n,e])=>`;${$E(n)}=${$E(e)}`).join("")}function bQ(t){let n=Object.entries(t).map(([e,i])=>Array.isArray(i)?i.map(r=>`${av(e)}=${av(r)}`).join("&"):`${av(e)}=${av(i)}`).filter(e=>e);return n.length?`?${n.join("&")}`:""}var vQ=/^[^\/()?;#]+/;function VE(t){let n=t.match(vQ);return n?n[0]:""}var yQ=/^[^\/()?;=#]+/;function xQ(t){let n=t.match(yQ);return n?n[0]:""}var CQ=/^[^=?&#]+/;function wQ(t){let n=t.match(CQ);return n?n[0]:""}var DQ=/^[^&#]+/;function MQ(t){let n=t.match(DQ);return n?n[0]:""}var WE=class{url;remaining;constructor(n){this.url=n,this.remaining=n}parseRootSegment(){return this.consumeOptional("/"),this.remaining===""||this.peekStartsWith("?")||this.peekStartsWith("#")?new ri([],{}):new ri([],this.parseChildren())}parseQueryParams(){let n={};if(this.consumeOptional("?"))do this.parseQueryParam(n);while(this.consumeOptional("&"));return n}parseFragment(){return this.consumeOptional("#")?decodeURIComponent(this.remaining):null}parseChildren(){if(this.remaining==="")return{};this.consumeOptional("/");let n=[];for(this.peekStartsWith("(")||n.push(this.parseSegment());this.peekStartsWith("/")&&!this.peekStartsWith("//")&&!this.peekStartsWith("/(");)this.capture("/"),n.push(this.parseSegment());let e={};this.peekStartsWith("/(")&&(this.capture("/"),e=this.parseParens(!0));let i={};return this.peekStartsWith("(")&&(i=this.parseParens(!1)),(n.length>0||Object.keys(e).length>0)&&(i[pt]=new ri(n,e)),i}parseSegment(){let n=VE(this.remaining);if(n===""&&this.peekStartsWith(";"))throw new ue(4009,!1);return this.capture(n),new gc(dv(n),this.parseMatrixParams())}parseMatrixParams(){let n={};for(;this.consumeOptional(";");)this.parseParam(n);return n}parseParam(n){let e=xQ(this.remaining);if(!e)return;this.capture(e);let i="";if(this.consumeOptional("=")){let r=VE(this.remaining);r&&(i=r,this.capture(i))}n[dv(e)]=dv(i)}parseQueryParam(n){let e=wQ(this.remaining);if(!e)return;this.capture(e);let i="";if(this.consumeOptional("=")){let a=MQ(this.remaining);a&&(i=a,this.capture(i))}let r=vN(e),o=vN(i);if(n.hasOwnProperty(r)){let a=n[r];Array.isArray(a)||(a=[a],n[r]=a),a.push(o)}else n[r]=o}parseParens(n){let e={};for(this.capture("(");!this.consumeOptional(")")&&this.remaining.length>0;){let i=VE(this.remaining),r=this.remaining[i.length];if(r!=="/"&&r!==")"&&r!==";")throw new ue(4010,!1);let o;i.indexOf(":")>-1?(o=i.slice(0,i.indexOf(":")),this.capture(o),this.capture(":")):n&&(o=pt);let a=this.parseChildren();e[o??pt]=Object.keys(a).length===1&&a[pt]?a[pt]:new ri([],a),this.consumeOptional("//")}return e}peekStartsWith(n){return this.remaining.startsWith(n)}consumeOptional(n){return this.peekStartsWith(n)?(this.remaining=this.remaining.substring(n.length),!0):!1}capture(n){if(!this.consumeOptional(n))throw new ue(4011,!1)}};function PN(t){return t.segments.length>0?new ri([],{[pt]:t}):t}function FN(t){let n={};for(let[i,r]of Object.entries(t.children)){let o=FN(r);if(i===pt&&o.segments.length===0&&o.hasChildren())for(let[a,s]of Object.entries(o.children))n[a]=s;else(o.segments.length>0||o.hasChildren())&&(n[i]=o)}let e=new ri(t.segments,n);return EQ(e)}function EQ(t){if(t.numberOfChildren===1&&t.children[pt]){let n=t.children[pt];return new ri(t.segments.concat(n.segments),n.children)}return t}function bc(t){return t instanceof Is}function NN(t,n,e=null,i=null){let r=LN(t);return VN(r,n,e,i)}function LN(t){let n;function e(o){let a={};for(let l of o.children){let c=e(l);a[l.outlet]=c}let s=new ri(o.url,a);return o===t&&(n=s),s}let i=e(t.root),r=PN(i);return n??r}function VN(t,n,e,i){let r=t;for(;r.parent;)r=r.parent;if(n.length===0)return BE(r,r,r,e,i);let o=SQ(n);if(o.toRoot())return BE(r,r,new ri([],{}),e,i);let a=kQ(o,r,t),s=a.processChildren?Af(a.segmentGroup,a.index,o.commands):jN(a.segmentGroup,a.index,o.commands);return BE(r,a.segmentGroup,s,e,i)}function uv(t){return typeof t=="object"&&t!=null&&!t.outlets&&!t.segmentPath}function Rf(t){return typeof t=="object"&&t!=null&&t.outlets}function BE(t,n,e,i,r){let o={};i&&Object.entries(i).forEach(([l,c])=>{o[l]=Array.isArray(c)?c.map(d=>`${d}`):`${c}`});let a;t===n?a=e:a=BN(t,n,e);let s=PN(FN(a));return new Is(s,o,r)}function BN(t,n,e){let i={};return Object.entries(t.children).forEach(([r,o])=>{o===n?i[r]=e:i[r]=BN(o,n,e)}),new ri(t.segments,i)}var mv=class{isAbsolute;numberOfDoubleDots;commands;constructor(n,e,i){if(this.isAbsolute=n,this.numberOfDoubleDots=e,this.commands=i,n&&i.length>0&&uv(i[0]))throw new ue(4003,!1);let r=i.find(Rf);if(r&&r!==SN(i))throw new ue(4004,!1)}toRoot(){return this.isAbsolute&&this.commands.length===1&&this.commands[0]=="/"}};function SQ(t){if(typeof t[0]=="string"&&t.length===1&&t[0]==="/")return new mv(!0,0,t);let n=0,e=!1,i=t.reduce((r,o,a)=>{if(typeof o=="object"&&o!=null){if(o.outlets){let s={};return Object.entries(o.outlets).forEach(([l,c])=>{s[l]=typeof c=="string"?c.split("/"):c}),[...r,{outlets:s}]}if(o.segmentPath)return[...r,o.segmentPath]}return typeof o!="string"?[...r,o]:a===0?(o.split("/").forEach((s,l)=>{l==0&&s==="."||(l==0&&s===""?e=!0:s===".."?n++:s!=""&&r.push(s))}),r):[...r,o]},[]);return new mv(e,n,i)}var Vm=class{segmentGroup;processChildren;index;constructor(n,e,i){this.segmentGroup=n,this.processChildren=e,this.index=i}};function kQ(t,n,e){if(t.isAbsolute)return new Vm(n,!0,0);if(!e)return new Vm(n,!1,NaN);if(e.parent===null)return new Vm(e,!0,0);let i=uv(t.commands[0])?0:1,r=e.segments.length-1+i;return TQ(e,r,t.numberOfDoubleDots)}function TQ(t,n,e){let i=t,r=n,o=e;for(;o>r;){if(o-=r,i=i.parent,!i)throw new ue(4005,!1);r=i.segments.length}return new Vm(i,!1,r-o)}function IQ(t){return Rf(t[0])?t[0].outlets:{[pt]:t}}function jN(t,n,e){if(t??=new ri([],{}),t.segments.length===0&&t.hasChildren())return Af(t,n,e);let i=AQ(t,n,e),r=e.slice(i.commandIndex);if(i.match&&i.pathIndexo!==pt)&&t.children[pt]&&t.numberOfChildren===1&&t.children[pt].segments.length===0){let o=Af(t.children[pt],n,e);return new ri(t.segments,o.children)}return Object.entries(i).forEach(([o,a])=>{typeof a=="string"&&(a=[a]),a!==null&&(r[o]=jN(t.children[o],n,a))}),Object.entries(t.children).forEach(([o,a])=>{i[o]===void 0&&(r[o]=a)}),new ri(t.segments,r)}}function AQ(t,n,e){let i=0,r=n,o={match:!1,pathIndex:0,commandIndex:0};for(;r=e.length)return o;let a=t.segments[r],s=e[i];if(Rf(s))break;let l=`${s}`,c=i0&&l===void 0)break;if(l&&c&&typeof c=="object"&&c.outlets===void 0){if(!xN(l,c,a))return o;i+=2}else{if(!xN(l,{},a))return o;i++}r++}return{match:!0,pathIndex:r,commandIndex:i}}function GE(t,n,e){let i=t.segments.slice(0,n),r=0;for(;r{typeof i=="string"&&(i=[i]),i!==null&&(n[e]=GE(new ri([],{}),0,i))}),n}function yN(t){let n={};return Object.entries(t).forEach(([e,i])=>n[e]=`${i}`),n}function xN(t,n,e){return t==e.path&&Ts(n,e.parameters)}var Bm="imperative",En=(function(t){return t[t.NavigationStart=0]="NavigationStart",t[t.NavigationEnd=1]="NavigationEnd",t[t.NavigationCancel=2]="NavigationCancel",t[t.NavigationError=3]="NavigationError",t[t.RoutesRecognized=4]="RoutesRecognized",t[t.ResolveStart=5]="ResolveStart",t[t.ResolveEnd=6]="ResolveEnd",t[t.GuardsCheckStart=7]="GuardsCheckStart",t[t.GuardsCheckEnd=8]="GuardsCheckEnd",t[t.RouteConfigLoadStart=9]="RouteConfigLoadStart",t[t.RouteConfigLoadEnd=10]="RouteConfigLoadEnd",t[t.ChildActivationStart=11]="ChildActivationStart",t[t.ChildActivationEnd=12]="ChildActivationEnd",t[t.ActivationStart=13]="ActivationStart",t[t.ActivationEnd=14]="ActivationEnd",t[t.Scroll=15]="Scroll",t[t.NavigationSkipped=16]="NavigationSkipped",t})(En||{}),Eo=class{id;url;constructor(n,e){this.id=n,this.url=e}},As=class extends Eo{type=En.NavigationStart;navigationTrigger;restoredState;constructor(n,e,i="imperative",r=null){super(n,e),this.navigationTrigger=i,this.restoredState=r}toString(){return`NavigationStart(id: ${this.id}, url: '${this.url}')`}},Si=class extends Eo{urlAfterRedirects;type=En.NavigationEnd;constructor(n,e,i){super(n,e),this.urlAfterRedirects=i}toString(){return`NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`}},Ur=(function(t){return t[t.Redirect=0]="Redirect",t[t.SupersededByNewNavigation=1]="SupersededByNewNavigation",t[t.NoDataFromResolver=2]="NoDataFromResolver",t[t.GuardRejected=3]="GuardRejected",t[t.Aborted=4]="Aborted",t})(Ur||{}),Hm=(function(t){return t[t.IgnoredSameUrlNavigation=0]="IgnoredSameUrlNavigation",t[t.IgnoredByUrlHandlingStrategy=1]="IgnoredByUrlHandlingStrategy",t})(Hm||{}),ea=class extends Eo{reason;code;type=En.NavigationCancel;constructor(n,e,i,r){super(n,e),this.reason=i,this.code=r}toString(){return`NavigationCancel(id: ${this.id}, url: '${this.url}')`}},Os=class extends Eo{reason;code;type=En.NavigationSkipped;constructor(n,e,i,r){super(n,e),this.reason=i,this.code=r}},vc=class extends Eo{error;target;type=En.NavigationError;constructor(n,e,i,r){super(n,e),this.error=i,this.target=r}toString(){return`NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`}},Pf=class extends Eo{urlAfterRedirects;state;type=En.RoutesRecognized;constructor(n,e,i,r){super(n,e),this.urlAfterRedirects=i,this.state=r}toString(){return`RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},hv=class extends Eo{urlAfterRedirects;state;type=En.GuardsCheckStart;constructor(n,e,i,r){super(n,e),this.urlAfterRedirects=i,this.state=r}toString(){return`GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},pv=class extends Eo{urlAfterRedirects;state;shouldActivate;type=En.GuardsCheckEnd;constructor(n,e,i,r,o){super(n,e),this.urlAfterRedirects=i,this.state=r,this.shouldActivate=o}toString(){return`GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`}},fv=class extends Eo{urlAfterRedirects;state;type=En.ResolveStart;constructor(n,e,i,r){super(n,e),this.urlAfterRedirects=i,this.state=r}toString(){return`ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},gv=class extends Eo{urlAfterRedirects;state;type=En.ResolveEnd;constructor(n,e,i,r){super(n,e),this.urlAfterRedirects=i,this.state=r}toString(){return`ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},_v=class{route;type=En.RouteConfigLoadStart;constructor(n){this.route=n}toString(){return`RouteConfigLoadStart(path: ${this.route.path})`}},bv=class{route;type=En.RouteConfigLoadEnd;constructor(n){this.route=n}toString(){return`RouteConfigLoadEnd(path: ${this.route.path})`}},vv=class{snapshot;type=En.ChildActivationStart;constructor(n){this.snapshot=n}toString(){return`ChildActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},yv=class{snapshot;type=En.ChildActivationEnd;constructor(n){this.snapshot=n}toString(){return`ChildActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},xv=class{snapshot;type=En.ActivationStart;constructor(n){this.snapshot=n}toString(){return`ActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},Cv=class{snapshot;type=En.ActivationEnd;constructor(n){this.snapshot=n}toString(){return`ActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},zm=class{routerEvent;position;anchor;type=En.Scroll;constructor(n,e,i){this.routerEvent=n,this.position=e,this.anchor=i}toString(){let n=this.position?`${this.position[0]}, ${this.position[1]}`:null;return`Scroll(anchor: '${this.anchor}', position: '${n}')`}},Ff=class{},Um=class{url;navigationBehaviorOptions;constructor(n,e){this.url=n,this.navigationBehaviorOptions=e}};function RQ(t){return!(t instanceof Ff)&&!(t instanceof Um)}function PQ(t,n){return t.providers&&!t._injector&&(t._injector=Dm(t.providers,n,`Route: ${t.path}`)),t._injector??n}function Ga(t){return t.outlet||pt}function FQ(t,n){let e=t.filter(i=>Ga(i)===n);return e.push(...t.filter(i=>Ga(i)!==n)),e}function Gm(t){if(!t)return null;if(t.routeConfig?._injector)return t.routeConfig._injector;for(let n=t.parent;n;n=n.parent){let e=n.routeConfig;if(e?._loadedInjector)return e._loadedInjector;if(e?._injector)return e._injector}return null}var wv=class{rootInjector;outlet=null;route=null;children;attachRef=null;get injector(){return Gm(this.route?.snapshot)??this.rootInjector}constructor(n){this.rootInjector=n,this.children=new Yd(this.rootInjector)}},Yd=(()=>{class t{rootInjector;contexts=new Map;constructor(e){this.rootInjector=e}onChildOutletCreated(e,i){let r=this.getOrCreateContext(e);r.outlet=i,this.contexts.set(e,r)}onChildOutletDestroyed(e){let i=this.getContext(e);i&&(i.outlet=null,i.attachRef=null)}onOutletDeactivated(){let e=this.contexts;return this.contexts=new Map,e}onOutletReAttached(e){this.contexts=e}getOrCreateContext(e){let i=this.getContext(e);return i||(i=new wv(this.rootInjector),this.contexts.set(e,i)),i}getContext(e){return this.contexts.get(e)||null}static \u0275fac=function(i){return new(i||t)(pe(ti))};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Dv=class{_root;constructor(n){this._root=n}get root(){return this._root.value}parent(n){let e=this.pathFromRoot(n);return e.length>1?e[e.length-2]:null}children(n){let e=qE(n,this._root);return e?e.children.map(i=>i.value):[]}firstChild(n){let e=qE(n,this._root);return e&&e.children.length>0?e.children[0].value:null}siblings(n){let e=YE(n,this._root);return e.length<2?[]:e[e.length-2].children.map(r=>r.value).filter(r=>r!==n)}pathFromRoot(n){return YE(n,this._root).map(e=>e.value)}};function qE(t,n){if(t===n.value)return n;for(let e of n.children){let i=qE(t,e);if(i)return i}return null}function YE(t,n){if(t===n.value)return[n];for(let e of n.children){let i=YE(t,e);if(i.length)return i.unshift(n),i}return[]}var Mo=class{value;children;constructor(n,e){this.value=n,this.children=e}toString(){return`TreeNode(${this.value})`}};function Lm(t){let n={};return t&&t.children.forEach(e=>n[e.value.outlet]=e),n}var Nf=class extends Dv{snapshot;constructor(n,e){super(n),this.snapshot=e,iS(this,n)}toString(){return this.snapshot.toString()}};function HN(t){let n=NQ(t),e=new rt([new gc("",{})]),i=new rt({}),r=new rt({}),o=new rt({}),a=new rt(""),s=new Ai(e,i,o,a,r,pt,t,n.root);return s.snapshot=n.root,new Nf(new Mo(s,[]),n)}function NQ(t){let n={},e={},i={},o=new Gd([],n,i,"",e,pt,t,null,{});return new Lf("",new Mo(o,[]))}var Ai=class{urlSubject;paramsSubject;queryParamsSubject;fragmentSubject;dataSubject;outlet;component;snapshot;_futureSnapshot;_routerState;_paramMap;_queryParamMap;title;url;params;queryParams;fragment;data;constructor(n,e,i,r,o,a,s,l){this.urlSubject=n,this.paramsSubject=e,this.queryParamsSubject=i,this.fragmentSubject=r,this.dataSubject=o,this.outlet=a,this.component=s,this._futureSnapshot=l,this.title=this.dataSubject?.pipe(se(c=>c[Hf]))??Q(void 0),this.url=n,this.params=e,this.queryParams=i,this.fragment=r,this.data=o}get routeConfig(){return this._futureSnapshot.routeConfig}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=this.params.pipe(se(n=>qd(n))),this._paramMap}get queryParamMap(){return this._queryParamMap??=this.queryParams.pipe(se(n=>qd(n))),this._queryParamMap}toString(){return this.snapshot?this.snapshot.toString():`Future(${this._futureSnapshot})`}};function Mv(t,n,e="emptyOnly"){let i,{routeConfig:r}=t;return n!==null&&(e==="always"||r?.path===""||!n.component&&!n.routeConfig?.loadComponent)?i={params:I(I({},n.params),t.params),data:I(I({},n.data),t.data),resolve:I(I(I(I({},t.data),n.data),r?.data),t._resolvedData)}:i={params:I({},t.params),data:I({},t.data),resolve:I(I({},t.data),t._resolvedData??{})},r&&UN(r)&&(i.resolve[Hf]=r.title),i}var Gd=class{url;params;queryParams;fragment;data;outlet;component;routeConfig;_resolve;_resolvedData;_routerState;_paramMap;_queryParamMap;get title(){return this.data?.[Hf]}constructor(n,e,i,r,o,a,s,l,c){this.url=n,this.params=e,this.queryParams=i,this.fragment=r,this.data=o,this.outlet=a,this.component=s,this.routeConfig=l,this._resolve=c}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=qd(this.params),this._paramMap}get queryParamMap(){return this._queryParamMap??=qd(this.queryParams),this._queryParamMap}toString(){let n=this.url.map(i=>i.toString()).join("/"),e=this.routeConfig?this.routeConfig.path:"";return`Route(url:'${n}', path:'${e}')`}},Lf=class extends Dv{url;constructor(n,e){super(e),this.url=n,iS(this,e)}toString(){return zN(this._root)}};function iS(t,n){n.value._routerState=t,n.children.forEach(e=>iS(t,e))}function zN(t){let n=t.children.length>0?` { ${t.children.map(zN).join(", ")} } `:"";return`${t.value}${n}`}function jE(t){if(t.snapshot){let n=t.snapshot,e=t._futureSnapshot;t.snapshot=e,Ts(n.queryParams,e.queryParams)||t.queryParamsSubject.next(e.queryParams),n.fragment!==e.fragment&&t.fragmentSubject.next(e.fragment),Ts(n.params,e.params)||t.paramsSubject.next(e.params),cQ(n.url,e.url)||t.urlSubject.next(e.url),Ts(n.data,e.data)||t.dataSubject.next(e.data)}else t.snapshot=t._futureSnapshot,t.dataSubject.next(t._futureSnapshot.data)}function QE(t,n){let e=Ts(t.params,n.params)&&hQ(t.url,n.url),i=!t.parent!=!n.parent;return e&&!i&&(!t.parent||QE(t.parent,n.parent))}function UN(t){return typeof t.title=="string"||t.title===null}var $N=new O(""),wl=(()=>{class t{activated=null;get activatedComponentRef(){return this.activated}_activatedRoute=null;name=pt;activateEvents=new U;deactivateEvents=new U;attachEvents=new U;detachEvents=new U;routerOutletData=re();parentContexts=u(Yd);location=u(st);changeDetector=u(ye);inputBinder=u(zf,{optional:!0});supportsBindingToComponentInputs=!0;ngOnChanges(e){if(e.name){let{firstChange:i,previousValue:r}=e.name;if(i)return;this.isTrackedInParentContexts(r)&&(this.deactivate(),this.parentContexts.onChildOutletDestroyed(r)),this.initializeOutletWithName()}}ngOnDestroy(){this.isTrackedInParentContexts(this.name)&&this.parentContexts.onChildOutletDestroyed(this.name),this.inputBinder?.unsubscribeFromRouteData(this)}isTrackedInParentContexts(e){return this.parentContexts.getContext(e)?.outlet===this}ngOnInit(){this.initializeOutletWithName()}initializeOutletWithName(){if(this.parentContexts.onChildOutletCreated(this.name,this),this.activated)return;let e=this.parentContexts.getContext(this.name);e?.route&&(e.attachRef?this.attach(e.attachRef,e.route):this.activateWith(e.route,e.injector))}get isActivated(){return!!this.activated}get component(){if(!this.activated)throw new ue(4012,!1);return this.activated.instance}get activatedRoute(){if(!this.activated)throw new ue(4012,!1);return this._activatedRoute}get activatedRouteData(){return this._activatedRoute?this._activatedRoute.snapshot.data:{}}detach(){if(!this.activated)throw new ue(4012,!1);this.location.detach();let e=this.activated;return this.activated=null,this._activatedRoute=null,this.detachEvents.emit(e.instance),e}attach(e,i){this.activated=e,this._activatedRoute=i,this.location.insert(e.hostView),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.attachEvents.emit(e.instance)}deactivate(){if(this.activated){let e=this.component;this.activated.destroy(),this.activated=null,this._activatedRoute=null,this.deactivateEvents.emit(e)}}activateWith(e,i){if(this.isActivated)throw new ue(4013,!1);this._activatedRoute=e;let r=this.location,a=e.snapshot.component,s=this.parentContexts.getOrCreateContext(this.name).children,l=new KE(e,s,r.injector,this.routerOutletData);this.activated=r.createComponent(a,{index:r.length,injector:l,environmentInjector:i}),this.changeDetector.markForCheck(),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.activateEvents.emit(this.activated.instance)}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["router-outlet"]],inputs:{name:"name",routerOutletData:[1,"routerOutletData"]},outputs:{activateEvents:"activate",deactivateEvents:"deactivate",attachEvents:"attach",detachEvents:"detach"},exportAs:["outlet"],features:[Oe]})}return t})(),KE=class{route;childContexts;parent;outletData;constructor(n,e,i,r){this.route=n,this.childContexts=e,this.parent=i,this.outletData=r}get(n,e){return n===Ai?this.route:n===Yd?this.childContexts:n===$N?this.outletData:this.parent.get(n,e)}},zf=new O(""),nS=(()=>{class t{outletDataSubscriptions=new Map;bindActivatedRouteToOutletComponent(e){this.unsubscribeFromRouteData(e),this.subscribeToRouteData(e)}unsubscribeFromRouteData(e){this.outletDataSubscriptions.get(e)?.unsubscribe(),this.outletDataSubscriptions.delete(e)}subscribeToRouteData(e){let{activatedRoute:i}=e,r=yo([i.queryParams,i.params,i.data]).pipe(je(([o,a,s],l)=>(s=I(I(I({},o),a),s),l===0?Q(s):Promise.resolve(s)))).subscribe(o=>{if(!e.isActivated||!e.activatedComponentRef||e.activatedRoute!==i||i.component===null){this.unsubscribeFromRouteData(e);return}let a=g2(i.component);if(!a){this.unsubscribeFromRouteData(e);return}for(let{templateName:s}of a.inputs)e.activatedComponentRef.setInput(s,o[s])});this.outletDataSubscriptions.set(e,r)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),rS=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["ng-component"]],exportAs:["emptyRouterOutlet"],decls:1,vars:0,template:function(i,r){i&1&&M(0,"router-outlet")},dependencies:[wl],encapsulation:2})}return t})();function oS(t){let n=t.children&&t.children.map(oS),e=n?Me(I({},t),{children:n}):I({},t);return!e.component&&!e.loadComponent&&(n||e.loadChildren)&&e.outlet&&e.outlet!==pt&&(e.component=rS),e}function LQ(t,n,e){let i=Vf(t,n._root,e?e._root:void 0);return new Nf(i,n)}function Vf(t,n,e){if(e&&t.shouldReuseRoute(n.value,e.value.snapshot)){let i=e.value;i._futureSnapshot=n.value;let r=VQ(t,n,e);return new Mo(i,r)}else{if(t.shouldAttach(n.value)){let o=t.retrieve(n.value);if(o!==null){let a=o.route;return a.value._futureSnapshot=n.value,a.children=n.children.map(s=>Vf(t,s)),a}}let i=BQ(n.value),r=n.children.map(o=>Vf(t,o));return new Mo(i,r)}}function VQ(t,n,e){return n.children.map(i=>{for(let r of e.children)if(t.shouldReuseRoute(i.value,r.value.snapshot))return Vf(t,i,r);return Vf(t,i)})}function BQ(t){return new Ai(new rt(t.url),new rt(t.params),new rt(t.queryParams),new rt(t.fragment),new rt(t.data),t.outlet,t.component,t)}var $m=class{redirectTo;navigationBehaviorOptions;constructor(n,e){this.redirectTo=n,this.navigationBehaviorOptions=e}},WN="ngNavigationCancelingError";function Ev(t,n){let{redirectTo:e,navigationBehaviorOptions:i}=bc(n)?{redirectTo:n,navigationBehaviorOptions:void 0}:n,r=GN(!1,Ur.Redirect);return r.url=e,r.navigationBehaviorOptions=i,r}function GN(t,n){let e=new Error(`NavigationCancelingError: ${t||""}`);return e[WN]=!0,e.cancellationCode=n,e}function jQ(t){return qN(t)&&bc(t.url)}function qN(t){return!!t&&t[WN]}var HQ=(t,n,e,i)=>se(r=>(new ZE(n,r.targetRouterState,r.currentRouterState,e,i).activate(t),r)),ZE=class{routeReuseStrategy;futureState;currState;forwardEvent;inputBindingEnabled;constructor(n,e,i,r,o){this.routeReuseStrategy=n,this.futureState=e,this.currState=i,this.forwardEvent=r,this.inputBindingEnabled=o}activate(n){let e=this.futureState._root,i=this.currState?this.currState._root:null;this.deactivateChildRoutes(e,i,n),jE(this.futureState.root),this.activateChildRoutes(e,i,n)}deactivateChildRoutes(n,e,i){let r=Lm(e);n.children.forEach(o=>{let a=o.value.outlet;this.deactivateRoutes(o,r[a],i),delete r[a]}),Object.values(r).forEach(o=>{this.deactivateRouteAndItsChildren(o,i)})}deactivateRoutes(n,e,i){let r=n.value,o=e?e.value:null;if(r===o)if(r.component){let a=i.getContext(r.outlet);a&&this.deactivateChildRoutes(n,e,a.children)}else this.deactivateChildRoutes(n,e,i);else o&&this.deactivateRouteAndItsChildren(e,i)}deactivateRouteAndItsChildren(n,e){n.value.component&&this.routeReuseStrategy.shouldDetach(n.value.snapshot)?this.detachAndStoreRouteSubtree(n,e):this.deactivateRouteAndOutlet(n,e)}detachAndStoreRouteSubtree(n,e){let i=e.getContext(n.value.outlet),r=i&&n.value.component?i.children:e,o=Lm(n);for(let a of Object.values(o))this.deactivateRouteAndItsChildren(a,r);if(i&&i.outlet){let a=i.outlet.detach(),s=i.children.onOutletDeactivated();this.routeReuseStrategy.store(n.value.snapshot,{componentRef:a,route:n,contexts:s})}}deactivateRouteAndOutlet(n,e){let i=e.getContext(n.value.outlet),r=i&&n.value.component?i.children:e,o=Lm(n);for(let a of Object.values(o))this.deactivateRouteAndItsChildren(a,r);i&&(i.outlet&&(i.outlet.deactivate(),i.children.onOutletDeactivated()),i.attachRef=null,i.route=null)}activateChildRoutes(n,e,i){let r=Lm(e);n.children.forEach(o=>{this.activateRoutes(o,r[o.value.outlet],i),this.forwardEvent(new Cv(o.value.snapshot))}),n.children.length&&this.forwardEvent(new yv(n.value.snapshot))}activateRoutes(n,e,i){let r=n.value,o=e?e.value:null;if(jE(r),r===o)if(r.component){let a=i.getOrCreateContext(r.outlet);this.activateChildRoutes(n,e,a.children)}else this.activateChildRoutes(n,e,i);else if(r.component){let a=i.getOrCreateContext(r.outlet);if(this.routeReuseStrategy.shouldAttach(r.snapshot)){let s=this.routeReuseStrategy.retrieve(r.snapshot);this.routeReuseStrategy.store(r.snapshot,null),a.children.onOutletReAttached(s.contexts),a.attachRef=s.componentRef,a.route=s.route.value,a.outlet&&a.outlet.attach(s.componentRef,s.route.value),jE(s.route.value),this.activateChildRoutes(n,null,a.children)}else a.attachRef=null,a.route=r,a.outlet&&a.outlet.activateWith(r,a.injector),this.activateChildRoutes(n,null,a.children)}else this.activateChildRoutes(n,null,i)}},Sv=class{path;route;constructor(n){this.path=n,this.route=this.path[this.path.length-1]}},jm=class{component;route;constructor(n,e){this.component=n,this.route=e}};function zQ(t,n,e){let i=t._root,r=n?n._root:null;return If(i,r,e,[i.value])}function UQ(t){let n=t.routeConfig?t.routeConfig.canActivateChild:null;return!n||n.length===0?null:{node:t,guards:n}}function qm(t,n){let e=Symbol(),i=n.get(t,e);return i===e?typeof t=="function"&&!bD(t)?t:n.get(t):i}function If(t,n,e,i,r={canDeactivateChecks:[],canActivateChecks:[]}){let o=Lm(n);return t.children.forEach(a=>{$Q(a,o[a.value.outlet],e,i.concat([a.value]),r),delete o[a.value.outlet]}),Object.entries(o).forEach(([a,s])=>Of(s,e.getContext(a),r)),r}function $Q(t,n,e,i,r={canDeactivateChecks:[],canActivateChecks:[]}){let o=t.value,a=n?n.value:null,s=e?e.getContext(t.value.outlet):null;if(a&&o.routeConfig===a.routeConfig){let l=WQ(a,o,o.routeConfig.runGuardsAndResolvers);l?r.canActivateChecks.push(new Sv(i)):(o.data=a.data,o._resolvedData=a._resolvedData),o.component?If(t,n,s?s.children:null,i,r):If(t,n,e,i,r),l&&s&&s.outlet&&s.outlet.isActivated&&r.canDeactivateChecks.push(new jm(s.outlet.component,a))}else a&&Of(n,s,r),r.canActivateChecks.push(new Sv(i)),o.component?If(t,null,s?s.children:null,i,r):If(t,null,e,i,r);return r}function WQ(t,n,e){if(typeof e=="function")return e(t,n);switch(e){case"pathParamsChange":return!Wd(t.url,n.url);case"pathParamsOrQueryParamsChange":return!Wd(t.url,n.url)||!Ts(t.queryParams,n.queryParams);case"always":return!0;case"paramsOrQueryParamsChange":return!QE(t,n)||!Ts(t.queryParams,n.queryParams);case"paramsChange":default:return!QE(t,n)}}function Of(t,n,e){let i=Lm(t),r=t.value;Object.entries(i).forEach(([o,a])=>{r.component?n?Of(a,n.children.getContext(o),e):Of(a,null,e):Of(a,n,e)}),r.component?n&&n.outlet&&n.outlet.isActivated?e.canDeactivateChecks.push(new jm(n.outlet.component,r)):e.canDeactivateChecks.push(new jm(null,r)):e.canDeactivateChecks.push(new jm(null,r))}function Uf(t){return typeof t=="function"}function GQ(t){return typeof t=="boolean"}function qQ(t){return t&&Uf(t.canLoad)}function YQ(t){return t&&Uf(t.canActivate)}function QQ(t){return t&&Uf(t.canActivateChild)}function KQ(t){return t&&Uf(t.canDeactivate)}function ZQ(t){return t&&Uf(t.canMatch)}function YN(t){return t instanceof rl||t?.name==="EmptyError"}var sv=Symbol("INITIAL_VALUE");function Wm(){return je(t=>yo(t.map(n=>n.pipe(mt(1),Ue(sv)))).pipe(se(n=>{for(let e of n)if(e!==!0){if(e===sv)return sv;if(e===!1||XQ(e))return e}return!0}),ce(n=>n!==sv),mt(1)))}function XQ(t){return bc(t)||t instanceof $m}function JQ(t,n){return Vt(e=>{let{targetSnapshot:i,currentSnapshot:r,guards:{canActivateChecks:o,canDeactivateChecks:a}}=e;return a.length===0&&o.length===0?Q(Me(I({},e),{guardsResult:!0})):eK(a,i,r,t).pipe(Vt(s=>s&&GQ(s)?tK(i,o,t,n):Q(s)),se(s=>Me(I({},e),{guardsResult:s})))})}function eK(t,n,e,i){return Ut(t).pipe(Vt(r=>aK(r.component,r.route,e,n,i)),xn(r=>r!==!0,!0))}function tK(t,n,e,i){return Ut(n).pipe(jo(r=>Co(nK(r.route.parent,i),iK(r.route,i),oK(t,r.path,e),rK(t,r.route,e))),xn(r=>r!==!0,!0))}function iK(t,n){return t!==null&&n&&n(new xv(t)),Q(!0)}function nK(t,n){return t!==null&&n&&n(new vv(t)),Q(!0)}function rK(t,n,e){let i=n.routeConfig?n.routeConfig.canActivate:null;if(!i||i.length===0)return Q(!0);let r=i.map(o=>Fn(()=>{let a=Gm(n)??e,s=qm(o,a),l=YQ(s)?s.canActivate(n,t):Vn(a,()=>s(n,t));return Cl(l).pipe(xn())}));return Q(r).pipe(Wm())}function oK(t,n,e){let i=n[n.length-1],o=n.slice(0,n.length-1).reverse().map(a=>UQ(a)).filter(a=>a!==null).map(a=>Fn(()=>{let s=a.guards.map(l=>{let c=Gm(a.node)??e,d=qm(l,c),p=QQ(d)?d.canActivateChild(i,t):Vn(c,()=>d(i,t));return Cl(p).pipe(xn())});return Q(s).pipe(Wm())}));return Q(o).pipe(Wm())}function aK(t,n,e,i,r){let o=n&&n.routeConfig?n.routeConfig.canDeactivate:null;if(!o||o.length===0)return Q(!0);let a=o.map(s=>{let l=Gm(n)??r,c=qm(s,l),d=KQ(c)?c.canDeactivate(t,n,e,i):Vn(l,()=>c(t,n,e,i));return Cl(d).pipe(xn())});return Q(a).pipe(Wm())}function sK(t,n,e,i){let r=n.canLoad;if(r===void 0||r.length===0)return Q(!0);let o=r.map(a=>{let s=qm(a,t),l=qQ(s)?s.canLoad(n,e):Vn(t,()=>s(n,e));return Cl(l)});return Q(o).pipe(Wm(),QN(i))}function QN(t){return Gw(He(n=>{if(typeof n!="boolean")throw Ev(t,n)}),se(n=>n===!0))}function lK(t,n,e,i){let r=n.canMatch;if(!r||r.length===0)return Q(!0);let o=r.map(a=>{let s=qm(a,t),l=ZQ(s)?s.canMatch(n,e):Vn(t,()=>s(n,e));return Cl(l)});return Q(o).pipe(Wm(),QN(i))}var Bf=class{segmentGroup;constructor(n){this.segmentGroup=n||null}},jf=class extends Error{urlTree;constructor(n){super(),this.urlTree=n}};function Nm(t){return er(new Bf(t))}function cK(t){return er(new ue(4e3,!1))}function dK(t){return er(GN(!1,Ur.GuardRejected))}var XE=class{urlSerializer;urlTree;constructor(n,e){this.urlSerializer=n,this.urlTree=e}lineralizeSegments(n,e){let i=[],r=e.root;for(;;){if(i=i.concat(r.segments),r.numberOfChildren===0)return Q(i);if(r.numberOfChildren>1||!r.children[pt])return cK(`${n.redirectTo}`);r=r.children[pt]}}applyRedirectCommands(n,e,i,r,o){return uK(e,r,o).pipe(se(a=>{if(a instanceof Is)throw new jf(a);let s=this.applyRedirectCreateUrlTree(a,this.urlSerializer.parse(a),n,i);if(a[0]==="/")throw new jf(s);return s}))}applyRedirectCreateUrlTree(n,e,i,r){let o=this.createSegmentGroup(n,e.root,i,r);return new Is(o,this.createQueryParams(e.queryParams,this.urlTree.queryParams),e.fragment)}createQueryParams(n,e){let i={};return Object.entries(n).forEach(([r,o])=>{if(typeof o=="string"&&o[0]===":"){let s=o.substring(1);i[r]=e[s]}else i[r]=o}),i}createSegmentGroup(n,e,i,r){let o=this.createSegments(n,e.segments,i,r),a={};return Object.entries(e.children).forEach(([s,l])=>{a[s]=this.createSegmentGroup(n,l,i,r)}),new ri(o,a)}createSegments(n,e,i,r){return e.map(o=>o.path[0]===":"?this.findPosParam(n,o,r):this.findOrReturn(o,i))}findPosParam(n,e,i){let r=i[e.path.substring(1)];if(!r)throw new ue(4001,!1);return r}findOrReturn(n,e){let i=0;for(let r of e){if(r.path===n.path)return e.splice(i),r;i++}return n}};function uK(t,n,e){if(typeof t=="string")return Q(t);let i=t,{queryParams:r,fragment:o,routeConfig:a,url:s,outlet:l,params:c,data:d,title:p}=n;return Cl(Vn(e,()=>i({params:c,data:d,queryParams:r,fragment:o,routeConfig:a,url:s,outlet:l,title:p})))}var JE={matched:!1,consumedSegments:[],remainingSegments:[],parameters:{},positionalParamSegments:{}};function mK(t,n,e,i,r){let o=KN(t,n,e);return o.matched?(i=PQ(n,i),lK(i,n,e,r).pipe(se(a=>a===!0?o:I({},JE)))):Q(o)}function KN(t,n,e){if(n.path==="**")return hK(e);if(n.path==="")return n.pathMatch==="full"&&(t.hasChildren()||e.length>0)?I({},JE):{matched:!0,consumedSegments:[],remainingSegments:e,parameters:{},positionalParamSegments:{}};let r=(n.matcher||MN)(e,t,n);if(!r)return I({},JE);let o={};Object.entries(r.posParams??{}).forEach(([s,l])=>{o[s]=l.path});let a=r.consumed.length>0?I(I({},o),r.consumed[r.consumed.length-1].parameters):o;return{matched:!0,consumedSegments:r.consumed,remainingSegments:e.slice(r.consumed.length),parameters:a,positionalParamSegments:r.posParams??{}}}function hK(t){return{matched:!0,parameters:t.length>0?SN(t).parameters:{},consumedSegments:t,remainingSegments:[],positionalParamSegments:{}}}function CN(t,n,e,i){return e.length>0&&gK(t,e,i)?{segmentGroup:new ri(n,fK(i,new ri(e,t.children))),slicedSegments:[]}:e.length===0&&_K(t,e,i)?{segmentGroup:new ri(t.segments,pK(t,e,i,t.children)),slicedSegments:e}:{segmentGroup:new ri(t.segments,t.children),slicedSegments:e}}function pK(t,n,e,i){let r={};for(let o of e)if(Tv(t,n,o)&&!i[Ga(o)]){let a=new ri([],{});r[Ga(o)]=a}return I(I({},i),r)}function fK(t,n){let e={};e[pt]=n;for(let i of t)if(i.path===""&&Ga(i)!==pt){let r=new ri([],{});e[Ga(i)]=r}return e}function gK(t,n,e){return e.some(i=>Tv(t,n,i)&&Ga(i)!==pt)}function _K(t,n,e){return e.some(i=>Tv(t,n,i))}function Tv(t,n,e){return(t.hasChildren()||n.length>0)&&e.pathMatch==="full"?!1:e.path===""}function bK(t,n,e){return n.length===0&&!t.children[e]}var eS=class{};function vK(t,n,e,i,r,o,a="emptyOnly"){return new tS(t,n,e,i,r,a,o).recognize()}var yK=31,tS=class{injector;configLoader;rootComponentType;config;urlTree;paramsInheritanceStrategy;urlSerializer;applyRedirects;absoluteRedirectCount=0;allowRedirects=!0;constructor(n,e,i,r,o,a,s){this.injector=n,this.configLoader=e,this.rootComponentType=i,this.config=r,this.urlTree=o,this.paramsInheritanceStrategy=a,this.urlSerializer=s,this.applyRedirects=new XE(this.urlSerializer,this.urlTree)}noMatchError(n){return new ue(4002,`'${n.segmentGroup}'`)}recognize(){let n=CN(this.urlTree.root,[],[],this.config).segmentGroup;return this.match(n).pipe(se(({children:e,rootSnapshot:i})=>{let r=new Mo(i,e),o=new Lf("",r),a=NN(i,[],this.urlTree.queryParams,this.urlTree.fragment);return a.queryParams=this.urlTree.queryParams,o.url=this.urlSerializer.serialize(a),{state:o,tree:a}}))}match(n){let e=new Gd([],Object.freeze({}),Object.freeze(I({},this.urlTree.queryParams)),this.urlTree.fragment,Object.freeze({}),pt,this.rootComponentType,null,{});return this.processSegmentGroup(this.injector,this.config,n,pt,e).pipe(se(i=>({children:i,rootSnapshot:e})),ei(i=>{if(i instanceof jf)return this.urlTree=i.urlTree,this.match(i.urlTree.root);throw i instanceof Bf?this.noMatchError(i):i}))}processSegmentGroup(n,e,i,r,o){return i.segments.length===0&&i.hasChildren()?this.processChildren(n,e,i,o):this.processSegment(n,e,i,i.segments,r,!0,o).pipe(se(a=>a instanceof Mo?[a]:[]))}processChildren(n,e,i,r){let o=[];for(let a of Object.keys(i.children))a==="primary"?o.unshift(a):o.push(a);return Ut(o).pipe(jo(a=>{let s=i.children[a],l=FQ(e,a);return this.processSegmentGroup(n,l,s,a,r)}),nD((a,s)=>(a.push(...s),a)),Jl(null),iD(),Vt(a=>{if(a===null)return Nm(i);let s=ZN(a);return xK(s),Q(s)}))}processSegment(n,e,i,r,o,a,s){return Ut(e).pipe(jo(l=>this.processSegmentAgainstRoute(l._injector??n,e,l,i,r,o,a,s).pipe(ei(c=>{if(c instanceof Bf)return Q(null);throw c}))),xn(l=>!!l),ei(l=>{if(YN(l))return bK(i,r,o)?Q(new eS):Nm(i);throw l}))}processSegmentAgainstRoute(n,e,i,r,o,a,s,l){return Ga(i)!==a&&(a===pt||!Tv(r,o,i))?Nm(r):i.redirectTo===void 0?this.matchSegmentAgainstRoute(n,r,i,o,a,l):this.allowRedirects&&s?this.expandSegmentAgainstRouteUsingRedirect(n,r,e,i,o,a,l):Nm(r)}expandSegmentAgainstRouteUsingRedirect(n,e,i,r,o,a,s){let{matched:l,parameters:c,consumedSegments:d,positionalParamSegments:p,remainingSegments:_}=KN(e,r,o);if(!l)return Nm(e);typeof r.redirectTo=="string"&&r.redirectTo[0]==="/"&&(this.absoluteRedirectCount++,this.absoluteRedirectCount>yK&&(this.allowRedirects=!1));let b=new Gd(o,c,Object.freeze(I({},this.urlTree.queryParams)),this.urlTree.fragment,wN(r),Ga(r),r.component??r._loadedComponent??null,r,DN(r)),y=Mv(b,s,this.paramsInheritanceStrategy);return b.params=Object.freeze(y.params),b.data=Object.freeze(y.data),this.applyRedirects.applyRedirectCommands(d,r.redirectTo,p,b,n).pipe(je(C=>this.applyRedirects.lineralizeSegments(r,C)),Vt(C=>this.processSegment(n,i,e,C.concat(_),a,!1,s)))}matchSegmentAgainstRoute(n,e,i,r,o,a){let s=mK(e,i,r,n,this.urlSerializer);return i.path==="**"&&(e.children={}),s.pipe(je(l=>l.matched?(n=i._injector??n,this.getChildConfig(n,i,r).pipe(je(({routes:c})=>{let d=i._loadedInjector??n,{parameters:p,consumedSegments:_,remainingSegments:b}=l,y=new Gd(_,p,Object.freeze(I({},this.urlTree.queryParams)),this.urlTree.fragment,wN(i),Ga(i),i.component??i._loadedComponent??null,i,DN(i)),w=Mv(y,a,this.paramsInheritanceStrategy);y.params=Object.freeze(w.params),y.data=Object.freeze(w.data);let{segmentGroup:C,slicedSegments:D}=CN(e,_,b,c);if(D.length===0&&C.hasChildren())return this.processChildren(d,c,C,y).pipe(se(W=>new Mo(y,W)));if(c.length===0&&D.length===0)return Q(new Mo(y,[]));let F=Ga(i)===o;return this.processSegment(d,c,C,D,F?pt:o,!0,y).pipe(se(W=>new Mo(y,W instanceof Mo?[W]:[])))}))):Nm(e)))}getChildConfig(n,e,i){return e.children?Q({routes:e.children,injector:n}):e.loadChildren?e._loadedRoutes!==void 0?Q({routes:e._loadedRoutes,injector:e._loadedInjector}):sK(n,e,i,this.urlSerializer).pipe(Vt(r=>r?this.configLoader.loadChildren(n,e).pipe(He(o=>{e._loadedRoutes=o.routes,e._loadedInjector=o.injector})):dK(e))):Q({routes:[],injector:n})}};function xK(t){t.sort((n,e)=>n.value.outlet===pt?-1:e.value.outlet===pt?1:n.value.outlet.localeCompare(e.value.outlet))}function CK(t){let n=t.value.routeConfig;return n&&n.path===""}function ZN(t){let n=[],e=new Set;for(let i of t){if(!CK(i)){n.push(i);continue}let r=n.find(o=>i.value.routeConfig===o.value.routeConfig);r!==void 0?(r.children.push(...i.children),e.add(r)):n.push(i)}for(let i of e){let r=ZN(i.children);n.push(new Mo(i.value,r))}return n.filter(i=>!e.has(i))}function wN(t){return t.data||{}}function DN(t){return t.resolve||{}}function wK(t,n,e,i,r,o){return Vt(a=>vK(t,n,e,i,a.extractedUrl,r,o).pipe(se(({state:s,tree:l})=>Me(I({},a),{targetSnapshot:s,urlAfterRedirects:l}))))}function DK(t,n){return Vt(e=>{let{targetSnapshot:i,guards:{canActivateChecks:r}}=e;if(!r.length)return Q(e);let o=new Set(r.map(l=>l.route)),a=new Set;for(let l of o)if(!a.has(l))for(let c of XN(l))a.add(c);let s=0;return Ut(a).pipe(jo(l=>o.has(l)?MK(l,i,t,n):(l.data=Mv(l,l.parent,t).resolve,Q(void 0))),He(()=>s++),Xu(1),Vt(l=>s===a.size?Q(e):zi))})}function XN(t){let n=t.children.map(e=>XN(e)).flat();return[t,...n]}function MK(t,n,e,i){let r=t.routeConfig,o=t._resolve;return r?.title!==void 0&&!UN(r)&&(o[Hf]=r.title),Fn(()=>(t.data=Mv(t,t.parent,e).resolve,EK(o,t,n,i).pipe(se(a=>(t._resolvedData=a,t.data=I(I({},t.data),a),null)))))}function EK(t,n,e,i){let r=UE(t);if(r.length===0)return Q({});let o={};return Ut(r).pipe(Vt(a=>SK(t[a],n,e,i).pipe(xn(),He(s=>{if(s instanceof $m)throw Ev(new _c,s);o[a]=s}))),Xu(1),se(()=>o),ei(a=>YN(a)?zi:er(a)))}function SK(t,n,e,i){let r=Gm(n)??i,o=qm(t,r),a=o.resolve?o.resolve(n,e):Vn(r,()=>o(n,e));return Cl(a)}function HE(t){return je(n=>{let e=t(n);return e?Ut(e).pipe(se(()=>n)):Q(n)})}var aS=(()=>{class t{buildTitle(e){let i,r=e.root;for(;r!==void 0;)i=this.getResolvedTitleForRoute(r)??i,r=r.children.find(o=>o.outlet===pt);return i}getResolvedTitleForRoute(e){return e.data[Hf]}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:()=>u(JN),providedIn:"root"})}return t})(),JN=(()=>{class t extends aS{title;constructor(e){super(),this.title=e}updateTitle(e){let i=this.buildTitle(e);i!==void 0&&this.title.setTitle(i)}static \u0275fac=function(i){return new(i||t)(pe(_N))};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),xc=new O("",{providedIn:"root",factory:()=>({})}),Qd=new O(""),Iv=(()=>{class t{componentLoaders=new WeakMap;childrenLoaders=new WeakMap;onLoadStartListener;onLoadEndListener;compiler=u(qM);loadComponent(e,i){if(this.componentLoaders.get(i))return this.componentLoaders.get(i);if(i._loadedComponent)return Q(i._loadedComponent);this.onLoadStartListener&&this.onLoadStartListener(i);let r=Cl(Vn(e,()=>i.loadComponent())).pipe(se(tL),je(iL),He(a=>{this.onLoadEndListener&&this.onLoadEndListener(i),i._loadedComponent=a}),Xr(()=>{this.componentLoaders.delete(i)})),o=new Ql(r,()=>new z).pipe(Wu());return this.componentLoaders.set(i,o),o}loadChildren(e,i){if(this.childrenLoaders.get(i))return this.childrenLoaders.get(i);if(i._loadedRoutes)return Q({routes:i._loadedRoutes,injector:i._loadedInjector});this.onLoadStartListener&&this.onLoadStartListener(i);let o=eL(i,this.compiler,e,this.onLoadEndListener).pipe(Xr(()=>{this.childrenLoaders.delete(i)})),a=new Ql(o,()=>new z).pipe(Wu());return this.childrenLoaders.set(i,a),a}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function eL(t,n,e,i){return Cl(Vn(e,()=>t.loadChildren())).pipe(se(tL),je(iL),Vt(r=>r instanceof k0||Array.isArray(r)?Q(r):Ut(n.compileModuleAsync(r))),se(r=>{i&&i(t);let o,a,s=!1;return Array.isArray(r)?(a=r,s=!0):(o=r.create(e).injector,a=o.get(Qd,[],{optional:!0,self:!0}).flat()),{routes:a.map(oS),injector:o}}))}function kK(t){return t&&typeof t=="object"&&"default"in t}function tL(t){return kK(t)?t.default:t}function iL(t){return Q(t)}var Av=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:()=>u(TK),providedIn:"root"})}return t})(),TK=(()=>{class t{shouldProcessUrl(e){return!0}extract(e){return e}merge(e,i){return e}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),sS=new O(""),lS=new O("");function nL(t,n,e){let i=t.get(lS),r=t.get(_e);if(!r.startViewTransition||i.skipNextTransition)return i.skipNextTransition=!1,new Promise(c=>setTimeout(c));let o,a=new Promise(c=>{o=c}),s=r.startViewTransition(()=>(o(),IK(t)));s.ready.catch(c=>{});let{onViewTransitionCreated:l}=i;return l&&Vn(t,()=>l({transition:s,from:n,to:e})),a}function IK(t){return new Promise(n=>{vt({read:()=>setTimeout(n)},{injector:t})})}var cS=new O(""),$f=(()=>{class t{currentNavigation=he(null,{equal:()=>!1});currentTransition=null;lastSuccessfulNavigation=null;events=new z;transitionAbortWithErrorSubject=new z;configLoader=u(Iv);environmentInjector=u(ti);destroyRef=u(ln);urlSerializer=u(yc);rootContexts=u(Yd);location=u(ks);inputBindingEnabled=u(zf,{optional:!0})!==null;titleStrategy=u(aS);options=u(xc,{optional:!0})||{};paramsInheritanceStrategy=this.options.paramsInheritanceStrategy||"emptyOnly";urlHandlingStrategy=u(Av);createViewTransition=u(sS,{optional:!0});navigationErrorHandler=u(cS,{optional:!0});navigationId=0;get hasRequestedNavigation(){return this.navigationId!==0}transitions;afterPreactivation=()=>Q(void 0);rootComponentType=null;destroyed=!1;constructor(){let e=r=>this.events.next(new _v(r)),i=r=>this.events.next(new bv(r));this.configLoader.onLoadEndListener=i,this.configLoader.onLoadStartListener=e,this.destroyRef.onDestroy(()=>{this.destroyed=!0})}complete(){this.transitions?.complete()}handleNavigationRequest(e){let i=++this.navigationId;Ni(()=>{this.transitions?.next(Me(I({},e),{extractedUrl:this.urlHandlingStrategy.extract(e.rawUrl),targetSnapshot:null,targetRouterState:null,guards:{canActivateChecks:[],canDeactivateChecks:[]},guardsResult:null,abortController:new AbortController,id:i}))})}setupNavigations(e){return this.transitions=new rt(null),this.transitions.pipe(ce(i=>i!==null),je(i=>{let r=!1;return Q(i).pipe(je(o=>{if(this.navigationId>i.id)return this.cancelNavigationTransition(i,"",Ur.SupersededByNewNavigation),zi;this.currentTransition=i,this.currentNavigation.set({id:o.id,initialUrl:o.rawUrl,extractedUrl:o.extractedUrl,targetBrowserUrl:typeof o.extras.browserUrl=="string"?this.urlSerializer.parse(o.extras.browserUrl):o.extras.browserUrl,trigger:o.source,extras:o.extras,previousNavigation:this.lastSuccessfulNavigation?Me(I({},this.lastSuccessfulNavigation),{previousNavigation:null}):null,abort:()=>o.abortController.abort()});let a=!e.navigated||this.isUpdatingInternalState()||this.isUpdatedBrowserUrl(),s=o.extras.onSameUrlNavigation??e.onSameUrlNavigation;if(!a&&s!=="reload")return this.events.next(new Os(o.id,this.urlSerializer.serialize(o.rawUrl),"",Hm.IgnoredSameUrlNavigation)),o.resolve(!1),zi;if(this.urlHandlingStrategy.shouldProcessUrl(o.rawUrl))return Q(o).pipe(je(l=>(this.events.next(new As(l.id,this.urlSerializer.serialize(l.extractedUrl),l.source,l.restoredState)),l.id!==this.navigationId?zi:Promise.resolve(l))),wK(this.environmentInjector,this.configLoader,this.rootComponentType,e.config,this.urlSerializer,this.paramsInheritanceStrategy),He(l=>{i.targetSnapshot=l.targetSnapshot,i.urlAfterRedirects=l.urlAfterRedirects,this.currentNavigation.update(d=>(d.finalUrl=l.urlAfterRedirects,d));let c=new Pf(l.id,this.urlSerializer.serialize(l.extractedUrl),this.urlSerializer.serialize(l.urlAfterRedirects),l.targetSnapshot);this.events.next(c)}));if(a&&this.urlHandlingStrategy.shouldProcessUrl(o.currentRawUrl)){let{id:l,extractedUrl:c,source:d,restoredState:p,extras:_}=o,b=new As(l,this.urlSerializer.serialize(c),d,p);this.events.next(b);let y=HN(this.rootComponentType).snapshot;return this.currentTransition=i=Me(I({},o),{targetSnapshot:y,urlAfterRedirects:c,extras:Me(I({},_),{skipLocationChange:!1,replaceUrl:!1})}),this.currentNavigation.update(w=>(w.finalUrl=c,w)),Q(i)}else return this.events.next(new Os(o.id,this.urlSerializer.serialize(o.extractedUrl),"",Hm.IgnoredByUrlHandlingStrategy)),o.resolve(!1),zi}),He(o=>{let a=new hv(o.id,this.urlSerializer.serialize(o.extractedUrl),this.urlSerializer.serialize(o.urlAfterRedirects),o.targetSnapshot);this.events.next(a)}),se(o=>(this.currentTransition=i=Me(I({},o),{guards:zQ(o.targetSnapshot,o.currentSnapshot,this.rootContexts)}),i)),JQ(this.environmentInjector,o=>this.events.next(o)),He(o=>{if(i.guardsResult=o.guardsResult,o.guardsResult&&typeof o.guardsResult!="boolean")throw Ev(this.urlSerializer,o.guardsResult);let a=new pv(o.id,this.urlSerializer.serialize(o.extractedUrl),this.urlSerializer.serialize(o.urlAfterRedirects),o.targetSnapshot,!!o.guardsResult);this.events.next(a)}),ce(o=>o.guardsResult?!0:(this.cancelNavigationTransition(o,"",Ur.GuardRejected),!1)),HE(o=>{if(o.guards.canActivateChecks.length!==0)return Q(o).pipe(He(a=>{let s=new fv(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);this.events.next(s)}),je(a=>{let s=!1;return Q(a).pipe(DK(this.paramsInheritanceStrategy,this.environmentInjector),He({next:()=>s=!0,complete:()=>{s||this.cancelNavigationTransition(a,"",Ur.NoDataFromResolver)}}))}),He(a=>{let s=new gv(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);this.events.next(s)}))}),HE(o=>{let a=s=>{let l=[];if(s.routeConfig?.loadComponent){let c=Gm(s)??this.environmentInjector;l.push(this.configLoader.loadComponent(c,s.routeConfig).pipe(He(d=>{s.component=d}),se(()=>{})))}for(let c of s.children)l.push(...a(c));return l};return yo(a(o.targetSnapshot.root)).pipe(Jl(null),mt(1))}),HE(()=>this.afterPreactivation()),je(()=>{let{currentSnapshot:o,targetSnapshot:a}=i,s=this.createViewTransition?.(this.environmentInjector,o.root,a.root);return s?Ut(s).pipe(se(()=>i)):Q(i)}),se(o=>{let a=LQ(e.routeReuseStrategy,o.targetSnapshot,o.currentRouterState);return this.currentTransition=i=Me(I({},o),{targetRouterState:a}),this.currentNavigation.update(s=>(s.targetRouterState=a,s)),i}),He(()=>{this.events.next(new Ff)}),HQ(this.rootContexts,e.routeReuseStrategy,o=>this.events.next(o),this.inputBindingEnabled),mt(1),xe(new Ne(o=>{let a=i.abortController.signal,s=()=>o.next();return a.addEventListener("abort",s),()=>a.removeEventListener("abort",s)}).pipe(ce(()=>!r&&!i.targetRouterState),He(()=>{this.cancelNavigationTransition(i,i.abortController.signal.reason+"",Ur.Aborted)}))),He({next:o=>{r=!0,this.lastSuccessfulNavigation=Ni(this.currentNavigation),this.events.next(new Si(o.id,this.urlSerializer.serialize(o.extractedUrl),this.urlSerializer.serialize(o.urlAfterRedirects))),this.titleStrategy?.updateTitle(o.targetRouterState.snapshot),o.resolve(!0)},complete:()=>{r=!0}}),xe(this.transitionAbortWithErrorSubject.pipe(He(o=>{throw o}))),Xr(()=>{r||this.cancelNavigationTransition(i,"",Ur.SupersededByNewNavigation),this.currentTransition?.id===i.id&&(this.currentNavigation.set(null),this.currentTransition=null)}),ei(o=>{if(this.destroyed)return i.resolve(!1),zi;if(r=!0,qN(o))this.events.next(new ea(i.id,this.urlSerializer.serialize(i.extractedUrl),o.message,o.cancellationCode)),jQ(o)?this.events.next(new Um(o.url,o.navigationBehaviorOptions)):i.resolve(!1);else{let a=new vc(i.id,this.urlSerializer.serialize(i.extractedUrl),o,i.targetSnapshot??void 0);try{let s=Vn(this.environmentInjector,()=>this.navigationErrorHandler?.(a));if(s instanceof $m){let{message:l,cancellationCode:c}=Ev(this.urlSerializer,s);this.events.next(new ea(i.id,this.urlSerializer.serialize(i.extractedUrl),l,c)),this.events.next(new Um(s.redirectTo,s.navigationBehaviorOptions))}else throw this.events.next(a),o}catch(s){this.options.resolveNavigationPromiseOnError?i.resolve(!1):i.reject(s)}}return zi}))}))}cancelNavigationTransition(e,i,r){let o=new ea(e.id,this.urlSerializer.serialize(e.extractedUrl),i,r);this.events.next(o),e.resolve(!1)}isUpdatingInternalState(){return this.currentTransition?.extractedUrl.toString()!==this.currentTransition?.currentUrlTree.toString()}isUpdatedBrowserUrl(){let e=this.urlHandlingStrategy.extract(this.urlSerializer.parse(this.location.path(!0))),i=Ni(this.currentNavigation),r=i?.targetBrowserUrl??i?.extractedUrl;return e.toString()!==r?.toString()&&!i?.extras.skipLocationChange}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function AK(t){return t!==Bm}var rL=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:()=>u(OK),providedIn:"root"})}return t})(),kv=class{shouldDetach(n){return!1}store(n,e){}shouldAttach(n){return!1}retrieve(n){return null}shouldReuseRoute(n,e){return n.routeConfig===e.routeConfig}},OK=(()=>{class t extends kv{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),oL=(()=>{class t{urlSerializer=u(yc);options=u(xc,{optional:!0})||{};canceledNavigationResolution=this.options.canceledNavigationResolution||"replace";location=u(ks);urlHandlingStrategy=u(Av);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";currentUrlTree=new Is;getCurrentUrlTree(){return this.currentUrlTree}rawUrlTree=this.currentUrlTree;getRawUrlTree(){return this.rawUrlTree}createBrowserPath({finalUrl:e,initialUrl:i,targetBrowserUrl:r}){let o=e!==void 0?this.urlHandlingStrategy.merge(e,i):i,a=r??o;return a instanceof Is?this.urlSerializer.serialize(a):a}commitTransition({targetRouterState:e,finalUrl:i,initialUrl:r}){i&&e?(this.currentUrlTree=i,this.rawUrlTree=this.urlHandlingStrategy.merge(i,r),this.routerState=e):this.rawUrlTree=r}routerState=HN(null);getRouterState(){return this.routerState}stateMemento=this.createStateMemento();updateStateMemento(){this.stateMemento=this.createStateMemento()}createStateMemento(){return{rawUrlTree:this.rawUrlTree,currentUrlTree:this.currentUrlTree,routerState:this.routerState}}resetInternalState({finalUrl:e}){this.routerState=this.stateMemento.routerState,this.currentUrlTree=this.stateMemento.currentUrlTree,this.rawUrlTree=this.urlHandlingStrategy.merge(this.currentUrlTree,e??this.rawUrlTree)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:()=>u(RK),providedIn:"root"})}return t})(),RK=(()=>{class t extends oL{currentPageId=0;lastSuccessfulId=-1;restoredState(){return this.location.getState()}get browserPageId(){return this.canceledNavigationResolution!=="computed"?this.currentPageId:this.restoredState()?.\u0275routerPageId??this.currentPageId}registerNonRouterCurrentEntryChangeListener(e){return this.location.subscribe(i=>{i.type==="popstate"&&setTimeout(()=>{e(i.url,i.state,"popstate")})})}handleRouterEvent(e,i){e instanceof As?this.updateStateMemento():e instanceof Os?this.commitTransition(i):e instanceof Pf?this.urlUpdateStrategy==="eager"&&(i.extras.skipLocationChange||this.setBrowserUrl(this.createBrowserPath(i),i)):e instanceof Ff?(this.commitTransition(i),this.urlUpdateStrategy==="deferred"&&!i.extras.skipLocationChange&&this.setBrowserUrl(this.createBrowserPath(i),i)):e instanceof ea&&e.code!==Ur.SupersededByNewNavigation&&e.code!==Ur.Redirect?this.restoreHistory(i):e instanceof vc?this.restoreHistory(i,!0):e instanceof Si&&(this.lastSuccessfulId=e.id,this.currentPageId=this.browserPageId)}setBrowserUrl(e,{extras:i,id:r}){let{replaceUrl:o,state:a}=i;if(this.location.isCurrentPathEqualTo(e)||o){let s=this.browserPageId,l=I(I({},a),this.generateNgRouterState(r,s));this.location.replaceState(e,"",l)}else{let s=I(I({},a),this.generateNgRouterState(r,this.browserPageId+1));this.location.go(e,"",s)}}restoreHistory(e,i=!1){if(this.canceledNavigationResolution==="computed"){let r=this.browserPageId,o=this.currentPageId-r;o!==0?this.location.historyGo(o):this.getCurrentUrlTree()===e.finalUrl&&o===0&&(this.resetInternalState(e),this.resetUrlToCurrentUrlTree())}else this.canceledNavigationResolution==="replace"&&(i&&this.resetInternalState(e),this.resetUrlToCurrentUrlTree())}resetUrlToCurrentUrlTree(){this.location.replaceState(this.urlSerializer.serialize(this.getRawUrlTree()),"",this.generateNgRouterState(this.lastSuccessfulId,this.currentPageId))}generateNgRouterState(e,i){return this.canceledNavigationResolution==="computed"?{navigationId:e,\u0275routerPageId:i}:{navigationId:e}}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function Ov(t,n){t.events.pipe(ce(e=>e instanceof Si||e instanceof ea||e instanceof vc||e instanceof Os),se(e=>e instanceof Si||e instanceof Os?0:(e instanceof ea?e.code===Ur.Redirect||e.code===Ur.SupersededByNewNavigation:!1)?2:1),ce(e=>e!==2),mt(1)).subscribe(()=>{n()})}var PK={paths:"exact",fragment:"ignored",matrixParams:"ignored",queryParams:"exact"},FK={paths:"subset",fragment:"ignored",matrixParams:"ignored",queryParams:"subset"},Ae=(()=>{class t{get currentUrlTree(){return this.stateManager.getCurrentUrlTree()}get rawUrlTree(){return this.stateManager.getRawUrlTree()}disposed=!1;nonRouterCurrentEntryChangeSubscription;console=u(T0);stateManager=u(oL);options=u(xc,{optional:!0})||{};pendingTasks=u(ys);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";navigationTransitions=u($f);urlSerializer=u(yc);location=u(ks);urlHandlingStrategy=u(Av);injector=u(ti);_events=new z;get events(){return this._events}get routerState(){return this.stateManager.getRouterState()}navigated=!1;routeReuseStrategy=u(rL);onSameUrlNavigation=this.options.onSameUrlNavigation||"ignore";config=u(Qd,{optional:!0})?.flat()??[];componentInputBindingEnabled=!!u(zf,{optional:!0});currentNavigation=this.navigationTransitions.currentNavigation.asReadonly();constructor(){this.resetConfig(this.config),this.navigationTransitions.setupNavigations(this).subscribe({error:e=>{this.console.warn(e)}}),this.subscribeToNavigationEvents()}eventsSubscription=new ke;subscribeToNavigationEvents(){let e=this.navigationTransitions.events.subscribe(i=>{try{let r=this.navigationTransitions.currentTransition,o=Ni(this.navigationTransitions.currentNavigation);if(r!==null&&o!==null){if(this.stateManager.handleRouterEvent(i,o),i instanceof ea&&i.code!==Ur.Redirect&&i.code!==Ur.SupersededByNewNavigation)this.navigated=!0;else if(i instanceof Si)this.navigated=!0;else if(i instanceof Um){let a=i.navigationBehaviorOptions,s=this.urlHandlingStrategy.merge(i.url,r.currentRawUrl),l=I({browserUrl:r.extras.browserUrl,info:r.extras.info,skipLocationChange:r.extras.skipLocationChange,replaceUrl:r.extras.replaceUrl||this.urlUpdateStrategy==="eager"||AK(r.source)},a);this.scheduleNavigation(s,Bm,null,l,{resolve:r.resolve,reject:r.reject,promise:r.promise})}}RQ(i)&&this._events.next(i)}catch(r){this.navigationTransitions.transitionAbortWithErrorSubject.next(r)}});this.eventsSubscription.add(e)}resetRootComponentType(e){this.routerState.root.component=e,this.navigationTransitions.rootComponentType=e}initialNavigation(){this.setUpLocationChangeListener(),this.navigationTransitions.hasRequestedNavigation||this.navigateToSyncWithBrowser(this.location.path(!0),Bm,this.stateManager.restoredState())}setUpLocationChangeListener(){this.nonRouterCurrentEntryChangeSubscription??=this.stateManager.registerNonRouterCurrentEntryChangeListener((e,i,r)=>{this.navigateToSyncWithBrowser(e,r,i)})}navigateToSyncWithBrowser(e,i,r){let o={replaceUrl:!0},a=r?.navigationId?r:null;if(r){let l=I({},r);delete l.navigationId,delete l.\u0275routerPageId,Object.keys(l).length!==0&&(o.state=l)}let s=this.parseUrl(e);this.scheduleNavigation(s,i,a,o).catch(l=>{this.disposed||this.injector.get(wr)(l)})}get url(){return this.serializeUrl(this.currentUrlTree)}getCurrentNavigation(){return Ni(this.navigationTransitions.currentNavigation)}get lastSuccessfulNavigation(){return this.navigationTransitions.lastSuccessfulNavigation}resetConfig(e){this.config=e.map(oS),this.navigated=!1}ngOnDestroy(){this.dispose()}dispose(){this._events.unsubscribe(),this.navigationTransitions.complete(),this.nonRouterCurrentEntryChangeSubscription&&(this.nonRouterCurrentEntryChangeSubscription.unsubscribe(),this.nonRouterCurrentEntryChangeSubscription=void 0),this.disposed=!0,this.eventsSubscription.unsubscribe()}createUrlTree(e,i={}){let{relativeTo:r,queryParams:o,fragment:a,queryParamsHandling:s,preserveFragment:l}=i,c=l?this.currentUrlTree.fragment:a,d=null;switch(s??this.options.defaultQueryParamsHandling){case"merge":d=I(I({},this.currentUrlTree.queryParams),o);break;case"preserve":d=this.currentUrlTree.queryParams;break;default:d=o||null}d!==null&&(d=this.removeEmptyProps(d));let p;try{let _=r?r.snapshot:this.routerState.snapshot.root;p=LN(_)}catch{(typeof e[0]!="string"||e[0][0]!=="/")&&(e=[]),p=this.currentUrlTree.root}return VN(p,e,d,c??null)}navigateByUrl(e,i={skipLocationChange:!1}){let r=bc(e)?e:this.parseUrl(e),o=this.urlHandlingStrategy.merge(r,this.rawUrlTree);return this.scheduleNavigation(o,Bm,null,i)}navigate(e,i={skipLocationChange:!1}){return NK(e),this.navigateByUrl(this.createUrlTree(e,i),i)}serializeUrl(e){return this.urlSerializer.serialize(e)}parseUrl(e){try{return this.urlSerializer.parse(e)}catch{return this.console.warn(zo(4018,!1)),this.urlSerializer.parse("/")}}isActive(e,i){let r;if(i===!0?r=I({},PK):i===!1?r=I({},FK):r=i,bc(e))return bN(this.currentUrlTree,e,r);let o=this.parseUrl(e);return bN(this.currentUrlTree,o,r)}removeEmptyProps(e){return Object.entries(e).reduce((i,[r,o])=>(o!=null&&(i[r]=o),i),{})}scheduleNavigation(e,i,r,o,a){if(this.disposed)return Promise.resolve(!1);let s,l,c;a?(s=a.resolve,l=a.reject,c=a.promise):c=new Promise((p,_)=>{s=p,l=_});let d=this.pendingTasks.add();return Ov(this,()=>{queueMicrotask(()=>this.pendingTasks.remove(d))}),this.navigationTransitions.handleNavigationRequest({source:i,restoredState:r,currentUrlTree:this.currentUrlTree,currentRawUrl:this.currentUrlTree,rawUrl:e,extras:o,resolve:s,reject:l,promise:c,currentSnapshot:this.routerState.snapshot,currentRouterState:this.routerState}),c.catch(p=>Promise.reject(p))}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function NK(t){for(let n=0;n{class t{router;route;tabIndexAttribute;renderer;el;locationStrategy;reactiveHref=he(null);get href(){return Ni(this.reactiveHref)}set href(e){this.reactiveHref.set(e)}target;queryParams;fragment;queryParamsHandling;state;info;relativeTo;isAnchorElement;subscription;onChanges=new z;applicationErrorHandler=u(wr);options=u(xc,{optional:!0});constructor(e,i,r,o,a,s){this.router=e,this.route=i,this.tabIndexAttribute=r,this.renderer=o,this.el=a,this.locationStrategy=s,this.reactiveHref.set(u(new Li("href"),{optional:!0}));let l=a.nativeElement.tagName?.toLowerCase();this.isAnchorElement=l==="a"||l==="area"||!!(typeof customElements=="object"&&customElements.get(l)?.observedAttributes?.includes?.("href")),this.isAnchorElement?this.setTabIndexIfNotOnNativeEl("0"):this.subscribeToNavigationEventsIfNecessary()}subscribeToNavigationEventsIfNecessary(){if(this.subscription!==void 0||!this.isAnchorElement)return;let e=this.preserveFragment,i=r=>r==="merge"||r==="preserve";e||=i(this.queryParamsHandling),e||=!this.queryParamsHandling&&!i(this.options?.defaultQueryParamsHandling),e&&(this.subscription=this.router.events.subscribe(r=>{r instanceof Si&&this.updateHref()}))}preserveFragment=!1;skipLocationChange=!1;replaceUrl=!1;setTabIndexIfNotOnNativeEl(e){this.tabIndexAttribute!=null||this.isAnchorElement||this.applyAttributeValue("tabindex",e)}ngOnChanges(e){this.isAnchorElement&&(this.updateHref(),this.subscribeToNavigationEventsIfNecessary()),this.onChanges.next(this)}routerLinkInput=null;set routerLink(e){e==null?(this.routerLinkInput=null,this.setTabIndexIfNotOnNativeEl(null)):(bc(e)?this.routerLinkInput=e:this.routerLinkInput=Array.isArray(e)?e:[e],this.setTabIndexIfNotOnNativeEl("0"))}onClick(e,i,r,o,a){let s=this.urlTree;if(s===null||this.isAnchorElement&&(e!==0||i||r||o||a||typeof this.target=="string"&&this.target!="_self"))return!0;let l={skipLocationChange:this.skipLocationChange,replaceUrl:this.replaceUrl,state:this.state,info:this.info};return this.router.navigateByUrl(s,l)?.catch(c=>{this.applicationErrorHandler(c)}),!this.isAnchorElement}ngOnDestroy(){this.subscription?.unsubscribe()}updateHref(){let e=this.urlTree;this.reactiveHref.set(e!==null&&this.locationStrategy?this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(e))??"":null)}applyAttributeValue(e,i){let r=this.renderer,o=this.el.nativeElement;i!==null?r.setAttribute(o,e,i):r.removeAttribute(o,e)}get urlTree(){return this.routerLinkInput===null?null:bc(this.routerLinkInput)?this.routerLinkInput:this.router.createUrlTree(this.routerLinkInput,{relativeTo:this.relativeTo!==void 0?this.relativeTo:this.route,queryParams:this.queryParams,fragment:this.fragment,queryParamsHandling:this.queryParamsHandling,preserveFragment:this.preserveFragment})}static \u0275fac=function(i){return new(i||t)(be(Ae),be(Ai),Jp("tabindex"),be(ze),be(Y),be(za))};static \u0275dir=P({type:t,selectors:[["","routerLink",""]],hostVars:2,hostBindings:function(i,r){i&1&&S("click",function(a){return r.onClick(a.button,a.ctrlKey,a.shiftKey,a.altKey,a.metaKey)}),i&2&&X("href",r.reactiveHref(),pM)("target",r.target)},inputs:{target:"target",queryParams:"queryParams",fragment:"fragment",queryParamsHandling:"queryParamsHandling",state:"state",info:"info",relativeTo:"relativeTo",preserveFragment:[2,"preserveFragment","preserveFragment",B],skipLocationChange:[2,"skipLocationChange","skipLocationChange",B],replaceUrl:[2,"replaceUrl","replaceUrl",B],routerLink:"routerLink"},features:[Oe]})}return t})(),Kd=(()=>{class t{router;element;renderer;cdr;link;links;classes=[];routerEventsSubscription;linkInputChangesSubscription;_isActive=!1;get isActive(){return this._isActive}routerLinkActiveOptions={exact:!1};ariaCurrentWhenActive;isActiveChange=new U;constructor(e,i,r,o,a){this.router=e,this.element=i,this.renderer=r,this.cdr=o,this.link=a,this.routerEventsSubscription=e.events.subscribe(s=>{s instanceof Si&&this.update()})}ngAfterContentInit(){Q(this.links.changes,Q(null)).pipe(xo()).subscribe(e=>{this.update(),this.subscribeToEachLinkOnChanges()})}subscribeToEachLinkOnChanges(){this.linkInputChangesSubscription?.unsubscribe();let e=[...this.links.toArray(),this.link].filter(i=>!!i).map(i=>i.onChanges);this.linkInputChangesSubscription=Ut(e).pipe(xo()).subscribe(i=>{this._isActive!==this.isLinkActive(this.router)(i)&&this.update()})}set routerLinkActive(e){let i=Array.isArray(e)?e:e.split(" ");this.classes=i.filter(r=>!!r)}ngOnChanges(e){this.update()}ngOnDestroy(){this.routerEventsSubscription.unsubscribe(),this.linkInputChangesSubscription?.unsubscribe()}update(){!this.links||!this.router.navigated||queueMicrotask(()=>{let e=this.hasActiveLinks();this.classes.forEach(i=>{e?this.renderer.addClass(this.element.nativeElement,i):this.renderer.removeClass(this.element.nativeElement,i)}),e&&this.ariaCurrentWhenActive!==void 0?this.renderer.setAttribute(this.element.nativeElement,"aria-current",this.ariaCurrentWhenActive.toString()):this.renderer.removeAttribute(this.element.nativeElement,"aria-current"),this._isActive!==e&&(this._isActive=e,this.cdr.markForCheck(),this.isActiveChange.emit(e))})}isLinkActive(e){let i=LK(this.routerLinkActiveOptions)?this.routerLinkActiveOptions:this.routerLinkActiveOptions.exact||!1;return r=>{let o=r.urlTree;return o?e.isActive(o,i):!1}}hasActiveLinks(){let e=this.isLinkActive(this.router);return this.link&&e(this.link)||this.links.some(e)}static \u0275fac=function(i){return new(i||t)(be(Ae),be(Y),be(ze),be(ye),be(Wn,8))};static \u0275dir=P({type:t,selectors:[["","routerLinkActive",""]],contentQueries:function(i,r,o){if(i&1&&Ce(o,Wn,5),i&2){let a;j(a=H())&&(r.links=a)}},inputs:{routerLinkActiveOptions:"routerLinkActiveOptions",ariaCurrentWhenActive:"ariaCurrentWhenActive",routerLinkActive:"routerLinkActive"},outputs:{isActiveChange:"isActiveChange"},exportAs:["routerLinkActive"],features:[Oe]})}return t})();function LK(t){return!!t.paths}var Wf=class{};var aL=(()=>{class t{router;injector;preloadingStrategy;loader;subscription;constructor(e,i,r,o){this.router=e,this.injector=i,this.preloadingStrategy=r,this.loader=o}setUpPreloading(){this.subscription=this.router.events.pipe(ce(e=>e instanceof Si),jo(()=>this.preload())).subscribe(()=>{})}preload(){return this.processRoutes(this.injector,this.router.config)}ngOnDestroy(){this.subscription&&this.subscription.unsubscribe()}processRoutes(e,i){let r=[];for(let o of i){o.providers&&!o._injector&&(o._injector=Dm(o.providers,e,`Route: ${o.path}`));let a=o._injector??e,s=o._loadedInjector??a;(o.loadChildren&&!o._loadedRoutes&&o.canLoad===void 0||o.loadComponent&&!o._loadedComponent)&&r.push(this.preloadConfig(a,o)),(o.children||o._loadedRoutes)&&r.push(this.processRoutes(s,o.children??o._loadedRoutes))}return Ut(r).pipe(xo())}preloadConfig(e,i){return this.preloadingStrategy.preload(i,()=>{let r;i.loadChildren&&i.canLoad===void 0?r=this.loader.loadChildren(e,i):r=Q(null);let o=r.pipe(Vt(a=>a===null?Q(void 0):(i._loadedRoutes=a.routes,i._loadedInjector=a.injector,this.processRoutes(a.injector??e,a.routes))));if(i.loadComponent&&!i._loadedComponent){let a=this.loader.loadComponent(e,i);return Ut([o,a]).pipe(xo())}else return o})}static \u0275fac=function(i){return new(i||t)(pe(Ae),pe(ti),pe(Wf),pe(Iv))};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),uS=new O(""),sL=(()=>{class t{urlSerializer;transitions;viewportScroller;zone;options;routerEventsSubscription;scrollEventsSubscription;lastId=0;lastSource=Bm;restoredId=0;store={};constructor(e,i,r,o,a={}){this.urlSerializer=e,this.transitions=i,this.viewportScroller=r,this.zone=o,this.options=a,a.scrollPositionRestoration||="disabled",a.anchorScrolling||="disabled"}init(){this.options.scrollPositionRestoration!=="disabled"&&this.viewportScroller.setHistoryScrollRestoration("manual"),this.routerEventsSubscription=this.createScrollEvents(),this.scrollEventsSubscription=this.consumeScrollEvents()}createScrollEvents(){return this.transitions.events.subscribe(e=>{e instanceof As?(this.store[this.lastId]=this.viewportScroller.getScrollPosition(),this.lastSource=e.navigationTrigger,this.restoredId=e.restoredState?e.restoredState.navigationId:0):e instanceof Si?(this.lastId=e.id,this.scheduleScrollEvent(e,this.urlSerializer.parse(e.urlAfterRedirects).fragment)):e instanceof Os&&e.code===Hm.IgnoredSameUrlNavigation&&(this.lastSource=void 0,this.restoredId=0,this.scheduleScrollEvent(e,this.urlSerializer.parse(e.url).fragment))})}consumeScrollEvents(){return this.transitions.events.subscribe(e=>{if(!(e instanceof zm))return;let i={behavior:"instant"};e.position?this.options.scrollPositionRestoration==="top"?this.viewportScroller.scrollToPosition([0,0],i):this.options.scrollPositionRestoration==="enabled"&&this.viewportScroller.scrollToPosition(e.position,i):e.anchor&&this.options.anchorScrolling==="enabled"?this.viewportScroller.scrollToAnchor(e.anchor):this.options.scrollPositionRestoration!=="disabled"&&this.viewportScroller.scrollToPosition([0,0])})}scheduleScrollEvent(e,i){this.zone.runOutsideAngular(()=>yn(this,null,function*(){yield new Promise(r=>{setTimeout(r),typeof requestAnimationFrame<"u"&&requestAnimationFrame(r)}),this.zone.run(()=>{this.transitions.events.next(new zm(e,this.lastSource==="popstate"?this.store[this.restoredId]:null,i))})}))}ngOnDestroy(){this.routerEventsSubscription?.unsubscribe(),this.scrollEventsSubscription?.unsubscribe()}static \u0275fac=function(i){jd()};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();function mS(t,...n){return Jr([{provide:Qd,multi:!0,useValue:t},[],{provide:Ai,useFactory:lL,deps:[Ae]},{provide:cf,multi:!0,useFactory:cL},n.map(e=>e.\u0275providers)])}function lL(t){return t.routerState.root}function Ym(t,n){return{\u0275kind:t,\u0275providers:n}}function hS(t={}){return Ym(4,[{provide:uS,useFactory:()=>{let e=u(yE),i=u(ae),r=u($f),o=u(yc);return new sL(o,r,e,i,t)}}])}function cL(){let t=u(de);return n=>{let e=t.get(tr);if(n!==e.components[0])return;let i=t.get(Ae),r=t.get(dL);t.get(pS)===1&&i.initialNavigation(),t.get(hL,null,{optional:!0})?.setUpPreloading(),t.get(uS,null,{optional:!0})?.init(),i.resetRootComponentType(e.componentTypes[0]),r.closed||(r.next(),r.complete(),r.unsubscribe())}}var dL=new O("",{factory:()=>new z}),pS=new O("",{providedIn:"root",factory:()=>1});function uL(){let t=[{provide:h0,useValue:!0},{provide:pS,useValue:0},hc(()=>{let n=u(de);return n.get(aE,Promise.resolve()).then(()=>new Promise(i=>{let r=n.get(Ae),o=n.get(dL);Ov(r,()=>{i(!0)}),n.get($f).afterPreactivation=()=>(i(!0),o.closed?Q(void 0):o),r.initialNavigation()}))})];return Ym(2,t)}function mL(){let t=[hc(()=>{u(Ae).setUpLocationChangeListener()}),{provide:pS,useValue:2}];return Ym(3,t)}var hL=new O("");function pL(t){return Ym(0,[{provide:hL,useExisting:aL},{provide:Wf,useExisting:t}])}function Rv(){return Ym(8,[nS,{provide:zf,useExisting:nS}])}function fL(t){Es("NgRouterViewTransitions");let n=[{provide:sS,useValue:nL},{provide:lS,useValue:I({skipNextTransition:!!t?.skipInitialTransition},t)}];return Ym(9,n)}var gL=[ks,{provide:yc,useClass:_c},Ae,Yd,{provide:Ai,useFactory:lL,deps:[Ae]},Iv,[]],fS=(()=>{class t{constructor(){}static forRoot(e,i){return{ngModule:t,providers:[gL,[],{provide:Qd,multi:!0,useValue:e},[],i?.errorHandler?{provide:cS,useValue:i.errorHandler}:[],{provide:xc,useValue:i||{}},i?.useHash?BK():jK(),VK(),i?.preloadingStrategy?pL(i.preloadingStrategy).\u0275providers:[],i?.initialNavigation?HK(i):[],i?.bindToComponentInputs?Rv().\u0275providers:[],i?.enableViewTransitions?fL().\u0275providers:[],zK()]}}static forChild(e){return{ngModule:t,providers:[{provide:Qd,multi:!0,useValue:e}]}}static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})();function VK(){return{provide:uS,useFactory:()=>{let t=u(yE),n=u(ae),e=u(xc),i=u($f),r=u(yc);return e.scrollOffset&&t.setOffset(e.scrollOffset),new sL(r,i,t,n,e)}}}function BK(){return{provide:za,useClass:fE}}function jK(){return{provide:za,useClass:j0}}function HK(t){return[t.initialNavigation==="disabled"?mL().\u0275providers:[],t.initialNavigation==="enabledBlocking"?uL().\u0275providers:[]]}var dS=new O("");function zK(){return[{provide:dS,useFactory:cL},{provide:cf,multi:!0,useExisting:dS}]}var _S=class{validateSignature(n){return Promise.resolve(null)}validateAtHash(n){return Promise.resolve(!0)}},Pv=class{};var Gf=class{},UK=(()=>{let n=class n extends Gf{now(){return Date.now()}new(){return new Date}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})();var Fv=class{},Nv=class{},$K=(()=>{let n=class n{constructor(){this.data=new Map}getItem(i){return this.data.get(i)}removeItem(i){this.data.delete(i)}setItem(i,r){this.data.set(i,r)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})();var qf=class{constructor(n){this.type=n}},$r=class extends qf{constructor(n,e=null){super(n),this.info=e}},qa=class extends qf{constructor(n,e=null){super(n),this.info=e}},Ki=class extends qf{constructor(n,e,i=null){super(n),this.reason=e,this.params=i}};function bL(t){let n=t.replace(/-/g,"+").replace(/_/g,"/");return decodeURIComponent(atob(n).split("").map(function(e){return"%"+("00"+e.charCodeAt(0).toString(16)).slice(-2)}).join(""))}function vL(t){return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}var Qm=class{constructor(n){this.clientId="",this.redirectUri="",this.postLogoutRedirectUri="",this.redirectUriAsPostLogoutRedirectUriFallback=!0,this.loginUrl="",this.scope="openid profile",this.resource="",this.rngUrl="",this.oidc=!0,this.requestAccessToken=!0,this.options=null,this.issuer="",this.logoutUrl="",this.clearHashAfterLogin=!0,this.tokenEndpoint=null,this.revocationEndpoint=null,this.customTokenParameters=[],this.userinfoEndpoint=null,this.responseType="",this.showDebugInformation=!1,this.silentRefreshRedirectUri="",this.silentRefreshMessagePrefix="",this.silentRefreshShowIFrame=!1,this.siletRefreshTimeout=1e3*20,this.silentRefreshTimeout=1e3*20,this.dummyClientSecret="",this.requireHttps="remoteOnly",this.strictDiscoveryDocumentValidation=!0,this.jwks=null,this.customQueryParams=null,this.silentRefreshIFrameName="angular-oauth-oidc-silent-refresh-iframe",this.timeoutFactor=.75,this.sessionChecksEnabled=!1,this.sessionCheckIntervall=3*1e3,this.sessionCheckIFrameUrl=null,this.sessionCheckIFrameName="angular-oauth-oidc-check-session-iframe",this.disableAtHashCheck=!1,this.skipSubjectCheck=!1,this.useIdTokenHintForSilentRefresh=!1,this.skipIssuerCheck=!1,this.nonceStateSeparator=";",this.useHttpBasicAuth=!1,this.decreaseExpirationBySec=0,this.waitForTokenInMsec=0,this.disablePKCE=!1,this.preserveRequestedRoute=!1,this.disableIdTokenTimer=!1,this.checkOrigin=!1,this.openUri=e=>{location.href=e},n&&Object.assign(this,n)}},Zd=class{encodeKey(n){return encodeURIComponent(n)}encodeValue(n){return encodeURIComponent(n)}decodeKey(n){return decodeURIComponent(n)}decodeValue(n){return decodeURIComponent(n)}},Lv=class{};var yL=(()=>{let n=class n{getHashFragmentParams(i){let r=i||window.location.hash;if(r=decodeURIComponent(r),r.indexOf("#")!==0)return{};let o=r.indexOf("?");return o>-1?r=r.substr(o+1):r=r.substr(1),this.parseQueryString(r)}parseQueryString(i){let r={},o,a,s,l,c,d;if(i===null)return r;let p=i.split("&");for(let _=0;_=64;){for(o=n[0],a=n[1],s=n[2],l=n[3],c=n[4],d=n[5],p=n[6],_=n[7],y=0;y<16;y++)w=i+y*4,t[y]=(e[w]&255)<<24|(e[w+1]&255)<<16|(e[w+2]&255)<<8|e[w+3]&255;for(y=16;y<64;y++)b=t[y-2],C=(b>>>17|b<<15)^(b>>>19|b<<13)^b>>>10,b=t[y-15],D=(b>>>7|b<<25)^(b>>>18|b<<14)^b>>>3,t[y]=(C+t[y-7]|0)+(D+t[y-16]|0);for(y=0;y<64;y++)C=(((c>>>6|c<<26)^(c>>>11|c<<21)^(c>>>25|c<<7))+(c&d^~c&p)|0)+(_+(GK[y]+t[y]|0)|0)|0,D=((o>>>2|o<<30)^(o>>>13|o<<19)^(o>>>22|o<<10))+(o&a^o&s^a&s)|0,_=p,p=d,d=c,c=l+C|0,l=s,s=a,a=o,o=C+D|0;n[0]+=o,n[1]+=a,n[2]+=s,n[3]+=l,n[4]+=c,n[5]+=d,n[6]+=p,n[7]+=_,i+=64,r-=64}return i}var bS=class{constructor(){this.digestLength=xL,this.blockSize=WK,this.state=new Int32Array(8),this.temp=new Int32Array(64),this.buffer=new Uint8Array(128),this.bufferLength=0,this.bytesHashed=0,this.finished=!1,this.reset()}reset(){return this.state[0]=1779033703,this.state[1]=3144134277,this.state[2]=1013904242,this.state[3]=2773480762,this.state[4]=1359893119,this.state[5]=2600822924,this.state[6]=528734635,this.state[7]=1541459225,this.bufferLength=0,this.bytesHashed=0,this.finished=!1,this}clean(){for(let n=0;n0){for(;this.bufferLength<64&&e>0;)this.buffer[this.bufferLength++]=n[i++],e--;this.bufferLength===64&&(gS(this.temp,this.state,this.buffer,0,64),this.bufferLength=0)}for(e>=64&&(i=gS(this.temp,this.state,n,i,e),e%=64);e>0;)this.buffer[this.bufferLength++]=n[i++],e--;return this}finish(n){if(!this.finished){let e=this.bytesHashed,i=this.bufferLength,r=e/536870912|0,o=e<<3,a=e%64<56?64:128;this.buffer[i]=128;for(let s=i+1;s>>24&255,this.buffer[a-7]=r>>>16&255,this.buffer[a-6]=r>>>8&255,this.buffer[a-5]=r>>>0&255,this.buffer[a-4]=o>>>24&255,this.buffer[a-3]=o>>>16&255,this.buffer[a-2]=o>>>8&255,this.buffer[a-1]=o>>>0&255,gS(this.temp,this.state,this.buffer,0,a),this.finished=!0}for(let e=0;e<8;e++)n[e*4+0]=this.state[e]>>>24&255,n[e*4+1]=this.state[e]>>>16&255,n[e*4+2]=this.state[e]>>>8&255,n[e*4+3]=this.state[e]>>>0&255;return this}digest(){let n=new Uint8Array(this.digestLength);return this.finish(n),n}_saveState(n){for(let e=0;e{let n=class n{calcHash(i,r){return yn(this,null,function*(){return QK(qK(YK(i)))})}toHashString2(i){let r="";for(let o of i)r+=String.fromCharCode(o);return r}toHashString(i){let r=new Uint8Array(i),o="";for(let a of r)o+=String.fromCharCode(a);return o}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),Km=(()=>{let n=class n extends Qm{constructor(i,r,o,a,s,l,c,d,p,_){super(),this.ngZone=i,this.http=r,this.config=s,this.urlHelper=l,this.logger=c,this.crypto=d,this.dateTimeService=_,this.discoveryDocumentLoaded=!1,this.state="",this.eventsSubject=new z,this.discoveryDocumentLoadedSubject=new z,this.grantTypesSupported=[],this.inImplicitFlow=!1,this.saveNoncesInLocalStorage=!1,this.debug("angular-oauth2-oidc v10"),this.document=p,s||(s={}),this.discoveryDocumentLoaded$=this.discoveryDocumentLoadedSubject.asObservable(),this.events=this.eventsSubject.asObservable(),a&&(this.tokenValidationHandler=a),s&&this.configure(s);try{o?this.setStorage(o):typeof sessionStorage<"u"&&this.setStorage(sessionStorage)}catch(b){console.error("No OAuthStorage provided and cannot access default (sessionStorage).Consider providing a custom OAuthStorage implementation in your module.",b)}if(this.checkLocalStorageAccessable()){let b=window?.navigator?.userAgent;(b?.includes("MSIE ")||b?.includes("Trident"))&&(this.saveNoncesInLocalStorage=!0)}this.setupRefreshTimer()}checkLocalStorageAccessable(){if(typeof window>"u")return!1;let i="test";try{return typeof window.localStorage>"u"?!1:(localStorage.setItem(i,i),localStorage.removeItem(i),!0)}catch{return!1}}configure(i){Object.assign(this,new Qm,i),this.config=Object.assign({},new Qm,i),this.sessionChecksEnabled&&this.setupSessionCheck(),this.configChanged()}configChanged(){this.setupRefreshTimer()}restartSessionChecksIfStillLoggedIn(){this.hasValidIdToken()&&this.initSessionCheck()}restartRefreshTimerIfStillLoggedIn(){this.setupExpirationTimers()}setupSessionCheck(){this.events.pipe(ce(i=>i.type==="token_received")).subscribe(()=>{this.initSessionCheck()})}setupAutomaticSilentRefresh(i={},r,o=!0){let a=!0;this.clearAutomaticRefreshTimer(),this.automaticRefreshSubscription=this.events.pipe(He(s=>{s.type==="token_received"?a=!0:s.type==="logout"&&(a=!1)}),ce(s=>s.type==="token_expires"&&(r==null||r==="any"||s.info===r)),Dt(1e3)).subscribe(()=>{a&&this.refreshInternal(i,o).catch(()=>{this.debug("Automatic silent refresh did not work")})}),this.restartRefreshTimerIfStillLoggedIn()}refreshInternal(i,r){return!this.useSilentRefresh&&this.responseType==="code"?this.refreshToken():this.silentRefresh(i,r)}loadDiscoveryDocumentAndTryLogin(i=null){return this.loadDiscoveryDocument().then(()=>this.tryLogin(i))}loadDiscoveryDocumentAndLogin(i=null){return i=i||{},this.loadDiscoveryDocumentAndTryLogin(i).then(()=>{if(!this.hasValidIdToken()||!this.hasValidAccessToken()){let r=typeof i.state=="string"?i.state:"";return this.initLoginFlow(r),!1}else return!0})}debug(...i){this.showDebugInformation&&this.logger.debug(...i)}validateUrlFromDiscoveryDocument(i){let r=[],o=this.validateUrlForHttps(i),a=this.validateUrlAgainstIssuer(i);return o||r.push("https for all urls required. Also for urls received by discovery."),a||r.push("Every url in discovery document has to start with the issuer url.Also see property strictDiscoveryDocumentValidation."),r}validateUrlForHttps(i){if(!i)return!0;let r=i.toLowerCase();return this.requireHttps===!1||(r.match(/^http:\/\/localhost($|[:/])/)||r.match(/^http:\/\/localhost($|[:/])/))&&this.requireHttps==="remoteOnly"?!0:r.startsWith("https://")}assertUrlNotNullAndCorrectProtocol(i,r){if(!i)throw new Error(`'${r}' should not be null`);if(!this.validateUrlForHttps(i))throw new Error(`'${r}' must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).`)}validateUrlAgainstIssuer(i){return!this.strictDiscoveryDocumentValidation||!i?!0:i.toLowerCase().startsWith(this.issuer.toLowerCase())}setupRefreshTimer(){if(typeof window>"u"){this.debug("timer not supported on this plattform");return}(this.hasValidIdToken()||this.hasValidAccessToken())&&(this.clearAccessTokenTimer(),this.clearIdTokenTimer(),this.setupExpirationTimers()),this.tokenReceivedSubscription&&this.tokenReceivedSubscription.unsubscribe(),this.tokenReceivedSubscription=this.events.pipe(ce(i=>i.type==="token_received")).subscribe(()=>{this.clearAccessTokenTimer(),this.clearIdTokenTimer(),this.setupExpirationTimers()})}setupExpirationTimers(){this.hasValidAccessToken()&&this.setupAccessTokenTimer(),!this.disableIdTokenTimer&&this.hasValidIdToken()&&this.setupIdTokenTimer()}setupAccessTokenTimer(){let i=this.getAccessTokenExpiration(),r=this.getAccessTokenStoredAt(),o=this.calcTimeout(r,i);this.ngZone.runOutsideAngular(()=>{this.accessTokenTimeoutSubscription=Q(new qa("token_expires","access_token")).pipe(Oa(o)).subscribe(a=>{this.ngZone.run(()=>{this.eventsSubject.next(a)})})})}setupIdTokenTimer(){let i=this.getIdTokenExpiration(),r=this.getIdTokenStoredAt(),o=this.calcTimeout(r,i);this.ngZone.runOutsideAngular(()=>{this.idTokenTimeoutSubscription=Q(new qa("token_expires","id_token")).pipe(Oa(o)).subscribe(a=>{this.ngZone.run(()=>{this.eventsSubject.next(a)})})})}stopAutomaticRefresh(){this.clearAccessTokenTimer(),this.clearIdTokenTimer(),this.clearAutomaticRefreshTimer()}clearAccessTokenTimer(){this.accessTokenTimeoutSubscription&&this.accessTokenTimeoutSubscription.unsubscribe()}clearIdTokenTimer(){this.idTokenTimeoutSubscription&&this.idTokenTimeoutSubscription.unsubscribe()}clearAutomaticRefreshTimer(){this.automaticRefreshSubscription&&this.automaticRefreshSubscription.unsubscribe()}calcTimeout(i,r){let o=this.dateTimeService.now(),a=(r-i)*this.timeoutFactor-(o-i),s=Math.max(0,a),l=2147483647;return s>l?l:s}setStorage(i){this._storage=i,this.configChanged()}loadDiscoveryDocument(i=null){return new Promise((r,o)=>{if(i||(i=this.issuer||"",i.endsWith("/")||(i+="/"),i+=".well-known/openid-configuration"),!this.validateUrlForHttps(i)){o("issuer must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).");return}this.http.get(i).subscribe(a=>{if(!this.validateDiscoveryDocument(a)){this.eventsSubject.next(new Ki("discovery_document_validation_error",null)),o("discovery_document_validation_error");return}this.loginUrl=a.authorization_endpoint,this.logoutUrl=a.end_session_endpoint||this.logoutUrl,this.grantTypesSupported=a.grant_types_supported,this.issuer=a.issuer,this.tokenEndpoint=a.token_endpoint,this.userinfoEndpoint=a.userinfo_endpoint||this.userinfoEndpoint,this.jwksUri=a.jwks_uri,this.sessionCheckIFrameUrl=a.check_session_iframe||this.sessionCheckIFrameUrl,this.discoveryDocumentLoaded=!0,this.discoveryDocumentLoadedSubject.next(a),this.revocationEndpoint=a.revocation_endpoint||this.revocationEndpoint,this.sessionChecksEnabled&&this.restartSessionChecksIfStillLoggedIn(),this.loadJwks().then(s=>{let l={discoveryDocument:a,jwks:s},c=new $r("discovery_document_loaded",l);this.eventsSubject.next(c),r(c)}).catch(s=>{this.eventsSubject.next(new Ki("discovery_document_load_error",s)),o(s)})},a=>{this.logger.error("error loading discovery document",a),this.eventsSubject.next(new Ki("discovery_document_load_error",a)),o(a)})})}loadJwks(){return new Promise((i,r)=>{this.jwksUri?this.http.get(this.jwksUri).subscribe(o=>{this.jwks=o,i(o)},o=>{this.logger.error("error loading jwks",o),this.eventsSubject.next(new Ki("jwks_load_error",o)),r(o)}):i(null)})}validateDiscoveryDocument(i){let r;return!this.skipIssuerCheck&&i.issuer!==this.issuer?(this.logger.error("invalid issuer in discovery document","expected: "+this.issuer,"current: "+i.issuer),!1):(r=this.validateUrlFromDiscoveryDocument(i.authorization_endpoint),r.length>0?(this.logger.error("error validating authorization_endpoint in discovery document",r),!1):(r=this.validateUrlFromDiscoveryDocument(i.end_session_endpoint),r.length>0?(this.logger.error("error validating end_session_endpoint in discovery document",r),!1):(r=this.validateUrlFromDiscoveryDocument(i.token_endpoint),r.length>0&&this.logger.error("error validating token_endpoint in discovery document",r),r=this.validateUrlFromDiscoveryDocument(i.revocation_endpoint),r.length>0&&this.logger.error("error validating revocation_endpoint in discovery document",r),r=this.validateUrlFromDiscoveryDocument(i.userinfo_endpoint),r.length>0?(this.logger.error("error validating userinfo_endpoint in discovery document",r),!1):(r=this.validateUrlFromDiscoveryDocument(i.jwks_uri),r.length>0?(this.logger.error("error validating jwks_uri in discovery document",r),!1):(this.sessionChecksEnabled&&!i.check_session_iframe&&this.logger.warn("sessionChecksEnabled is activated but discovery document does not contain a check_session_iframe field"),!0)))))}fetchTokenUsingPasswordFlowAndLoadUserProfile(i,r,o=new Sr){return this.fetchTokenUsingPasswordFlow(i,r,o).then(()=>this.loadUserProfile())}loadUserProfile(){if(!this.hasValidAccessToken())throw new Error("Can not load User Profile without access_token");if(!this.validateUrlForHttps(this.userinfoEndpoint))throw new Error("userinfoEndpoint must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).");return new Promise((i,r)=>{let o=new Sr().set("Authorization","Bearer "+this.getAccessToken());this.http.get(this.userinfoEndpoint,{headers:o,observe:"response",responseType:"text"}).subscribe(a=>{if(this.debug("userinfo received",JSON.stringify(a)),a.headers.get("content-type").startsWith("application/json")){let s=JSON.parse(a.body),l=this.getIdentityClaims()||{};if(!this.skipSubjectCheck&&this.oidc&&(!l.sub||s.sub!==l.sub)){r(`if property oidc is true, the received user-id (sub) has to be the user-id of the user that has logged in with oidc. +if you are not using oidc but just oauth2 password flow set oidc to false`);return}s=Object.assign({},l,s),this._storage.setItem("id_token_claims_obj",JSON.stringify(s)),this.eventsSubject.next(new $r("user_profile_loaded")),i({info:s})}else this.debug("userinfo is not JSON, treating it as JWE/JWS"),this.eventsSubject.next(new $r("user_profile_loaded")),i(JSON.parse(a.body))},a=>{this.logger.error("error loading user info",a),this.eventsSubject.next(new Ki("user_profile_load_error",a)),r(a)})})}fetchTokenUsingPasswordFlow(i,r,o=new Sr){let a={username:i,password:r};return this.fetchTokenUsingGrant("password",a,o)}fetchTokenUsingGrant(i,r,o=new Sr){this.assertUrlNotNullAndCorrectProtocol(this.tokenEndpoint,"tokenEndpoint");let a=new rr({encoder:new Zd}).set("grant_type",i).set("scope",this.scope);if(this.useHttpBasicAuth){let s=btoa(`${this.clientId}:${this.dummyClientSecret}`);o=o.set("Authorization","Basic "+s)}if(this.useHttpBasicAuth||(a=a.set("client_id",this.clientId)),!this.useHttpBasicAuth&&this.dummyClientSecret&&(a=a.set("client_secret",this.dummyClientSecret)),this.customQueryParams)for(let s of Object.getOwnPropertyNames(this.customQueryParams))a=a.set(s,this.customQueryParams[s]);for(let s of Object.keys(r))a=a.set(s,r[s]);return o=o.set("Content-Type","application/x-www-form-urlencoded"),new Promise((s,l)=>{this.http.post(this.tokenEndpoint,a,{headers:o}).subscribe(c=>{this.debug("tokenResponse",c),this.storeAccessTokenResponse(c.access_token,c.refresh_token,c.expires_in||this.fallbackAccessTokenExpirationTimeInSec,c.scope,this.extractRecognizedCustomParameters(c)),this.oidc&&c.id_token&&this.processIdToken(c.id_token,c.access_token).then(d=>{this.storeIdToken(d),s(c)}),this.eventsSubject.next(new $r("token_received")),s(c)},c=>{this.logger.error("Error performing ${grantType} flow",c),this.eventsSubject.next(new Ki("token_error",c)),l(c)})})}refreshToken(){return this.assertUrlNotNullAndCorrectProtocol(this.tokenEndpoint,"tokenEndpoint"),new Promise((i,r)=>{let o=new rr({encoder:new Zd}).set("grant_type","refresh_token").set("scope",this.scope).set("refresh_token",this._storage.getItem("refresh_token")),a=new Sr().set("Content-Type","application/x-www-form-urlencoded");if(this.useHttpBasicAuth){let s=btoa(`${this.clientId}:${this.dummyClientSecret}`);a=a.set("Authorization","Basic "+s)}if(this.useHttpBasicAuth||(o=o.set("client_id",this.clientId)),!this.useHttpBasicAuth&&this.dummyClientSecret&&(o=o.set("client_secret",this.dummyClientSecret)),this.customQueryParams)for(let s of Object.getOwnPropertyNames(this.customQueryParams))o=o.set(s,this.customQueryParams[s]);this.http.post(this.tokenEndpoint,o,{headers:a}).pipe(je(s=>this.oidc&&s.id_token?Ut(this.processIdToken(s.id_token,s.access_token,!0)).pipe(He(l=>this.storeIdToken(l)),se(()=>s)):Q(s))).subscribe(s=>{this.debug("refresh tokenResponse",s),this.storeAccessTokenResponse(s.access_token,s.refresh_token,s.expires_in||this.fallbackAccessTokenExpirationTimeInSec,s.scope,this.extractRecognizedCustomParameters(s)),this.eventsSubject.next(new $r("token_received")),this.eventsSubject.next(new $r("token_refreshed")),i(s)},s=>{this.logger.error("Error refreshing token",s),this.eventsSubject.next(new Ki("token_refresh_error",s)),r(s)})})}removeSilentRefreshEventListener(){this.silentRefreshPostMessageEventListener&&(window.removeEventListener("message",this.silentRefreshPostMessageEventListener),this.silentRefreshPostMessageEventListener=null)}setupSilentRefreshEventListener(){this.removeSilentRefreshEventListener(),this.silentRefreshPostMessageEventListener=i=>{let r=this.processMessageEventMessage(i);this.checkOrigin&&i.origin!==location.origin&&console.error("wrong origin requested silent refresh!"),this.tryLogin({customHashFragment:r,preventClearHashAfterLogin:!0,customRedirectUri:this.silentRefreshRedirectUri||this.redirectUri}).catch(o=>this.debug("tryLogin during silent refresh failed",o))},window.addEventListener("message",this.silentRefreshPostMessageEventListener)}silentRefresh(i={},r=!0){let o=this.getIdentityClaims()||{};if(this.useIdTokenHintForSilentRefresh&&this.hasValidIdToken()&&(i.id_token_hint=this.getIdToken()),!this.validateUrlForHttps(this.loginUrl))throw new Error("loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).");if(typeof this.document>"u")throw new Error("silent refresh is not supported on this platform");let a=this.document.getElementById(this.silentRefreshIFrameName);a&&this.document.body.removeChild(a),this.silentRefreshSubject=o.sub;let s=this.document.createElement("iframe");s.id=this.silentRefreshIFrameName,this.setupSilentRefreshEventListener();let l=this.silentRefreshRedirectUri||this.redirectUri;this.createLoginUrl(null,null,l,r,i).then(_=>{s.setAttribute("src",_),this.silentRefreshShowIFrame||(s.style.display="none"),this.document.body.appendChild(s)});let c=this.events.pipe(ce(_=>_ instanceof Ki),xn()),d=this.events.pipe(ce(_=>_.type==="token_received"),xn()),p=Q(new Ki("silent_refresh_timeout",null)).pipe(Oa(this.silentRefreshTimeout));return Ku([c,d,p]).pipe(se(_=>{if(_ instanceof Ki)throw _.type==="silent_refresh_timeout"?this.eventsSubject.next(_):(_=new Ki("silent_refresh_error",_),this.eventsSubject.next(_)),_;return _.type==="token_received"&&(_=new $r("silently_refreshed"),this.eventsSubject.next(_)),_})).toPromise()}initImplicitFlowInPopup(i){return this.initLoginFlowInPopup(i)}initLoginFlowInPopup(i){return i=i||{},this.createLoginUrl(null,null,this.silentRefreshRedirectUri,!1,{display:"popup"}).then(r=>new Promise((o,a)=>{let l=null;i.windowRef?i.windowRef&&!i.windowRef.closed&&(l=i.windowRef,l.location.href=r):l=window.open(r,"ngx-oauth2-oidc-login",this.calculatePopupFeatures(i));let c,d=w=>{this.tryLogin({customHashFragment:w,preventClearHashAfterLogin:!0,customRedirectUri:this.silentRefreshRedirectUri}).then(()=>{_(),o(!0)},C=>{_(),a(C)})},p=()=>{(!l||l.closed)&&(_(),a(new Ki("popup_closed",{})))};l?c=window.setInterval(p,500):a(new Ki("popup_blocked",{}));let _=()=>{window.clearInterval(c),window.removeEventListener("storage",y),window.removeEventListener("message",b),l!==null&&l.close(),l=null},b=w=>{let C=this.processMessageEventMessage(w);C&&C!==null?(window.removeEventListener("storage",y),d(C)):console.log("false event firing")},y=w=>{w.key==="auth_hash"&&(window.removeEventListener("message",b),d(w.newValue))};window.addEventListener("message",b),window.addEventListener("storage",y)}))}calculatePopupFeatures(i){let r=i.height||470,o=i.width||500,a=window.screenLeft+(window.outerWidth-o)/2,s=window.screenTop+(window.outerHeight-r)/2;return`location=no,toolbar=no,width=${o},height=${r},top=${s},left=${a}`}processMessageEventMessage(i){let r="#";if(this.silentRefreshMessagePrefix&&(r+=this.silentRefreshMessagePrefix),!i||!i.data||typeof i.data!="string")return;let o=i.data;if(o.startsWith(r))return"#"+o.substr(r.length)}canPerformSessionCheck(){return this.sessionChecksEnabled?this.sessionCheckIFrameUrl?this.getSessionState()?!(typeof this.document>"u"):(console.warn("sessionChecksEnabled is activated but there is no session_state"),!1):(console.warn("sessionChecksEnabled is activated but there is no sessionCheckIFrameUrl"),!1):!1}setupSessionCheckEventListener(){this.removeSessionCheckEventListener(),this.sessionCheckEventListener=i=>{let r=i.origin.toLowerCase(),o=this.issuer.toLowerCase();if(this.debug("sessionCheckEventListener"),!o.startsWith(r)){this.debug("sessionCheckEventListener","wrong origin",r,"expected",o,"event",i);return}switch(i.data){case"unchanged":this.ngZone.run(()=>{this.handleSessionUnchanged()});break;case"changed":this.ngZone.run(()=>{this.handleSessionChange()});break;case"error":this.ngZone.run(()=>{this.handleSessionError()});break}this.debug("got info from session check inframe",i)},this.ngZone.runOutsideAngular(()=>{window.addEventListener("message",this.sessionCheckEventListener)})}handleSessionUnchanged(){this.debug("session check","session unchanged"),this.eventsSubject.next(new qa("session_unchanged"))}handleSessionChange(){this.eventsSubject.next(new qa("session_changed")),this.stopSessionCheckTimer(),!this.useSilentRefresh&&this.responseType==="code"?this.refreshToken().then(()=>{this.debug("token refresh after session change worked")}).catch(()=>{this.debug("token refresh did not work after session changed"),this.eventsSubject.next(new qa("session_terminated")),this.logOut(!0)}):this.silentRefreshRedirectUri?(this.silentRefresh().catch(()=>this.debug("silent refresh failed after session changed")),this.waitForSilentRefreshAfterSessionChange()):(this.eventsSubject.next(new qa("session_terminated")),this.logOut(!0))}waitForSilentRefreshAfterSessionChange(){this.events.pipe(ce(i=>i.type==="silently_refreshed"||i.type==="silent_refresh_timeout"||i.type==="silent_refresh_error"),xn()).subscribe(i=>{i.type!=="silently_refreshed"&&(this.debug("silent refresh did not work after session changed"),this.eventsSubject.next(new qa("session_terminated")),this.logOut(!0))})}handleSessionError(){this.stopSessionCheckTimer(),this.eventsSubject.next(new qa("session_error"))}removeSessionCheckEventListener(){this.sessionCheckEventListener&&(window.removeEventListener("message",this.sessionCheckEventListener),this.sessionCheckEventListener=null)}initSessionCheck(){if(!this.canPerformSessionCheck())return;let i=this.document.getElementById(this.sessionCheckIFrameName);i&&this.document.body.removeChild(i);let r=this.document.createElement("iframe");r.id=this.sessionCheckIFrameName,this.setupSessionCheckEventListener();let o=this.sessionCheckIFrameUrl;r.setAttribute("src",o),r.style.display="none",this.document.body.appendChild(r),this.startSessionCheckTimer()}startSessionCheckTimer(){this.stopSessionCheckTimer(),this.ngZone.runOutsideAngular(()=>{this.sessionCheckTimer=setInterval(this.checkSession.bind(this),this.sessionCheckIntervall)})}stopSessionCheckTimer(){this.sessionCheckTimer&&(clearInterval(this.sessionCheckTimer),this.sessionCheckTimer=null)}checkSession(){let i=this.document.getElementById(this.sessionCheckIFrameName);i||this.logger.warn("checkSession did not find iframe",this.sessionCheckIFrameName);let r=this.getSessionState();r||this.stopSessionCheckTimer();let o=this.clientId+" "+r;i.contentWindow.postMessage(o,this.issuer)}createLoginUrl(){return yn(this,arguments,function*(i="",r="",o="",a=!1,s={}){let l=this,c;o?c=o:c=this.redirectUri;let d=yield this.createAndSaveNonce();if(i?i=d+this.config.nonceStateSeparator+encodeURIComponent(i):i=d,!this.requestAccessToken&&!this.oidc)throw new Error("Either requestAccessToken or oidc or both must be true");this.config.responseType?this.responseType=this.config.responseType:this.oidc&&this.requestAccessToken?this.responseType="id_token token":this.oidc&&!this.requestAccessToken?this.responseType="id_token":this.responseType="token";let p=l.loginUrl.indexOf("?")>-1?"&":"?",_=l.scope;this.oidc&&!_.match(/(^|\s)openid($|\s)/)&&(_="openid "+_);let b=l.loginUrl+p+"response_type="+encodeURIComponent(l.responseType)+"&client_id="+encodeURIComponent(l.clientId)+"&state="+encodeURIComponent(i)+"&redirect_uri="+encodeURIComponent(c)+"&scope="+encodeURIComponent(_);if(this.responseType.includes("code")&&!this.disablePKCE){let[y,w]=yield this.createChallangeVerifierPairForPKCE();this.saveNoncesInLocalStorage&&typeof window.localStorage<"u"?localStorage.setItem("PKCE_verifier",w):this._storage.setItem("PKCE_verifier",w),b+="&code_challenge="+y,b+="&code_challenge_method=S256"}r&&(b+="&login_hint="+encodeURIComponent(r)),l.resource&&(b+="&resource="+encodeURIComponent(l.resource)),l.oidc&&(b+="&nonce="+encodeURIComponent(d)),a&&(b+="&prompt=none");for(let y of Object.keys(s))b+="&"+encodeURIComponent(y)+"="+encodeURIComponent(s[y]);if(this.customQueryParams)for(let y of Object.getOwnPropertyNames(this.customQueryParams))b+="&"+y+"="+encodeURIComponent(this.customQueryParams[y]);return b})}initImplicitFlowInternal(i="",r=""){if(this.inImplicitFlow)return;if(this.inImplicitFlow=!0,!this.validateUrlForHttps(this.loginUrl))throw new Error("loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).");let o={},a=null;typeof r=="string"?a=r:typeof r=="object"&&(o=r),this.createLoginUrl(i,a,null,!1,o).then(this.config.openUri).catch(s=>{console.error("Error in initImplicitFlow",s),this.inImplicitFlow=!1})}initImplicitFlow(i="",r=""){this.loginUrl!==""?this.initImplicitFlowInternal(i,r):this.events.pipe(ce(o=>o.type==="discovery_document_loaded")).subscribe(()=>this.initImplicitFlowInternal(i,r))}resetImplicitFlow(){this.inImplicitFlow=!1}callOnTokenReceivedIfExists(i){let r=this;if(i.onTokenReceived){let o={idClaims:r.getIdentityClaims(),idToken:r.getIdToken(),accessToken:r.getAccessToken(),state:r.state};i.onTokenReceived(o)}}storeAccessTokenResponse(i,r,o,a,s){if(this._storage.setItem("access_token",i),a&&!Array.isArray(a)?this._storage.setItem("granted_scopes",JSON.stringify(a.split(" "))):a&&Array.isArray(a)&&this._storage.setItem("granted_scopes",JSON.stringify(a)),this._storage.setItem("access_token_stored_at",""+this.dateTimeService.now()),o){let l=o*1e3,d=this.dateTimeService.new().getTime()+l;this._storage.setItem("expires_at",""+d)}r&&this._storage.setItem("refresh_token",r),s&&s.forEach((l,c)=>{this._storage.setItem(c,l)})}tryLogin(i=null){return this.config.responseType==="code"?this.tryLoginCodeFlow(i).then(()=>!0):this.tryLoginImplicitFlow(i)}parseQueryString(i){return!i||i.length===0?{}:(i.charAt(0)==="?"&&(i=i.substr(1)),this.urlHelper.parseQueryString(i))}tryLoginCodeFlow(i=null){return yn(this,null,function*(){i=i||{};let r=i.customHashFragment?i.customHashFragment.substring(1):window.location.search,o=this.getCodePartsFromUrl(r),a=o.code,s=o.state,l=o.session_state;if(!i.preventClearHashAfterLogin){let p=location.origin+location.pathname+location.search.replace(/code=[^&$]*/,"").replace(/scope=[^&$]*/,"").replace(/state=[^&$]*/,"").replace(/session_state=[^&$]*/,"").replace(/^\?&/,"?").replace(/&$/,"").replace(/^\?$/,"").replace(/&+/g,"&").replace(/\?&/,"?").replace(/\?$/,"")+location.hash;history.replaceState(null,window.name,p)}let[c,d]=this.parseState(s);if(this.state=d,o.error){this.debug("error trying to login"),this.handleLoginError(i,o);let p=new Ki("code_error",{},o);return this.eventsSubject.next(p),Promise.reject(p)}if(!i.disableNonceCheck){if(!c)return this.saveRequestedRoute(),Promise.resolve();if(!i.disableOAuth2StateCheck&&!this.validateNonce(c)){let _=new Ki("invalid_nonce_in_state",null);return this.eventsSubject.next(_),Promise.reject(_)}}return this.storeSessionState(l),a&&(yield this.getTokenFromCode(a,i),this.restoreRequestedRoute()),Promise.resolve()})}saveRequestedRoute(){this.config.preserveRequestedRoute&&this._storage.setItem("requested_route",window.location.pathname+window.location.search)}restoreRequestedRoute(){let i=this._storage.getItem("requested_route");i&&history.replaceState(null,"",window.location.origin+i)}getCodePartsFromUrl(i){return!i||i.length===0?this.urlHelper.getHashFragmentParams():(i.charAt(0)==="?"&&(i=i.substr(1)),this.urlHelper.parseQueryString(i))}getTokenFromCode(i,r){let o=new rr({encoder:new Zd}).set("grant_type","authorization_code").set("code",i).set("redirect_uri",r.customRedirectUri||this.redirectUri);if(!this.disablePKCE){let a;this.saveNoncesInLocalStorage&&typeof window.localStorage<"u"?a=localStorage.getItem("PKCE_verifier"):a=this._storage.getItem("PKCE_verifier"),a?o=o.set("code_verifier",a):console.warn("No PKCE verifier found in oauth storage!")}return this.fetchAndProcessToken(o,r)}fetchAndProcessToken(i,r){r=r||{},this.assertUrlNotNullAndCorrectProtocol(this.tokenEndpoint,"tokenEndpoint");let o=new Sr().set("Content-Type","application/x-www-form-urlencoded");if(this.useHttpBasicAuth){let a=btoa(`${this.clientId}:${this.dummyClientSecret}`);o=o.set("Authorization","Basic "+a)}return this.useHttpBasicAuth||(i=i.set("client_id",this.clientId)),!this.useHttpBasicAuth&&this.dummyClientSecret&&(i=i.set("client_secret",this.dummyClientSecret)),new Promise((a,s)=>{if(this.customQueryParams)for(let l of Object.getOwnPropertyNames(this.customQueryParams))i=i.set(l,this.customQueryParams[l]);this.http.post(this.tokenEndpoint,i,{headers:o}).subscribe(l=>{this.debug("refresh tokenResponse",l),this.storeAccessTokenResponse(l.access_token,l.refresh_token,l.expires_in||this.fallbackAccessTokenExpirationTimeInSec,l.scope,this.extractRecognizedCustomParameters(l)),this.oidc&&l.id_token?this.processIdToken(l.id_token,l.access_token,r.disableNonceCheck).then(c=>{this.storeIdToken(c),this.eventsSubject.next(new $r("token_received")),this.eventsSubject.next(new $r("token_refreshed")),a(l)}).catch(c=>{this.eventsSubject.next(new Ki("token_validation_error",c)),console.error("Error validating tokens"),console.error(c),s(c)}):(this.eventsSubject.next(new $r("token_received")),this.eventsSubject.next(new $r("token_refreshed")),a(l))},l=>{console.error("Error getting token",l),this.eventsSubject.next(new Ki("token_error",l)),s(l)})})}tryLoginImplicitFlow(i=null){i=i||{};let r;i.customHashFragment?r=this.urlHelper.getHashFragmentParams(i.customHashFragment):r=this.urlHelper.getHashFragmentParams(),this.debug("parsed url",r);let o=r.state,[a,s]=this.parseState(o);if(this.state=s,r.error){this.debug("error trying to login"),this.handleLoginError(i,r);let _=new Ki("token_error",{},r);return this.eventsSubject.next(_),Promise.reject(_)}let l=r.access_token,c=r.id_token,d=r.session_state,p=r.scope;if(!this.requestAccessToken&&!this.oidc)return Promise.reject("Either requestAccessToken or oidc (or both) must be true.");if(this.requestAccessToken&&!l||this.requestAccessToken&&!i.disableOAuth2StateCheck&&!o||this.oidc&&!c)return Promise.resolve(!1);if(this.sessionChecksEnabled&&!d&&this.logger.warn("session checks (Session Status Change Notification) were activated in the configuration but the id_token does not contain a session_state claim"),this.requestAccessToken&&!i.disableNonceCheck&&!this.validateNonce(a)){let b=new Ki("invalid_nonce_in_state",null);return this.eventsSubject.next(b),Promise.reject(b)}return this.requestAccessToken&&this.storeAccessTokenResponse(l,null,r.expires_in||this.fallbackAccessTokenExpirationTimeInSec,p),this.oidc?this.processIdToken(c,l,i.disableNonceCheck).then(_=>i.validationHandler?i.validationHandler({accessToken:l,idClaims:_.idTokenClaims,idToken:_.idToken,state:o}).then(()=>_):_).then(_=>(this.storeIdToken(_),this.storeSessionState(d),this.clearHashAfterLogin&&!i.preventClearHashAfterLogin&&this.clearLocationHash(),this.eventsSubject.next(new $r("token_received")),this.callOnTokenReceivedIfExists(i),this.inImplicitFlow=!1,!0)).catch(_=>(this.eventsSubject.next(new Ki("token_validation_error",_)),this.logger.error("Error validating tokens"),this.logger.error(_),Promise.reject(_))):(this.eventsSubject.next(new $r("token_received")),this.clearHashAfterLogin&&!i.preventClearHashAfterLogin&&this.clearLocationHash(),this.callOnTokenReceivedIfExists(i),Promise.resolve(!0))}parseState(i){let r=i,o="";if(i){let a=i.indexOf(this.config.nonceStateSeparator);a>-1&&(r=i.substr(0,a),o=i.substr(a+this.config.nonceStateSeparator.length))}return[r,o]}validateNonce(i){let r;return this.saveNoncesInLocalStorage&&typeof window.localStorage<"u"?r=localStorage.getItem("nonce"):r=this._storage.getItem("nonce"),r!==i?(console.error("Validating access_token failed, wrong state/nonce.",r,i),!1):!0}storeIdToken(i){this._storage.setItem("id_token",i.idToken),this._storage.setItem("id_token_claims_obj",i.idTokenClaimsJson),this._storage.setItem("id_token_expires_at",""+i.idTokenExpiresAt),this._storage.setItem("id_token_stored_at",""+this.dateTimeService.now())}storeSessionState(i){this._storage.setItem("session_state",i)}getSessionState(){return this._storage.getItem("session_state")}handleLoginError(i,r){i.onLoginError&&i.onLoginError(r),this.clearHashAfterLogin&&!i.preventClearHashAfterLogin&&this.clearLocationHash()}getClockSkewInMsec(i=6e5){return!this.clockSkewInSec&&this.clockSkewInSec!==0?i:this.clockSkewInSec*1e3}processIdToken(i,r,o=!1){let a=i.split("."),s=this.padBase64(a[0]),l=bL(s),c=JSON.parse(l),d=this.padBase64(a[1]),p=bL(d),_=JSON.parse(p),b;if(this.saveNoncesInLocalStorage&&typeof window.localStorage<"u"?b=localStorage.getItem("nonce"):b=this._storage.getItem("nonce"),Array.isArray(_.aud)){if(_.aud.every(W=>W!==this.clientId)){let W="Wrong audience: "+_.aud.join(",");return this.logger.warn(W),Promise.reject(W)}}else if(_.aud!==this.clientId){let W="Wrong audience: "+_.aud;return this.logger.warn(W),Promise.reject(W)}if(!_.sub){let W="No sub claim in id_token";return this.logger.warn(W),Promise.reject(W)}if(this.sessionChecksEnabled&&this.silentRefreshSubject&&this.silentRefreshSubject!==_.sub){let W=`After refreshing, we got an id_token for another user (sub). Expected sub: ${this.silentRefreshSubject}, received sub: ${_.sub}`;return this.logger.warn(W),Promise.reject(W)}if(!_.iat){let W="No iat claim in id_token";return this.logger.warn(W),Promise.reject(W)}if(!this.skipIssuerCheck&&_.iss!==this.issuer){let W="Wrong issuer: "+_.iss;return this.logger.warn(W),Promise.reject(W)}if(!o&&_.nonce!==b){let W="Wrong nonce: "+_.nonce;return this.logger.warn(W),Promise.reject(W)}if(Object.prototype.hasOwnProperty.call(this,"responseType")&&(this.responseType==="code"||this.responseType==="id_token")&&(this.disableAtHashCheck=!0),!this.disableAtHashCheck&&this.requestAccessToken&&!_.at_hash){let W="An at_hash is needed!";return this.logger.warn(W),Promise.reject(W)}let y=this.dateTimeService.now(),w=_.iat*1e3,C=_.exp*1e3,D=this.getClockSkewInMsec();if(w-D>=y||C+D-this.decreaseExpirationBySec<=y){let W="Token has expired";return console.error(W),console.error({now:y,issuedAtMSec:w,expiresAtMSec:C}),Promise.reject(W)}let F={accessToken:r,idToken:i,jwks:this.jwks,idTokenClaims:_,idTokenHeader:c,loadKeys:()=>this.loadJwks()};return this.disableAtHashCheck?this.checkSignature(F).then(()=>({idToken:i,idTokenClaims:_,idTokenClaimsJson:p,idTokenHeader:c,idTokenHeaderJson:l,idTokenExpiresAt:C})):this.checkAtHash(F).then(W=>{if(!this.disableAtHashCheck&&this.requestAccessToken&&!W){let Z="Wrong at_hash";return this.logger.warn(Z),Promise.reject(Z)}return this.checkSignature(F).then(()=>{let Z=!this.disableAtHashCheck,K={idToken:i,idTokenClaims:_,idTokenClaimsJson:p,idTokenHeader:c,idTokenHeaderJson:l,idTokenExpiresAt:C};return Z?this.checkAtHash(F).then(oe=>{if(this.requestAccessToken&&!oe){let Se="Wrong at_hash";return this.logger.warn(Se),Promise.reject(Se)}else return K}):K})})}getIdentityClaims(){let i=this._storage.getItem("id_token_claims_obj");return i?JSON.parse(i):null}getGrantedScopes(){let i=this._storage.getItem("granted_scopes");return i?JSON.parse(i):null}getIdToken(){return this._storage?this._storage.getItem("id_token"):null}padBase64(i){for(;i.length%4!==0;)i+="=";return i}getAccessToken(){return this._storage?this._storage.getItem("access_token"):null}getRefreshToken(){return this._storage?this._storage.getItem("refresh_token"):null}getAccessTokenExpiration(){return this._storage.getItem("expires_at")?parseInt(this._storage.getItem("expires_at"),10):null}getAccessTokenStoredAt(){return parseInt(this._storage.getItem("access_token_stored_at"),10)}getIdTokenStoredAt(){return parseInt(this._storage.getItem("id_token_stored_at"),10)}getIdTokenExpiration(){return this._storage.getItem("id_token_expires_at")?parseInt(this._storage.getItem("id_token_expires_at"),10):null}hasValidAccessToken(){if(this.getAccessToken()){let i=this._storage.getItem("expires_at"),r=this.dateTimeService.new();return!(i&&parseInt(i,10)-this.decreaseExpirationBySec=0&&this._storage.getItem(i)!==null?JSON.parse(this._storage.getItem(i)):null}authorizationHeader(){return"Bearer "+this.getAccessToken()}logOut(i={},r=""){let o=!1;typeof i=="boolean"&&(o=i,i={});let a=this.getIdToken();if(this._storage.removeItem("access_token"),this._storage.removeItem("id_token"),this._storage.removeItem("refresh_token"),this.saveNoncesInLocalStorage?(localStorage.removeItem("nonce"),localStorage.removeItem("PKCE_verifier")):(this._storage.removeItem("nonce"),this._storage.removeItem("PKCE_verifier")),this._storage.removeItem("expires_at"),this._storage.removeItem("id_token_claims_obj"),this._storage.removeItem("id_token_expires_at"),this._storage.removeItem("id_token_stored_at"),this._storage.removeItem("access_token_stored_at"),this._storage.removeItem("granted_scopes"),this._storage.removeItem("session_state"),this.config.customTokenParameters&&this.config.customTokenParameters.forEach(l=>this._storage.removeItem(l)),this.silentRefreshSubject=null,this.eventsSubject.next(new qa("logout")),!this.logoutUrl||o)return;let s;if(!this.validateUrlForHttps(this.logoutUrl))throw new Error("logoutUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).");if(this.logoutUrl.indexOf("{{")>-1)s=this.logoutUrl.replace(/\{\{id_token\}\}/,encodeURIComponent(a)).replace(/\{\{client_id\}\}/,encodeURIComponent(this.clientId));else{let l=new rr({encoder:new Zd});a&&(l=l.set("id_token_hint",a));let c=this.postLogoutRedirectUri||this.redirectUriAsPostLogoutRedirectUriFallback&&this.redirectUri||"";c&&(l=l.set("post_logout_redirect_uri",c),r&&(l=l.set("state",r)));for(let d in i)l=l.set(d,i[d]);s=this.logoutUrl+(this.logoutUrl.indexOf("?")>-1?"&":"?")+l.toString()}this.config.openUri(s)}createAndSaveNonce(){let i=this;return this.createNonce().then(function(r){return i.saveNoncesInLocalStorage&&typeof window.localStorage<"u"?localStorage.setItem("nonce",r):i._storage.setItem("nonce",r),r})}ngOnDestroy(){this.clearAccessTokenTimer(),this.clearIdTokenTimer(),this.removeSilentRefreshEventListener();let i=this.document.getElementById(this.silentRefreshIFrameName);i&&i.remove(),this.stopSessionCheckTimer(),this.removeSessionCheckEventListener();let r=this.document.getElementById(this.sessionCheckIFrameName);r&&r.remove()}createNonce(){return new Promise(i=>{if(this.rngUrl)throw new Error("createNonce with rng-web-api has not been implemented so far");let r="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~",o=45,a="",s=typeof self>"u"?null:self.crypto||self.msCrypto;if(s){let l=new Uint8Array(o);s.getRandomValues(l),l.map||(l.map=Array.prototype.map),l=l.map(c=>r.charCodeAt(c%r.length)),a=String.fromCharCode.apply(null,l)}else for(;0o.type==="discovery_document_loaded")).subscribe(()=>this.initCodeFlowInternal(i,r))}initCodeFlowInternal(i="",r={}){if(!this.validateUrlForHttps(this.loginUrl))throw new Error("loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).");let o={},a=null;typeof r=="string"?a=r:typeof r=="object"&&(o=r),this.createLoginUrl(i,a,null,!1,o).then(this.config.openUri).catch(s=>{console.error("Error in initAuthorizationCodeFlow"),console.error(s)})}createChallangeVerifierPairForPKCE(){return yn(this,null,function*(){if(!this.crypto)throw new Error("PKCE support for code flow needs a CryptoHander. Did you import the OAuthModule using forRoot() ?");let i=yield this.createNonce(),r=yield this.crypto.calcHash(i,"sha-256");return[vL(r),i]})}extractRecognizedCustomParameters(i){let r=new Map;return this.config.customTokenParameters&&this.config.customTokenParameters.forEach(o=>{i[o]&&r.set(o,JSON.stringify(i[o]))}),r}revokeTokenAndLogout(i={},r=!1){let o=this.revocationEndpoint,a=this.getAccessToken(),s=this.getRefreshToken();if(!a)return Promise.resolve();let l=new rr({encoder:new Zd}),c=new Sr().set("Content-Type","application/x-www-form-urlencoded");if(this.useHttpBasicAuth){let d=btoa(`${this.clientId}:${this.dummyClientSecret}`);c=c.set("Authorization","Basic "+d)}if(this.useHttpBasicAuth||(l=l.set("client_id",this.clientId)),!this.useHttpBasicAuth&&this.dummyClientSecret&&(l=l.set("client_secret",this.dummyClientSecret)),this.customQueryParams)for(let d of Object.getOwnPropertyNames(this.customQueryParams))l=l.set(d,this.customQueryParams[d]);return new Promise((d,p)=>{let _,b;if(a){let y=l.set("token",a).set("token_type_hint","access_token");_=this.http.post(o,y,{headers:c})}else _=Q(null);if(s){let y=l.set("token",s).set("token_type_hint","refresh_token");b=this.http.post(o,y,{headers:c})}else b=Q(null);r&&(_=_.pipe(ei(y=>y.status===0?Q(null):er(y))),b=b.pipe(ei(y=>y.status===0?Q(null):er(y)))),yo([_,b]).subscribe(y=>{this.logOut(i),d(y),this.logger.info("Token successfully revoked")},y=>{this.logger.error("Error revoking token",y),this.eventsSubject.next(new Ki("token_revoke_error",y)),p(y)})})}clearLocationHash(){location.hash!=""&&(location.hash="")}};n.\u0275fac=function(r){return new(r||n)(pe(ae),pe(kr),pe(Nv,8),pe(Lv,8),pe(Qm,8),pe(yL),pe(Fv),pe(Vv,8),pe(_e),pe(Gf))},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),Bv=class{},vS=class{handleError(n){return er(n)}},ZK=(()=>{let n=class n{constructor(i,r,o){this.oAuthService=i,this.errorHandler=r,this.moduleConfig=o}checkUrl(i){return this.moduleConfig.resourceServer.customUrlValidation?this.moduleConfig.resourceServer.customUrlValidation(i):this.moduleConfig.resourceServer.allowedUrls?!!this.moduleConfig.resourceServer.allowedUrls.find(r=>i.toLowerCase().startsWith(r.toLowerCase())):!0}intercept(i,r){let o=i.url.toLowerCase();return!this.moduleConfig||!this.moduleConfig.resourceServer||!this.checkUrl(o)?r.handle(i):this.moduleConfig.resourceServer.sendAccessToken?it(Q(this.oAuthService.getAccessToken()).pipe(ce(s=>!!s)),this.oAuthService.events.pipe(ce(s=>s.type==="token_received"),Jw(this.oAuthService.waitForTokenInMsec||0),ei(()=>Q(null)),se(()=>this.oAuthService.getAccessToken()))).pipe(mt(1),Vt(s=>{if(s){let l="Bearer "+s,c=i.headers.set("Authorization",l);i=i.clone({headers:c})}return r.handle(i).pipe(ei(l=>this.errorHandler.handleError(l)))})):r.handle(i).pipe(ei(s=>this.errorHandler.handleError(s)))}};n.\u0275fac=function(r){return new(r||n)(pe(Km),pe(Bv),pe(Pv,8))},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})();function XK(){return console}function JK(){return typeof sessionStorage<"u"?sessionStorage:new $K}function CL(t=null,n=_S){return Jr([Km,yL,{provide:Fv,useFactory:XK},{provide:Nv,useFactory:JK},{provide:Lv,useClass:n},{provide:Vv,useClass:KK},{provide:Bv,useClass:vS},{provide:Pv,useValue:t},{provide:OE,useClass:ZK,multi:!0},{provide:Gf,useClass:UK}])}var eZ=["mat-internal-form-field",""],tZ=["*"],Zm=(()=>{class t{labelPosition;static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["div","mat-internal-form-field",""]],hostAttrs:[1,"mdc-form-field","mat-internal-form-field"],hostVars:2,hostBindings:function(i,r){i&2&&G("mdc-form-field--align-end",r.labelPosition==="before")},inputs:{labelPosition:"labelPosition"},attrs:eZ,ngContentSelectors:tZ,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),ne(0))},styles:[`.mat-internal-form-field{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-flex;align-items:center;vertical-align:middle}.mat-internal-form-field>label{margin-left:0;margin-right:auto;padding-left:4px;padding-right:0;order:0}[dir=rtl] .mat-internal-form-field>label{margin-left:auto;margin-right:0;padding-left:0;padding-right:4px}.mdc-form-field--align-end>label{margin-left:auto;margin-right:0;padding-left:0;padding-right:4px;order:-1}[dir=rtl] .mdc-form-field--align-end .mdc-form-field--align-end label{margin-left:0;margin-right:auto;padding-left:4px;padding-right:0} +`],encapsulation:2,changeDetection:0})}return t})();var yS;try{yS=typeof Intl<"u"&&Intl.v8BreakIterator}catch{yS=!1}var Ye=(()=>{class t{_platformId=u(hl);isBrowser=this._platformId?Q2(this._platformId):typeof document=="object"&&!!document;EDGE=this.isBrowser&&/(edge)/i.test(navigator.userAgent);TRIDENT=this.isBrowser&&/(msie|trident)/i.test(navigator.userAgent);BLINK=this.isBrowser&&!!(window.chrome||yS)&&typeof CSS<"u"&&!this.EDGE&&!this.TRIDENT;WEBKIT=this.isBrowser&&/AppleWebKit/i.test(navigator.userAgent)&&!this.BLINK&&!this.EDGE&&!this.TRIDENT;IOS=this.isBrowser&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!("MSStream"in window);FIREFOX=this.isBrowser&&/(firefox|minefield)/i.test(navigator.userAgent);ANDROID=this.isBrowser&&/android/i.test(navigator.userAgent)&&!this.TRIDENT;SAFARI=this.isBrowser&&/safari/i.test(navigator.userAgent)&&this.WEBKIT;constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function Dl(t){return Array.isArray(t)?t:[t]}var wL=new Set,Xd,Xm=(()=>{class t{_platform=u(Ye);_nonce=u(xm,{optional:!0});_matchMedia;constructor(){this._matchMedia=this._platform.isBrowser&&window.matchMedia?window.matchMedia.bind(window):nZ}matchMedia(e){return(this._platform.WEBKIT||this._platform.BLINK)&&iZ(e,this._nonce),this._matchMedia(e)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function iZ(t,n){if(!wL.has(t))try{Xd||(Xd=document.createElement("style"),n&&Xd.setAttribute("nonce",n),Xd.setAttribute("type","text/css"),document.head.appendChild(Xd)),Xd.sheet&&(Xd.sheet.insertRule(`@media ${t} {body{ }}`,0),wL.add(t))}catch(e){console.error(e)}}function nZ(t){return{matches:t==="all"||t==="",media:t,addListener:()=>{},removeListener:()=>{}}}var Ml=(()=>{class t{_mediaMatcher=u(Xm);_zone=u(ae);_queries=new Map;_destroySubject=new z;constructor(){}ngOnDestroy(){this._destroySubject.next(),this._destroySubject.complete()}isMatched(e){return DL(Dl(e)).some(r=>this._registerQuery(r).mql.matches)}observe(e){let r=DL(Dl(e)).map(a=>this._registerQuery(a).observable),o=yo(r);return o=Co(o.pipe(mt(1)),o.pipe(us(1),Dt(0))),o.pipe(se(a=>{let s={matches:!1,breakpoints:{}};return a.forEach(({matches:l,query:c})=>{s.matches=s.matches||l,s.breakpoints[c]=l}),s}))}_registerQuery(e){if(this._queries.has(e))return this._queries.get(e);let i=this._mediaMatcher.matchMedia(e),o={observable:new Ne(a=>{let s=l=>this._zone.run(()=>a.next(l));return i.addListener(s),()=>{i.removeListener(s)}}).pipe(Ue(i),se(({matches:a})=>({query:e,matches:a})),xe(this._destroySubject)),mql:i};return this._queries.set(e,o),o}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function DL(t){return t.map(n=>n.split(",")).reduce((n,e)=>n.concat(e)).map(n=>n.trim())}var jv={XSmall:"(max-width: 599.98px)",Small:"(min-width: 600px) and (max-width: 959.98px)",Medium:"(min-width: 960px) and (max-width: 1279.98px)",Large:"(min-width: 1280px) and (max-width: 1919.98px)",XLarge:"(min-width: 1920px)",Handset:"(max-width: 599.98px) and (orientation: portrait), (max-width: 959.98px) and (orientation: landscape)",Tablet:"(min-width: 600px) and (max-width: 839.98px) and (orientation: portrait), (min-width: 960px) and (max-width: 1279.98px) and (orientation: landscape)",Web:"(min-width: 840px) and (orientation: portrait), (min-width: 1280px) and (orientation: landscape)",HandsetPortrait:"(max-width: 599.98px) and (orientation: portrait)",TabletPortrait:"(min-width: 600px) and (max-width: 839.98px) and (orientation: portrait)",WebPortrait:"(min-width: 840px) and (orientation: portrait)",HandsetLandscape:"(max-width: 959.98px) and (orientation: landscape)",TabletLandscape:"(min-width: 960px) and (max-width: 1279.98px) and (orientation: landscape)",WebLandscape:"(min-width: 1280px) and (orientation: landscape)"};var rZ=new O("MATERIAL_ANIMATIONS");var ML=null;function xS(){return u(rZ,{optional:!0})?.animationsDisabled||u(ef,{optional:!0})==="NoopAnimations"?"di-disabled":(ML??=u(Xm).matchMedia("(prefers-reduced-motion)").matches,ML?"reduced-motion":"enabled")}function Qe(){return xS()!=="enabled"}function Jd(t){return t.buttons===0||t.detail===0}function eu(t){let n=t.touches&&t.touches[0]||t.changedTouches&&t.changedTouches[0];return!!n&&n.identifier===-1&&(n.radiusX==null||n.radiusX===1)&&(n.radiusY==null||n.radiusY===1)}var CS;function EL(){if(CS==null){let t=typeof document<"u"?document.head:null;CS=!!(t&&(t.createShadowRoot||t.attachShadow))}return CS}function wS(t){if(EL()){let n=t.getRootNode?t.getRootNode():null;if(typeof ShadowRoot<"u"&&ShadowRoot&&n instanceof ShadowRoot)return n}return null}function So(){let t=typeof document<"u"&&document?document.activeElement:null;for(;t&&t.shadowRoot;){let n=t.shadowRoot.activeElement;if(n===t)break;t=n}return t}function or(t){return t.composedPath?t.composedPath()[0]:t.target}var Yf;function SL(){if(Yf==null&&typeof window<"u")try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:()=>Yf=!0}))}finally{Yf=Yf||!1}return Yf}function Cc(t){return SL()?t:!!t.capture}function Gn(t,n=0){return Hv(t)?Number(t):arguments.length===2?n:0}function Hv(t){return!isNaN(parseFloat(t))&&!isNaN(Number(t))}function Wr(t){return t instanceof Y?t.nativeElement:t}var kL=new O("cdk-input-modality-detector-options"),TL={ignoreKeys:[18,17,224,91,16]},IL=650,DS={passive:!0,capture:!0},AL=(()=>{class t{_platform=u(Ye);_listenerCleanups;modalityDetected;modalityChanged;get mostRecentModality(){return this._modality.value}_mostRecentTarget=null;_modality=new rt(null);_options;_lastTouchMs=0;_onKeydown=e=>{this._options?.ignoreKeys?.some(i=>i===e.keyCode)||(this._modality.next("keyboard"),this._mostRecentTarget=or(e))};_onMousedown=e=>{Date.now()-this._lastTouchMs{if(eu(e)){this._modality.next("keyboard");return}this._lastTouchMs=Date.now(),this._modality.next("touch"),this._mostRecentTarget=or(e)};constructor(){let e=u(ae),i=u(_e),r=u(kL,{optional:!0});if(this._options=I(I({},TL),r),this.modalityDetected=this._modality.pipe(us(1)),this.modalityChanged=this.modalityDetected.pipe(Nn()),this._platform.isBrowser){let o=u(hn).createRenderer(null,null);this._listenerCleanups=e.runOutsideAngular(()=>[o.listen(i,"keydown",this._onKeydown,DS),o.listen(i,"mousedown",this._onMousedown,DS),o.listen(i,"touchstart",this._onTouchstart,DS)])}}ngOnDestroy(){this._modality.complete(),this._listenerCleanups?.forEach(e=>e())}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Qf=(function(t){return t[t.IMMEDIATE=0]="IMMEDIATE",t[t.EVENTUAL=1]="EVENTUAL",t})(Qf||{}),OL=new O("cdk-focus-monitor-default-options"),zv=Cc({passive:!0,capture:!0}),oi=(()=>{class t{_ngZone=u(ae);_platform=u(Ye);_inputModalityDetector=u(AL);_origin=null;_lastFocusOrigin;_windowFocused=!1;_windowFocusTimeoutId;_originTimeoutId;_originFromTouchInteraction=!1;_elementInfo=new Map;_monitoredElementCount=0;_rootNodeFocusListenerCount=new Map;_detectionMode;_windowFocusListener=()=>{this._windowFocused=!0,this._windowFocusTimeoutId=setTimeout(()=>this._windowFocused=!1)};_document=u(_e);_stopInputModalityDetector=new z;constructor(){let e=u(OL,{optional:!0});this._detectionMode=e?.detectionMode||Qf.IMMEDIATE}_rootNodeFocusAndBlurListener=e=>{let i=or(e);for(let r=i;r;r=r.parentElement)e.type==="focus"?this._onFocus(e,r):this._onBlur(e,r)};monitor(e,i=!1){let r=Wr(e);if(!this._platform.isBrowser||r.nodeType!==1)return Q();let o=wS(r)||this._document,a=this._elementInfo.get(r);if(a)return i&&(a.checkChildren=!0),a.subject;let s={checkChildren:i,subject:new z,rootNode:o};return this._elementInfo.set(r,s),this._registerGlobalListeners(s),s.subject}stopMonitoring(e){let i=Wr(e),r=this._elementInfo.get(i);r&&(r.subject.complete(),this._setClasses(i),this._elementInfo.delete(i),this._removeGlobalListeners(r))}focusVia(e,i,r){let o=Wr(e),a=this._document.activeElement;o===a?this._getClosestElementsInfo(o).forEach(([s,l])=>this._originChanged(s,i,l)):(this._setOrigin(i),typeof o.focus=="function"&&o.focus(r))}ngOnDestroy(){this._elementInfo.forEach((e,i)=>this.stopMonitoring(i))}_getWindow(){return this._document.defaultView||window}_getFocusOrigin(e){return this._origin?this._originFromTouchInteraction?this._shouldBeAttributedToTouch(e)?"touch":"program":this._origin:this._windowFocused&&this._lastFocusOrigin?this._lastFocusOrigin:e&&this._isLastInteractionFromInputLabel(e)?"mouse":"program"}_shouldBeAttributedToTouch(e){return this._detectionMode===Qf.EVENTUAL||!!e?.contains(this._inputModalityDetector._mostRecentTarget)}_setClasses(e,i){e.classList.toggle("cdk-focused",!!i),e.classList.toggle("cdk-touch-focused",i==="touch"),e.classList.toggle("cdk-keyboard-focused",i==="keyboard"),e.classList.toggle("cdk-mouse-focused",i==="mouse"),e.classList.toggle("cdk-program-focused",i==="program")}_setOrigin(e,i=!1){this._ngZone.runOutsideAngular(()=>{if(this._origin=e,this._originFromTouchInteraction=e==="touch"&&i,this._detectionMode===Qf.IMMEDIATE){clearTimeout(this._originTimeoutId);let r=this._originFromTouchInteraction?IL:1;this._originTimeoutId=setTimeout(()=>this._origin=null,r)}})}_onFocus(e,i){let r=this._elementInfo.get(i),o=or(e);!r||!r.checkChildren&&i!==o||this._originChanged(i,this._getFocusOrigin(o),r)}_onBlur(e,i){let r=this._elementInfo.get(i);!r||r.checkChildren&&e.relatedTarget instanceof Node&&i.contains(e.relatedTarget)||(this._setClasses(i),this._emitOrigin(r,null))}_emitOrigin(e,i){e.subject.observers.length&&this._ngZone.run(()=>e.subject.next(i))}_registerGlobalListeners(e){if(!this._platform.isBrowser)return;let i=e.rootNode,r=this._rootNodeFocusListenerCount.get(i)||0;r||this._ngZone.runOutsideAngular(()=>{i.addEventListener("focus",this._rootNodeFocusAndBlurListener,zv),i.addEventListener("blur",this._rootNodeFocusAndBlurListener,zv)}),this._rootNodeFocusListenerCount.set(i,r+1),++this._monitoredElementCount===1&&(this._ngZone.runOutsideAngular(()=>{this._getWindow().addEventListener("focus",this._windowFocusListener)}),this._inputModalityDetector.modalityDetected.pipe(xe(this._stopInputModalityDetector)).subscribe(o=>{this._setOrigin(o,!0)}))}_removeGlobalListeners(e){let i=e.rootNode;if(this._rootNodeFocusListenerCount.has(i)){let r=this._rootNodeFocusListenerCount.get(i);r>1?this._rootNodeFocusListenerCount.set(i,r-1):(i.removeEventListener("focus",this._rootNodeFocusAndBlurListener,zv),i.removeEventListener("blur",this._rootNodeFocusAndBlurListener,zv),this._rootNodeFocusListenerCount.delete(i))}--this._monitoredElementCount||(this._getWindow().removeEventListener("focus",this._windowFocusListener),this._stopInputModalityDetector.next(),clearTimeout(this._windowFocusTimeoutId),clearTimeout(this._originTimeoutId))}_originChanged(e,i,r){this._setClasses(e,i),this._emitOrigin(r,i),this._lastFocusOrigin=i}_getClosestElementsInfo(e){let i=[];return this._elementInfo.forEach((r,o)=>{(o===e||r.checkChildren&&o.contains(e))&&i.push([o,r])}),i}_isLastInteractionFromInputLabel(e){let{_mostRecentTarget:i,mostRecentModality:r}=this._inputModalityDetector;if(r!=="mouse"||!i||i===e||e.nodeName!=="INPUT"&&e.nodeName!=="TEXTAREA"||e.disabled)return!1;let o=e.labels;if(o){for(let a=0;a{class t{_elementRef=u(Y);_focusMonitor=u(oi);_monitorSubscription;_focusOrigin=null;cdkFocusChange=new U;constructor(){}get focusOrigin(){return this._focusOrigin}ngAfterViewInit(){let e=this._elementRef.nativeElement;this._monitorSubscription=this._focusMonitor.monitor(e,e.nodeType===1&&e.hasAttribute("cdkMonitorSubtreeFocus")).subscribe(i=>{this._focusOrigin=i,this.cdkFocusChange.emit(i)})}ngOnDestroy(){this._focusMonitor.stopMonitoring(this._elementRef),this._monitorSubscription&&this._monitorSubscription.unsubscribe()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkMonitorElementFocus",""],["","cdkMonitorSubtreeFocus",""]],outputs:{cdkFocusChange:"cdkFocusChange"},exportAs:["cdkMonitorFocus"]})}return t})();var Uv=new WeakMap,ft=(()=>{class t{_appRef;_injector=u(de);_environmentInjector=u(ti);load(e){let i=this._appRef=this._appRef||this._injector.get(tr),r=Uv.get(i);r||(r={loaders:new Set,refs:[]},Uv.set(i,r),i.onDestroy(()=>{Uv.get(i)?.refs.forEach(o=>o.destroy()),Uv.delete(i)})),r.loaders.has(e)||(r.loaders.add(e),r.refs.push(Am(e,{environmentInjector:this._environmentInjector})))}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var ro=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["ng-component"]],exportAs:["cdkVisuallyHidden"],decls:0,vars:0,template:function(i,r){},styles:[`.cdk-visually-hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap;outline:0;-webkit-appearance:none;-moz-appearance:none;left:0}[dir=rtl] .cdk-visually-hidden{left:auto;right:0} +`],encapsulation:2,changeDetection:0})}return t})();function oZ(t){if(t.type==="characterData"&&t.target instanceof Comment)return!0;if(t.type==="childList"){for(let n=0;n{class t{create(e){return typeof MutationObserver>"u"?null:new MutationObserver(e)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),PL=(()=>{class t{_mutationObserverFactory=u(RL);_observedElements=new Map;_ngZone=u(ae);constructor(){}ngOnDestroy(){this._observedElements.forEach((e,i)=>this._cleanupObserver(i))}observe(e){let i=Wr(e);return new Ne(r=>{let a=this._observeElement(i).pipe(se(s=>s.filter(l=>!oZ(l))),ce(s=>!!s.length)).subscribe(s=>{this._ngZone.run(()=>{r.next(s)})});return()=>{a.unsubscribe(),this._unobserveElement(i)}})}_observeElement(e){return this._ngZone.runOutsideAngular(()=>{if(this._observedElements.has(e))this._observedElements.get(e).count++;else{let i=new z,r=this._mutationObserverFactory.create(o=>i.next(o));r&&r.observe(e,{characterData:!0,childList:!0,subtree:!0}),this._observedElements.set(e,{observer:r,stream:i,count:1})}return this._observedElements.get(e).stream})}_unobserveElement(e){this._observedElements.has(e)&&(this._observedElements.get(e).count--,this._observedElements.get(e).count||this._cleanupObserver(e))}_cleanupObserver(e){if(this._observedElements.has(e)){let{observer:i,stream:r}=this._observedElements.get(e);i&&i.disconnect(),r.complete(),this._observedElements.delete(e)}}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Zf=(()=>{class t{_contentObserver=u(PL);_elementRef=u(Y);event=new U;get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._disabled?this._unsubscribe():this._subscribe()}_disabled=!1;get debounce(){return this._debounce}set debounce(e){this._debounce=Gn(e),this._subscribe()}_debounce;_currentSubscription=null;constructor(){}ngAfterContentInit(){!this._currentSubscription&&!this.disabled&&this._subscribe()}ngOnDestroy(){this._unsubscribe()}_subscribe(){this._unsubscribe();let e=this._contentObserver.observe(this._elementRef);this._currentSubscription=(this.debounce?e.pipe(Dt(this.debounce)):e).subscribe(this.event)}_unsubscribe(){this._currentSubscription?.unsubscribe()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkObserveContent",""]],inputs:{disabled:[2,"cdkObserveContentDisabled","disabled",B],debounce:"debounce"},outputs:{event:"cdkObserveContent"},exportAs:["cdkObserveContent"]})}return t})(),Jm=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[RL]})}return t})();var Dc=(()=>{class t{_platform=u(Ye);constructor(){}isDisabled(e){return e.hasAttribute("disabled")}isVisible(e){return sZ(e)&&getComputedStyle(e).visibility==="visible"}isTabbable(e){if(!this._platform.isBrowser)return!1;let i=aZ(fZ(e));if(i&&(FL(i)===-1||!this.isVisible(i)))return!1;let r=e.nodeName.toLowerCase(),o=FL(e);return e.hasAttribute("contenteditable")?o!==-1:r==="iframe"||r==="object"||this._platform.WEBKIT&&this._platform.IOS&&!hZ(e)?!1:r==="audio"?e.hasAttribute("controls")?o!==-1:!1:r==="video"?o===-1?!1:o!==null?!0:this._platform.FIREFOX||e.hasAttribute("controls"):e.tabIndex>=0}isFocusable(e,i){return pZ(e)&&!this.isDisabled(e)&&(i?.ignoreVisibility||this.isVisible(e))}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function aZ(t){try{return t.frameElement}catch{return null}}function sZ(t){return!!(t.offsetWidth||t.offsetHeight||typeof t.getClientRects=="function"&&t.getClientRects().length)}function lZ(t){let n=t.nodeName.toLowerCase();return n==="input"||n==="select"||n==="button"||n==="textarea"}function cZ(t){return uZ(t)&&t.type=="hidden"}function dZ(t){return mZ(t)&&t.hasAttribute("href")}function uZ(t){return t.nodeName.toLowerCase()=="input"}function mZ(t){return t.nodeName.toLowerCase()=="a"}function VL(t){if(!t.hasAttribute("tabindex")||t.tabIndex===void 0)return!1;let n=t.getAttribute("tabindex");return!!(n&&!isNaN(parseInt(n,10)))}function FL(t){if(!VL(t))return null;let n=parseInt(t.getAttribute("tabindex")||"",10);return isNaN(n)?-1:n}function hZ(t){let n=t.nodeName.toLowerCase(),e=n==="input"&&t.type;return e==="text"||e==="password"||n==="select"||n==="textarea"}function pZ(t){return cZ(t)?!1:lZ(t)||dZ(t)||t.hasAttribute("contenteditable")||VL(t)}function fZ(t){return t.ownerDocument&&t.ownerDocument.defaultView||window}var $v=class{_element;_checker;_ngZone;_document;_injector;_startAnchor;_endAnchor;_hasAttached=!1;startAnchorListener=()=>this.focusLastTabbableElement();endAnchorListener=()=>this.focusFirstTabbableElement();get enabled(){return this._enabled}set enabled(n){this._enabled=n,this._startAnchor&&this._endAnchor&&(this._toggleAnchorTabIndex(n,this._startAnchor),this._toggleAnchorTabIndex(n,this._endAnchor))}_enabled=!0;constructor(n,e,i,r,o=!1,a){this._element=n,this._checker=e,this._ngZone=i,this._document=r,this._injector=a,o||this.attachAnchors()}destroy(){let n=this._startAnchor,e=this._endAnchor;n&&(n.removeEventListener("focus",this.startAnchorListener),n.remove()),e&&(e.removeEventListener("focus",this.endAnchorListener),e.remove()),this._startAnchor=this._endAnchor=null,this._hasAttached=!1}attachAnchors(){return this._hasAttached?!0:(this._ngZone.runOutsideAngular(()=>{this._startAnchor||(this._startAnchor=this._createAnchor(),this._startAnchor.addEventListener("focus",this.startAnchorListener)),this._endAnchor||(this._endAnchor=this._createAnchor(),this._endAnchor.addEventListener("focus",this.endAnchorListener))}),this._element.parentNode&&(this._element.parentNode.insertBefore(this._startAnchor,this._element),this._element.parentNode.insertBefore(this._endAnchor,this._element.nextSibling),this._hasAttached=!0),this._hasAttached)}focusInitialElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusInitialElement(n)))})}focusFirstTabbableElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusFirstTabbableElement(n)))})}focusLastTabbableElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusLastTabbableElement(n)))})}_getRegionBoundary(n){let e=this._element.querySelectorAll(`[cdk-focus-region-${n}], [cdkFocusRegion${n}], [cdk-focus-${n}]`);return n=="start"?e.length?e[0]:this._getFirstTabbableElement(this._element):e.length?e[e.length-1]:this._getLastTabbableElement(this._element)}focusInitialElement(n){let e=this._element.querySelector("[cdk-focus-initial], [cdkFocusInitial]");if(e){if(!this._checker.isFocusable(e)){let i=this._getFirstTabbableElement(e);return i?.focus(n),!!i}return e.focus(n),!0}return this.focusFirstTabbableElement(n)}focusFirstTabbableElement(n){let e=this._getRegionBoundary("start");return e&&e.focus(n),!!e}focusLastTabbableElement(n){let e=this._getRegionBoundary("end");return e&&e.focus(n),!!e}hasAttached(){return this._hasAttached}_getFirstTabbableElement(n){if(this._checker.isFocusable(n)&&this._checker.isTabbable(n))return n;let e=n.children;for(let i=0;i=0;i--){let r=e[i].nodeType===this._document.ELEMENT_NODE?this._getLastTabbableElement(e[i]):null;if(r)return r}return null}_createAnchor(){let n=this._document.createElement("div");return this._toggleAnchorTabIndex(this._enabled,n),n.classList.add("cdk-visually-hidden"),n.classList.add("cdk-focus-trap-anchor"),n.setAttribute("aria-hidden","true"),n}_toggleAnchorTabIndex(n,e){n?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")}toggleAnchors(n){this._startAnchor&&this._endAnchor&&(this._toggleAnchorTabIndex(n,this._startAnchor),this._toggleAnchorTabIndex(n,this._endAnchor))}_executeOnStable(n){this._injector?vt(n,{injector:this._injector}):setTimeout(n)}},eh=(()=>{class t{_checker=u(Dc);_ngZone=u(ae);_document=u(_e);_injector=u(de);constructor(){u(ft).load(ro)}create(e,i=!1){return new $v(e,this._checker,this._ngZone,this._document,i,this._injector)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),ES=(()=>{class t{_elementRef=u(Y);_focusTrapFactory=u(eh);focusTrap;_previouslyFocusedElement=null;get enabled(){return this.focusTrap?.enabled||!1}set enabled(e){this.focusTrap&&(this.focusTrap.enabled=e)}autoCapture;constructor(){u(Ye).isBrowser&&(this.focusTrap=this._focusTrapFactory.create(this._elementRef.nativeElement,!0))}ngOnDestroy(){this.focusTrap?.destroy(),this._previouslyFocusedElement&&(this._previouslyFocusedElement.focus(),this._previouslyFocusedElement=null)}ngAfterContentInit(){this.focusTrap?.attachAnchors(),this.autoCapture&&this._captureFocus()}ngDoCheck(){this.focusTrap&&!this.focusTrap.hasAttached()&&this.focusTrap.attachAnchors()}ngOnChanges(e){let i=e.autoCapture;i&&!i.firstChange&&this.autoCapture&&this.focusTrap?.hasAttached()&&this._captureFocus()}_captureFocus(){this._previouslyFocusedElement=So(),this.focusTrap?.focusInitialElementWhenReady()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkTrapFocus",""]],inputs:{enabled:[2,"cdkTrapFocus","enabled",B],autoCapture:[2,"cdkTrapFocusAutoCapture","autoCapture",B]},exportAs:["cdkTrapFocus"],features:[Oe]})}return t})(),BL=new O("liveAnnouncerElement",{providedIn:"root",factory:jL});function jL(){return null}var HL=new O("LIVE_ANNOUNCER_DEFAULT_OPTIONS"),gZ=0,Xf=(()=>{class t{_ngZone=u(ae);_defaultOptions=u(HL,{optional:!0});_liveElement;_document=u(_e);_previousTimeout;_currentPromise;_currentResolve;constructor(){let e=u(BL,{optional:!0});this._liveElement=e||this._createLiveElement()}announce(e,...i){let r=this._defaultOptions,o,a;return i.length===1&&typeof i[0]=="number"?a=i[0]:[o,a]=i,this.clear(),clearTimeout(this._previousTimeout),o||(o=r&&r.politeness?r.politeness:"polite"),a==null&&r&&(a=r.duration),this._liveElement.setAttribute("aria-live",o),this._liveElement.id&&this._exposeAnnouncerToModals(this._liveElement.id),this._ngZone.runOutsideAngular(()=>(this._currentPromise||(this._currentPromise=new Promise(s=>this._currentResolve=s)),clearTimeout(this._previousTimeout),this._previousTimeout=setTimeout(()=>{this._liveElement.textContent=e,typeof a=="number"&&(this._previousTimeout=setTimeout(()=>this.clear(),a)),this._currentResolve?.(),this._currentPromise=this._currentResolve=void 0},100),this._currentPromise))}clear(){this._liveElement&&(this._liveElement.textContent="")}ngOnDestroy(){clearTimeout(this._previousTimeout),this._liveElement?.remove(),this._liveElement=null,this._currentResolve?.(),this._currentPromise=this._currentResolve=void 0}_createLiveElement(){let e="cdk-live-announcer-element",i=this._document.getElementsByClassName(e),r=this._document.createElement("div");for(let o=0;o .cdk-overlay-container [aria-modal="true"]');for(let r=0;r{class t{_platform=u(Ye);_hasCheckedHighContrastMode;_document=u(_e);_breakpointSubscription;constructor(){this._breakpointSubscription=u(Ml).observe("(forced-colors: active)").subscribe(()=>{this._hasCheckedHighContrastMode&&(this._hasCheckedHighContrastMode=!1,this._applyBodyHighContrastModeCssClasses())})}getHighContrastMode(){if(!this._platform.isBrowser)return wc.NONE;let e=this._document.createElement("div");e.style.backgroundColor="rgb(1,2,3)",e.style.position="absolute",this._document.body.appendChild(e);let i=this._document.defaultView||window,r=i&&i.getComputedStyle?i.getComputedStyle(e):null,o=(r&&r.backgroundColor||"").replace(/ /g,"");switch(e.remove(),o){case"rgb(0,0,0)":case"rgb(45,50,54)":case"rgb(32,32,32)":return wc.WHITE_ON_BLACK;case"rgb(255,255,255)":case"rgb(255,250,239)":return wc.BLACK_ON_WHITE}return wc.NONE}ngOnDestroy(){this._breakpointSubscription.unsubscribe()}_applyBodyHighContrastModeCssClasses(){if(!this._hasCheckedHighContrastMode&&this._platform.isBrowser&&this._document.body){let e=this._document.body.classList;e.remove(MS,NL,LL),this._hasCheckedHighContrastMode=!0;let i=this.getHighContrastMode();i===wc.BLACK_ON_WHITE?e.add(MS,NL):i===wc.WHITE_ON_BLACK&&e.add(MS,LL)}}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),El=(()=>{class t{constructor(){u(Wv)._applyBodyHighContrastModeCssClasses()}static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[Jm]})}return t})();var SS={},et=(()=>{class t{_appId=u(uc);getId(e){return this._appId!=="ng"&&(e+=this._appId),SS.hasOwnProperty(e)||(SS[e]=0),`${e}${SS[e]++}`}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var _Z=200,Gv=class{_letterKeyStream=new z;_items=[];_selectedItemIndex=-1;_pressedLetters=[];_skipPredicateFn;_selectedItem=new z;selectedItem=this._selectedItem;constructor(n,e){let i=typeof e?.debounceInterval=="number"?e.debounceInterval:_Z;e?.skipPredicate&&(this._skipPredicateFn=e.skipPredicate),this.setItems(n),this._setupKeyHandler(i)}destroy(){this._pressedLetters=[],this._letterKeyStream.complete(),this._selectedItem.complete()}setCurrentSelectedItemIndex(n){this._selectedItemIndex=n}setItems(n){this._items=n}handleKey(n){let e=n.keyCode;n.key&&n.key.length===1?this._letterKeyStream.next(n.key.toLocaleUpperCase()):(e>=65&&e<=90||e>=48&&e<=57)&&this._letterKeyStream.next(String.fromCharCode(e))}isTyping(){return this._pressedLetters.length>0}reset(){this._pressedLetters=[]}_setupKeyHandler(n){this._letterKeyStream.pipe(He(e=>this._pressedLetters.push(e)),Dt(n),ce(()=>this._pressedLetters.length>0),se(()=>this._pressedLetters.join("").toLocaleUpperCase())).subscribe(e=>{for(let i=1;it[e]):t.altKey||t.shiftKey||t.ctrlKey||t.metaKey}var th=class{_items;_activeItemIndex=he(-1);_activeItem=he(null);_wrap=!1;_typeaheadSubscription=ke.EMPTY;_itemChangesSubscription;_vertical=!0;_horizontal;_allowedModifierKeys=[];_homeAndEnd=!1;_pageUpAndDown={enabled:!1,delta:10};_effectRef;_typeahead;_skipPredicateFn=n=>n.disabled;constructor(n,e){this._items=n,n instanceof Dr?this._itemChangesSubscription=n.changes.subscribe(i=>this._itemsChanged(i.toArray())):vs(n)&&(this._effectRef=zr(()=>this._itemsChanged(n()),{injector:e}))}tabOut=new z;change=new z;skipPredicate(n){return this._skipPredicateFn=n,this}withWrap(n=!0){return this._wrap=n,this}withVerticalOrientation(n=!0){return this._vertical=n,this}withHorizontalOrientation(n){return this._horizontal=n,this}withAllowedModifierKeys(n){return this._allowedModifierKeys=n,this}withTypeAhead(n=200){this._typeaheadSubscription.unsubscribe();let e=this._getItemsArray();return this._typeahead=new Gv(e,{debounceInterval:typeof n=="number"?n:void 0,skipPredicate:i=>this._skipPredicateFn(i)}),this._typeaheadSubscription=this._typeahead.selectedItem.subscribe(i=>{this.setActiveItem(i)}),this}cancelTypeahead(){return this._typeahead?.reset(),this}withHomeAndEnd(n=!0){return this._homeAndEnd=n,this}withPageUpDown(n=!0,e=10){return this._pageUpAndDown={enabled:n,delta:e},this}setActiveItem(n){let e=this._activeItem();this.updateActiveItem(n),this._activeItem()!==e&&this.change.next(this._activeItemIndex())}onKeydown(n){let e=n.keyCode,r=["altKey","ctrlKey","metaKey","shiftKey"].every(o=>!n[o]||this._allowedModifierKeys.indexOf(o)>-1);switch(e){case 9:this.tabOut.next();return;case 40:if(this._vertical&&r){this.setNextItemActive();break}else return;case 38:if(this._vertical&&r){this.setPreviousItemActive();break}else return;case 39:if(this._horizontal&&r){this._horizontal==="rtl"?this.setPreviousItemActive():this.setNextItemActive();break}else return;case 37:if(this._horizontal&&r){this._horizontal==="rtl"?this.setNextItemActive():this.setPreviousItemActive();break}else return;case 36:if(this._homeAndEnd&&r){this.setFirstItemActive();break}else return;case 35:if(this._homeAndEnd&&r){this.setLastItemActive();break}else return;case 33:if(this._pageUpAndDown.enabled&&r){let o=this._activeItemIndex()-this._pageUpAndDown.delta;this._setActiveItemByIndex(o>0?o:0,1);break}else return;case 34:if(this._pageUpAndDown.enabled&&r){let o=this._activeItemIndex()+this._pageUpAndDown.delta,a=this._getItemsArray().length;this._setActiveItemByIndex(o-1&&i!==this._activeItemIndex()&&(this._activeItemIndex.set(i),this._typeahead?.setCurrentSelectedItemIndex(i))}}};var tu=class extends th{setActiveItem(n){this.activeItem&&this.activeItem.setInactiveStyles(),super.setActiveItem(n),this.activeItem&&this.activeItem.setActiveStyles()}};var Fs=class extends th{_origin="program";setFocusOrigin(n){return this._origin=n,this}setActiveItem(n){super.setActiveItem(n),this.activeItem&&this.activeItem.focus(this._origin)}};var WL=" ";function ih(t,n,e){let i=Kv(t,n);e=e.trim(),!i.some(r=>r.trim()===e)&&(i.push(e),t.setAttribute(n,i.join(WL)))}function Mc(t,n,e){let i=Kv(t,n);e=e.trim();let r=i.filter(o=>o!==e);r.length?t.setAttribute(n,r.join(WL)):t.removeAttribute(n)}function Kv(t,n){return t.getAttribute(n)?.match(/\S+/g)??[]}var GL="cdk-describedby-message",Qv="cdk-describedby-host",TS=0,nh=(()=>{class t{_platform=u(Ye);_document=u(_e);_messageRegistry=new Map;_messagesContainer=null;_id=`${TS++}`;constructor(){u(ft).load(ro),this._id=u(uc)+"-"+TS++}describe(e,i,r){if(!this._canBeDescribed(e,i))return;let o=kS(i,r);typeof i!="string"?($L(i,this._id),this._messageRegistry.set(o,{messageElement:i,referenceCount:0})):this._messageRegistry.has(o)||this._createMessageElement(i,r),this._isElementDescribedByMessage(e,o)||this._addMessageReference(e,o)}removeDescription(e,i,r){if(!i||!this._isElementNode(e))return;let o=kS(i,r);if(this._isElementDescribedByMessage(e,o)&&this._removeMessageReference(e,o),typeof i=="string"){let a=this._messageRegistry.get(o);a&&a.referenceCount===0&&this._deleteMessageElement(o)}this._messagesContainer?.childNodes.length===0&&(this._messagesContainer.remove(),this._messagesContainer=null)}ngOnDestroy(){let e=this._document.querySelectorAll(`[${Qv}="${this._id}"]`);for(let i=0;ir.indexOf(GL)!=0);e.setAttribute("aria-describedby",i.join(" "))}_addMessageReference(e,i){let r=this._messageRegistry.get(i);ih(e,"aria-describedby",r.messageElement.id),e.setAttribute(Qv,this._id),r.referenceCount++}_removeMessageReference(e,i){let r=this._messageRegistry.get(i);r.referenceCount--,Mc(e,"aria-describedby",r.messageElement.id),e.removeAttribute(Qv)}_isElementDescribedByMessage(e,i){let r=Kv(e,"aria-describedby"),o=this._messageRegistry.get(i),a=o&&o.messageElement.id;return!!a&&r.indexOf(a)!=-1}_canBeDescribed(e,i){if(!this._isElementNode(e))return!1;if(i&&typeof i=="object")return!0;let r=i==null?"":`${i}`.trim(),o=e.getAttribute("aria-label");return r?!o||o.trim()!==r:!1}_isElementNode(e){return e.nodeType===this._document.ELEMENT_NODE}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function kS(t,n){return typeof t=="string"?`${n||""}/${t}`:t}function $L(t,n){t.id||(t.id=`${GL}-${n}-${TS++}`)}var bZ=new O("cdk-dir-doc",{providedIn:"root",factory:vZ});function vZ(){return u(_e)}var yZ=/^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Adlm|Arab|Hebr|Nkoo|Rohg|Thaa))(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)/i;function IS(t){let n=t?.toLowerCase()||"";return n==="auto"&&typeof navigator<"u"&&navigator?.language?yZ.test(navigator.language)?"rtl":"ltr":n==="rtl"?"rtl":"ltr"}var Yt=(()=>{class t{get value(){return this.valueSignal()}valueSignal=he("ltr");change=new U;constructor(){let e=u(bZ,{optional:!0});if(e){let i=e.body?e.body.dir:null,r=e.documentElement?e.documentElement.dir:null;this.valueSignal.set(IS(i||r||"ltr"))}}ngOnDestroy(){this.change.complete()}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var YL=(()=>{class t{_isInitialized=!1;_rawDir;change=new U;get dir(){return this.valueSignal()}set dir(e){let i=this.valueSignal();this.valueSignal.set(IS(e)),this._rawDir=e,i!==this.valueSignal()&&this._isInitialized&&this.change.emit(this.valueSignal())}get value(){return this.dir}valueSignal=he("ltr");ngAfterContentInit(){this._isInitialized=!0}ngOnDestroy(){this.change.complete()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","dir",""]],hostVars:1,hostBindings:function(i,r){i&2&&X("dir",r._rawDir)},inputs:{dir:"dir"},outputs:{change:"dirChange"},exportAs:["dir"],features:[we([{provide:Yt,useExisting:t}])]})}return t})(),Ns=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})();var De=(()=>{class t{constructor(){u(Wv)._applyBodyHighContrastModeCssClasses()}static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[Ns,Ns]})}return t})();var Sl=class{_defaultMatcher;ngControl;_parentFormGroup;_parentForm;_stateChanges;errorState=!1;matcher;constructor(n,e,i,r,o){this._defaultMatcher=n,this.ngControl=e,this._parentFormGroup=i,this._parentForm=r,this._stateChanges=o}updateErrorState(){let n=this.errorState,e=this._parentFormGroup||this._parentForm,i=this.matcher||this._defaultMatcher,r=this.ngControl?this.ngControl.control:null,o=i?.isErrorState(r,e)??!1;o!==n&&(this.errorState=o,this._stateChanges.next())}};var Ls=new O("MAT_DATE_LOCALE",{providedIn:"root",factory:CZ});function CZ(){return u(bl)}var rh="Method not implemented",$i=class{locale;_localeChanges=new z;localeChanges=this._localeChanges;setTime(n,e,i,r){throw new Error(rh)}getHours(n){throw new Error(rh)}getMinutes(n){throw new Error(rh)}getSeconds(n){throw new Error(rh)}parseTime(n,e){throw new Error(rh)}addSeconds(n,e){throw new Error(rh)}getValidDateOrNull(n){return this.isDateInstance(n)&&this.isValid(n)?n:null}deserialize(n){return n==null||this.isDateInstance(n)&&this.isValid(n)?n:this.invalid()}setLocale(n){this.locale=n,this._localeChanges.next()}compareDate(n,e){return this.getYear(n)-this.getYear(e)||this.getMonth(n)-this.getMonth(e)||this.getDate(n)-this.getDate(e)}compareTime(n,e){return this.getHours(n)-this.getHours(e)||this.getMinutes(n)-this.getMinutes(e)||this.getSeconds(n)-this.getSeconds(e)}sameDate(n,e){if(n&&e){let i=this.isValid(n),r=this.isValid(e);return i&&r?!this.compareDate(n,e):i==r}return n==e}sameTime(n,e){if(n&&e){let i=this.isValid(n),r=this.isValid(e);return i&&r?!this.compareTime(n,e):i==r}return n==e}clampDate(n,e,i){return e&&this.compareDate(n,e)<0?e:i&&this.compareDate(n,i)>0?i:n}},Vs=new O("mat-date-formats");var kl=(()=>{class t{isErrorState(e,i){return!!(e&&e.invalid&&(e.touched||i&&i.submitted))}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var Oi=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["structural-styles"]],decls:0,vars:0,template:function(i,r){},styles:[`.mat-focus-indicator{position:relative}.mat-focus-indicator::before{top:0;left:0;right:0;bottom:0;position:absolute;box-sizing:border-box;pointer-events:none;display:var(--mat-focus-indicator-display, none);border-width:var(--mat-focus-indicator-border-width, 3px);border-style:var(--mat-focus-indicator-border-style, solid);border-color:var(--mat-focus-indicator-border-color, transparent);border-radius:var(--mat-focus-indicator-border-radius, 4px)}.mat-focus-indicator:focus::before{content:""}@media(forced-colors: active){html{--mat-focus-indicator-display: block}} +`],encapsulation:2,changeDetection:0})}return t})();var Ya=(function(t){return t[t.NORMAL=0]="NORMAL",t[t.NEGATED=1]="NEGATED",t[t.INVERTED=2]="INVERTED",t})(Ya||{}),Zv,iu;function Xv(){if(iu==null){if(typeof document!="object"||!document||typeof Element!="function"||!Element)return iu=!1,iu;if(document.documentElement?.style&&"scrollBehavior"in document.documentElement.style)iu=!0;else{let t=Element.prototype.scrollTo;t?iu=!/\{\s*\[native code\]\s*\}/.test(t.toString()):iu=!1}}return iu}function oh(){if(typeof document!="object"||!document)return Ya.NORMAL;if(Zv==null){let t=document.createElement("div"),n=t.style;t.dir="rtl",n.width="1px",n.overflow="auto",n.visibility="hidden",n.pointerEvents="none",n.position="absolute";let e=document.createElement("div"),i=e.style;i.width="2px",i.height="1px",t.appendChild(e),document.body.appendChild(t),Zv=Ya.NORMAL,t.scrollLeft===0&&(t.scrollLeft=1,Zv=t.scrollLeft===0?Ya.NEGATED:Ya.INVERTED),t.remove()}return Zv}function AS(){return typeof __karma__<"u"&&!!__karma__||typeof jasmine<"u"&&!!jasmine||typeof jest<"u"&&!!jest||typeof Mocha<"u"&&!!Mocha}var ah,QL=["color","button","checkbox","date","datetime-local","email","file","hidden","image","month","number","password","radio","range","reset","search","submit","tel","text","time","url","week"];function OS(){if(ah)return ah;if(typeof document!="object"||!document)return ah=new Set(QL),ah;let t=document.createElement("input");return ah=new Set(QL.filter(n=>(t.setAttribute("type",n),t.type===n))),ah}function dn(t){return t==null?"":typeof t=="string"?t:`${t}px`}function Vi(t){return t!=null&&`${t}`!="false"}function KL(t,n=/\s+/){let e=[];if(t!=null){let i=Array.isArray(t)?t:`${t}`.split(n);for(let r of i){let o=`${r}`.trim();o&&e.push(o)}}return e}var To=(function(t){return t[t.FADING_IN=0]="FADING_IN",t[t.VISIBLE=1]="VISIBLE",t[t.FADING_OUT=2]="FADING_OUT",t[t.HIDDEN=3]="HIDDEN",t})(To||{}),Jv=class{_renderer;element;config;_animationForciblyDisabledThroughCss;state=To.HIDDEN;constructor(n,e,i,r=!1){this._renderer=n,this.element=e,this.config=i,this._animationForciblyDisabledThroughCss=r}fadeOut(){this._renderer.fadeOutRipple(this)}},ZL=Cc({passive:!0,capture:!0}),RS=class{_events=new Map;addHandler(n,e,i,r){let o=this._events.get(e);if(o){let a=o.get(i);a?a.add(r):o.set(i,new Set([r]))}else this._events.set(e,new Map([[i,new Set([r])]])),n.runOutsideAngular(()=>{document.addEventListener(e,this._delegateEventHandler,ZL)})}removeHandler(n,e,i){let r=this._events.get(n);if(!r)return;let o=r.get(e);o&&(o.delete(i),o.size===0&&r.delete(e),r.size===0&&(this._events.delete(n),document.removeEventListener(n,this._delegateEventHandler,ZL)))}_delegateEventHandler=n=>{let e=or(n);e&&this._events.get(n.type)?.forEach((i,r)=>{(r===e||r.contains(e))&&i.forEach(o=>o.handleEvent(n))})}},sh={enterDuration:225,exitDuration:150},wZ=800,XL=Cc({passive:!0,capture:!0}),JL=["mousedown","touchstart"],eV=["mouseup","mouseleave","touchend","touchcancel"],DZ=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["ng-component"]],hostAttrs:["mat-ripple-style-loader",""],decls:0,vars:0,template:function(i,r){},styles:[`.mat-ripple{overflow:hidden;position:relative}.mat-ripple:not(:empty){transform:translateZ(0)}.mat-ripple.mat-ripple-unbounded{overflow:visible}.mat-ripple-element{position:absolute;border-radius:50%;pointer-events:none;transition:opacity,transform 0ms cubic-bezier(0, 0, 0.2, 1);transform:scale3d(0, 0, 0);background-color:var(--mat-ripple-color, color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent))}@media(forced-colors: active){.mat-ripple-element{display:none}}.cdk-drag-preview .mat-ripple-element,.cdk-drag-placeholder .mat-ripple-element{display:none} +`],encapsulation:2,changeDetection:0})}return t})(),Ec=class t{_target;_ngZone;_platform;_containerElement;_triggerElement;_isPointerDown=!1;_activeRipples=new Map;_mostRecentTransientRipple;_lastTouchStartEvent;_pointerUpEventsRegistered=!1;_containerRect;static _eventManager=new RS;constructor(n,e,i,r,o){this._target=n,this._ngZone=e,this._platform=r,r.isBrowser&&(this._containerElement=Wr(i)),o&&o.get(ft).load(DZ)}fadeInRipple(n,e,i={}){let r=this._containerRect=this._containerRect||this._containerElement.getBoundingClientRect(),o=I(I({},sh),i.animation);i.centered&&(n=r.left+r.width/2,e=r.top+r.height/2);let a=i.radius||MZ(n,e,r),s=n-r.left,l=e-r.top,c=o.enterDuration,d=document.createElement("div");d.classList.add("mat-ripple-element"),d.style.left=`${s-a}px`,d.style.top=`${l-a}px`,d.style.height=`${a*2}px`,d.style.width=`${a*2}px`,i.color!=null&&(d.style.backgroundColor=i.color),d.style.transitionDuration=`${c}ms`,this._containerElement.appendChild(d);let p=window.getComputedStyle(d),_=p.transitionProperty,b=p.transitionDuration,y=_==="none"||b==="0s"||b==="0s, 0s"||r.width===0&&r.height===0,w=new Jv(this,d,i,y);d.style.transform="scale3d(1, 1, 1)",w.state=To.FADING_IN,i.persistent||(this._mostRecentTransientRipple=w);let C=null;return!y&&(c||o.exitDuration)&&this._ngZone.runOutsideAngular(()=>{let D=()=>{C&&(C.fallbackTimer=null),clearTimeout(W),this._finishRippleTransition(w)},F=()=>this._destroyRipple(w),W=setTimeout(F,c+100);d.addEventListener("transitionend",D),d.addEventListener("transitioncancel",F),C={onTransitionEnd:D,onTransitionCancel:F,fallbackTimer:W}}),this._activeRipples.set(w,C),(y||!c)&&this._finishRippleTransition(w),w}fadeOutRipple(n){if(n.state===To.FADING_OUT||n.state===To.HIDDEN)return;let e=n.element,i=I(I({},sh),n.config.animation);e.style.transitionDuration=`${i.exitDuration}ms`,e.style.opacity="0",n.state=To.FADING_OUT,(n._animationForciblyDisabledThroughCss||!i.exitDuration)&&this._finishRippleTransition(n)}fadeOutAll(){this._getActiveRipples().forEach(n=>n.fadeOut())}fadeOutAllNonPersistent(){this._getActiveRipples().forEach(n=>{n.config.persistent||n.fadeOut()})}setupTriggerEvents(n){let e=Wr(n);!this._platform.isBrowser||!e||e===this._triggerElement||(this._removeTriggerEvents(),this._triggerElement=e,JL.forEach(i=>{t._eventManager.addHandler(this._ngZone,i,e,this)}))}handleEvent(n){n.type==="mousedown"?this._onMousedown(n):n.type==="touchstart"?this._onTouchStart(n):this._onPointerUp(),this._pointerUpEventsRegistered||(this._ngZone.runOutsideAngular(()=>{eV.forEach(e=>{this._triggerElement.addEventListener(e,this,XL)})}),this._pointerUpEventsRegistered=!0)}_finishRippleTransition(n){n.state===To.FADING_IN?this._startFadeOutTransition(n):n.state===To.FADING_OUT&&this._destroyRipple(n)}_startFadeOutTransition(n){let e=n===this._mostRecentTransientRipple,{persistent:i}=n.config;n.state=To.VISIBLE,!i&&(!e||!this._isPointerDown)&&n.fadeOut()}_destroyRipple(n){let e=this._activeRipples.get(n)??null;this._activeRipples.delete(n),this._activeRipples.size||(this._containerRect=null),n===this._mostRecentTransientRipple&&(this._mostRecentTransientRipple=null),n.state=To.HIDDEN,e!==null&&(n.element.removeEventListener("transitionend",e.onTransitionEnd),n.element.removeEventListener("transitioncancel",e.onTransitionCancel),e.fallbackTimer!==null&&clearTimeout(e.fallbackTimer)),n.element.remove()}_onMousedown(n){let e=Jd(n),i=this._lastTouchStartEvent&&Date.now(){let e=n.state===To.VISIBLE||n.config.terminateOnPointerUp&&n.state===To.FADING_IN;!n.config.persistent&&e&&n.fadeOut()}))}_getActiveRipples(){return Array.from(this._activeRipples.keys())}_removeTriggerEvents(){let n=this._triggerElement;n&&(JL.forEach(e=>t._eventManager.removeHandler(e,n,this)),this._pointerUpEventsRegistered&&(eV.forEach(e=>n.removeEventListener(e,this,XL)),this._pointerUpEventsRegistered=!1))}};function MZ(t,n,e){let i=Math.max(Math.abs(t-e.left),Math.abs(t-e.right)),r=Math.max(Math.abs(n-e.top),Math.abs(n-e.bottom));return Math.sqrt(i*i+r*r)}var Bs=new O("mat-ripple-global-options"),qn=(()=>{class t{_elementRef=u(Y);_animationsDisabled=Qe();color;unbounded;centered;radius=0;animation;get disabled(){return this._disabled}set disabled(e){e&&this.fadeOutAllNonPersistent(),this._disabled=e,this._setupTriggerEventsIfEnabled()}_disabled=!1;get trigger(){return this._trigger||this._elementRef.nativeElement}set trigger(e){this._trigger=e,this._setupTriggerEventsIfEnabled()}_trigger;_rippleRenderer;_globalOptions;_isInitialized=!1;constructor(){let e=u(ae),i=u(Ye),r=u(Bs,{optional:!0}),o=u(de);this._globalOptions=r||{},this._rippleRenderer=new Ec(this,e,this._elementRef,i,o)}ngOnInit(){this._isInitialized=!0,this._setupTriggerEventsIfEnabled()}ngOnDestroy(){this._rippleRenderer._removeTriggerEvents()}fadeOutAll(){this._rippleRenderer.fadeOutAll()}fadeOutAllNonPersistent(){this._rippleRenderer.fadeOutAllNonPersistent()}get rippleConfig(){return{centered:this.centered,radius:this.radius,color:this.color,animation:I(I(I({},this._globalOptions.animation),this._animationsDisabled?{enterDuration:0,exitDuration:0}:{}),this.animation),terminateOnPointerUp:this._globalOptions.terminateOnPointerUp}}get rippleDisabled(){return this.disabled||!!this._globalOptions.disabled}_setupTriggerEventsIfEnabled(){!this.disabled&&this._isInitialized&&this._rippleRenderer.setupTriggerEvents(this.trigger)}launch(e,i=0,r){return typeof e=="number"?this._rippleRenderer.fadeInRipple(e,i,I(I({},this.rippleConfig),r)):this._rippleRenderer.fadeInRipple(0,0,I(I({},this.rippleConfig),e))}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","mat-ripple",""],["","matRipple",""]],hostAttrs:[1,"mat-ripple"],hostVars:2,hostBindings:function(i,r){i&2&&G("mat-ripple-unbounded",r.unbounded)},inputs:{color:[0,"matRippleColor","color"],unbounded:[0,"matRippleUnbounded","unbounded"],centered:[0,"matRippleCentered","centered"],radius:[0,"matRippleRadius","radius"],animation:[0,"matRippleAnimation","animation"],disabled:[0,"matRippleDisabled","disabled"],trigger:[0,"matRippleTrigger","trigger"]},exportAs:["matRipple"]})}return t})();var Io=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,De]})}return t})();var nu=(()=>{class t{_animationsDisabled=Qe();state="unchecked";disabled=!1;appearance="full";constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-pseudo-checkbox"]],hostAttrs:[1,"mat-pseudo-checkbox"],hostVars:12,hostBindings:function(i,r){i&2&&G("mat-pseudo-checkbox-indeterminate",r.state==="indeterminate")("mat-pseudo-checkbox-checked",r.state==="checked")("mat-pseudo-checkbox-disabled",r.disabled)("mat-pseudo-checkbox-minimal",r.appearance==="minimal")("mat-pseudo-checkbox-full",r.appearance==="full")("_mat-animation-noopable",r._animationsDisabled)},inputs:{state:"state",disabled:"disabled",appearance:"appearance"},decls:0,vars:0,template:function(i,r){},styles:[`.mat-pseudo-checkbox{border-radius:2px;cursor:pointer;display:inline-block;vertical-align:middle;box-sizing:border-box;position:relative;flex-shrink:0;transition:border-color 90ms cubic-bezier(0, 0, 0.2, 0.1),background-color 90ms cubic-bezier(0, 0, 0.2, 0.1)}.mat-pseudo-checkbox::after{position:absolute;opacity:0;content:"";border-bottom:2px solid currentColor;transition:opacity 90ms cubic-bezier(0, 0, 0.2, 0.1)}.mat-pseudo-checkbox._mat-animation-noopable{transition:none !important;animation:none !important}.mat-pseudo-checkbox._mat-animation-noopable::after{transition:none}.mat-pseudo-checkbox-disabled{cursor:default}.mat-pseudo-checkbox-indeterminate::after{left:1px;opacity:1;border-radius:2px}.mat-pseudo-checkbox-checked::after{left:1px;border-left:2px solid currentColor;transform:rotate(-45deg);opacity:1;box-sizing:content-box}.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked::after,.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate::after{color:var(--mat-pseudo-checkbox-minimal-selected-checkmark-color, var(--mat-sys-primary))}.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled::after,.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled::after{color:var(--mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-pseudo-checkbox-full{border-color:var(--mat-pseudo-checkbox-full-unselected-icon-color, var(--mat-sys-on-surface-variant));border-width:2px;border-style:solid}.mat-pseudo-checkbox-full.mat-pseudo-checkbox-disabled{border-color:var(--mat-pseudo-checkbox-full-disabled-unselected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked,.mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate{background-color:var(--mat-pseudo-checkbox-full-selected-icon-color, var(--mat-sys-primary));border-color:rgba(0,0,0,0)}.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked::after,.mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate::after{color:var(--mat-pseudo-checkbox-full-selected-checkmark-color, var(--mat-sys-on-primary))}.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled,.mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled{background-color:var(--mat-pseudo-checkbox-full-disabled-selected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled::after,.mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled::after{color:var(--mat-pseudo-checkbox-full-disabled-selected-checkmark-color, var(--mat-sys-surface))}.mat-pseudo-checkbox{width:18px;height:18px}.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked::after{width:14px;height:6px;transform-origin:center;top:-4.2426406871px;left:0;bottom:0;right:0;margin:auto}.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate::after{top:8px;width:16px}.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked::after{width:10px;height:4px;transform-origin:center;top:-2.8284271247px;left:0;bottom:0;right:0;margin:auto}.mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate::after{top:6px;width:12px} +`],encapsulation:2,changeDetection:0})}return t})();var ey=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De]})}return t})();var EZ=["*",[["mat-option"],["ng-container"]]],SZ=["*","mat-option, ng-container"],kZ=["text"],TZ=[[["mat-icon"]],"*"],IZ=["mat-icon","*"];function AZ(t,n){if(t&1&&M(0,"mat-pseudo-checkbox",1),t&2){let e=x();v("disabled",e.disabled)("state",e.selected?"checked":"unchecked")}}function OZ(t,n){if(t&1&&M(0,"mat-pseudo-checkbox",3),t&2){let e=x();v("disabled",e.disabled)}}function RZ(t,n){if(t&1&&(m(0,"span",4),f(1),h()),t&2){let e=x();g(),fe("(",e.group.label,")")}}var ru=new O("MAT_OPTION_PARENT_COMPONENT"),ou=new O("MatOptgroup"),PS=(()=>{class t{label;disabled=!1;_labelId=u(et).getId("mat-optgroup-label-");_inert;constructor(){let e=u(ru,{optional:!0});this._inert=e?.inertGroups??!1}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-optgroup"]],hostAttrs:[1,"mat-mdc-optgroup"],hostVars:3,hostBindings:function(i,r){i&2&&X("role",r._inert?null:"group")("aria-disabled",r._inert?null:r.disabled.toString())("aria-labelledby",r._inert?null:r._labelId)},inputs:{label:"label",disabled:[2,"disabled","disabled",B]},exportAs:["matOptgroup"],features:[we([{provide:ou,useExisting:t}])],ngContentSelectors:SZ,decls:5,vars:4,consts:[["role","presentation",1,"mat-mdc-optgroup-label",3,"id"],[1,"mdc-list-item__primary-text"]],template:function(i,r){i&1&&(Ee(EZ),gt(0,"span",0)(1,"span",1),f(2),ne(3),yt()(),ne(4,1)),i&2&&(G("mdc-list-item--disabled",r.disabled),pi("id",r._labelId),g(2),fe("",r.label," "))},styles:[`.mat-mdc-optgroup{color:var(--mat-optgroup-label-text-color, var(--mat-sys-on-surface-variant));font-family:var(--mat-optgroup-label-text-font, var(--mat-sys-title-small-font));line-height:var(--mat-optgroup-label-text-line-height, var(--mat-sys-title-small-line-height));font-size:var(--mat-optgroup-label-text-size, var(--mat-sys-title-small-size));letter-spacing:var(--mat-optgroup-label-text-tracking, var(--mat-sys-title-small-tracking));font-weight:var(--mat-optgroup-label-text-weight, var(--mat-sys-title-small-weight))}.mat-mdc-optgroup-label{display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;min-height:48px;padding:0 16px;outline:none}.mat-mdc-optgroup-label.mdc-list-item--disabled{opacity:.38}.mat-mdc-optgroup-label .mdc-list-item__primary-text{font-size:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;font-family:inherit;text-decoration:inherit;text-transform:inherit;white-space:normal;color:inherit} +`],encapsulation:2,changeDetection:0})}return t})(),lh=class{source;isUserInput;constructor(n,e=!1){this.source=n,this.isUserInput=e}},Sn=(()=>{class t{_element=u(Y);_changeDetectorRef=u(ye);_parent=u(ru,{optional:!0});group=u(ou,{optional:!0});_signalDisableRipple=!1;_selected=!1;_active=!1;_mostRecentViewValue="";get multiple(){return this._parent&&this._parent.multiple}get selected(){return this._selected}value;id=u(et).getId("mat-option-");get disabled(){return this.group&&this.group.disabled||this._disabled()}set disabled(e){this._disabled.set(e)}_disabled=he(!1);get disableRipple(){return this._signalDisableRipple?this._parent.disableRipple():!!this._parent?.disableRipple}get hideSingleSelectionIndicator(){return!!(this._parent&&this._parent.hideSingleSelectionIndicator)}onSelectionChange=new U;_text;_stateChanges=new z;constructor(){let e=u(ft);e.load(Oi),e.load(ro),this._signalDisableRipple=!!this._parent&&vs(this._parent.disableRipple)}get active(){return this._active}get viewValue(){return(this._text?.nativeElement.textContent||"").trim()}select(e=!0){this._selected||(this._selected=!0,this._changeDetectorRef.markForCheck(),e&&this._emitSelectionChangeEvent())}deselect(e=!0){this._selected&&(this._selected=!1,this._changeDetectorRef.markForCheck(),e&&this._emitSelectionChangeEvent())}focus(e,i){let r=this._getHostElement();typeof r.focus=="function"&&r.focus(i)}setActiveStyles(){this._active||(this._active=!0,this._changeDetectorRef.markForCheck())}setInactiveStyles(){this._active&&(this._active=!1,this._changeDetectorRef.markForCheck())}getLabel(){return this.viewValue}_handleKeydown(e){(e.keyCode===13||e.keyCode===32)&&!Gt(e)&&(this._selectViaInteraction(),e.preventDefault())}_selectViaInteraction(){this.disabled||(this._selected=this.multiple?!this._selected:!0,this._changeDetectorRef.markForCheck(),this._emitSelectionChangeEvent(!0))}_getTabIndex(){return this.disabled?"-1":"0"}_getHostElement(){return this._element.nativeElement}ngAfterViewChecked(){if(this._selected){let e=this.viewValue;e!==this._mostRecentViewValue&&(this._mostRecentViewValue&&this._stateChanges.next(),this._mostRecentViewValue=e)}}ngOnDestroy(){this._stateChanges.complete()}_emitSelectionChangeEvent(e=!1){this.onSelectionChange.emit(new lh(this,e))}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-option"]],viewQuery:function(i,r){if(i&1&&ie(kZ,7),i&2){let o;j(o=H())&&(r._text=o.first)}},hostAttrs:["role","option",1,"mat-mdc-option","mdc-list-item"],hostVars:11,hostBindings:function(i,r){i&1&&S("click",function(){return r._selectViaInteraction()})("keydown",function(a){return r._handleKeydown(a)}),i&2&&(pi("id",r.id),X("aria-selected",r.selected)("aria-disabled",r.disabled.toString()),G("mdc-list-item--selected",r.selected)("mat-mdc-option-multiple",r.multiple)("mat-mdc-option-active",r.active)("mdc-list-item--disabled",r.disabled))},inputs:{value:"value",id:"id",disabled:[2,"disabled","disabled",B]},outputs:{onSelectionChange:"onSelectionChange"},exportAs:["matOption"],ngContentSelectors:IZ,decls:8,vars:5,consts:[["text",""],["aria-hidden","true",1,"mat-mdc-option-pseudo-checkbox",3,"disabled","state"],[1,"mdc-list-item__primary-text"],["state","checked","aria-hidden","true","appearance","minimal",1,"mat-mdc-option-pseudo-checkbox",3,"disabled"],[1,"cdk-visually-hidden"],["aria-hidden","true","mat-ripple","",1,"mat-mdc-option-ripple","mat-focus-indicator",3,"matRippleTrigger","matRippleDisabled"]],template:function(i,r){i&1&&(Ee(TZ),L(0,AZ,1,2,"mat-pseudo-checkbox",1),ne(1),m(2,"span",2,0),ne(4,1),h(),L(5,OZ,1,1,"mat-pseudo-checkbox",3),L(6,RZ,2,1,"span",4),M(7,"div",5)),i&2&&(V(r.multiple?0:-1),g(5),V(!r.multiple&&r.selected&&!r.hideSingleSelectionIndicator?5:-1),g(),V(r.group&&r.group._inert?6:-1),g(),v("matRippleTrigger",r._getHostElement())("matRippleDisabled",r.disabled||r.disableRipple))},dependencies:[nu,qn],styles:[`.mat-mdc-option{-webkit-user-select:none;user-select:none;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;min-height:48px;padding:0 16px;cursor:pointer;-webkit-tap-highlight-color:rgba(0,0,0,0);color:var(--mat-option-label-text-color, var(--mat-sys-on-surface));font-family:var(--mat-option-label-text-font, var(--mat-sys-label-large-font));line-height:var(--mat-option-label-text-line-height, var(--mat-sys-label-large-line-height));font-size:var(--mat-option-label-text-size, var(--mat-sys-body-large-size));letter-spacing:var(--mat-option-label-text-tracking, var(--mat-sys-label-large-tracking));font-weight:var(--mat-option-label-text-weight, var(--mat-sys-body-large-weight))}.mat-mdc-option:hover:not(.mdc-list-item--disabled){background-color:var(--mat-option-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}.mat-mdc-option:focus.mdc-list-item,.mat-mdc-option.mat-mdc-option-active.mdc-list-item{background-color:var(--mat-option-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent));outline:0}.mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple){background-color:var(--mat-option-selected-state-layer-color, var(--mat-sys-secondary-container))}.mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) .mdc-list-item__primary-text{color:var(--mat-option-selected-state-label-text-color, var(--mat-sys-on-secondary-container))}.mat-mdc-option .mat-pseudo-checkbox{--mat-pseudo-checkbox-minimal-selected-checkmark-color: var(--mat-option-selected-state-label-text-color, var(--mat-sys-on-secondary-container))}.mat-mdc-option.mdc-list-item{align-items:center;background:rgba(0,0,0,0)}.mat-mdc-option.mdc-list-item--disabled{cursor:default;pointer-events:none}.mat-mdc-option.mdc-list-item--disabled .mat-mdc-option-pseudo-checkbox,.mat-mdc-option.mdc-list-item--disabled .mdc-list-item__primary-text,.mat-mdc-option.mdc-list-item--disabled>mat-icon{opacity:.38}.mat-mdc-optgroup .mat-mdc-option:not(.mat-mdc-option-multiple){padding-left:32px}[dir=rtl] .mat-mdc-optgroup .mat-mdc-option:not(.mat-mdc-option-multiple){padding-left:16px;padding-right:32px}.mat-mdc-option .mat-icon,.mat-mdc-option .mat-pseudo-checkbox-full{margin-right:16px;flex-shrink:0}[dir=rtl] .mat-mdc-option .mat-icon,[dir=rtl] .mat-mdc-option .mat-pseudo-checkbox-full{margin-right:0;margin-left:16px}.mat-mdc-option .mat-pseudo-checkbox-minimal{margin-left:16px;flex-shrink:0}[dir=rtl] .mat-mdc-option .mat-pseudo-checkbox-minimal{margin-right:16px;margin-left:0}.mat-mdc-option .mat-mdc-option-ripple{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none}.mat-mdc-option .mdc-list-item__primary-text{white-space:normal;font-size:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;font-family:inherit;text-decoration:inherit;text-transform:inherit;margin-right:auto}[dir=rtl] .mat-mdc-option .mdc-list-item__primary-text{margin-right:0;margin-left:auto}@media(forced-colors: active){.mat-mdc-option.mdc-list-item--selected:not(:has(.mat-mdc-option-pseudo-checkbox))::after{content:"";position:absolute;top:50%;right:16px;transform:translateY(-50%);width:10px;height:0;border-bottom:solid 10px;border-radius:10px}[dir=rtl] .mat-mdc-option.mdc-list-item--selected:not(:has(.mat-mdc-option-pseudo-checkbox))::after{right:auto;left:16px}}.mat-mdc-option-multiple{--mat-list-list-item-selected-container-color: var(--mat-list-list-item-container-color, transparent)}.mat-mdc-option-active .mat-focus-indicator::before{content:""} +`],encapsulation:2,changeDetection:0})}return t})();function tg(t,n,e){if(e.length){let i=n.toArray(),r=e.toArray(),o=0;for(let a=0;ae+i?Math.max(0,t-i+n):e}var ch=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[Io,De,ey,Sn]})}return t})();var PZ={capture:!0},FZ=["focus","mousedown","mouseenter","touchstart"],FS="mat-ripple-loader-uninitialized",NS="mat-ripple-loader-class-name",tV="mat-ripple-loader-centered",ty="mat-ripple-loader-disabled",iy=(()=>{class t{_document=u(_e);_animationsDisabled=Qe();_globalRippleOptions=u(Bs,{optional:!0});_platform=u(Ye);_ngZone=u(ae);_injector=u(de);_eventCleanups;_hosts=new Map;constructor(){let e=u(hn).createRenderer(null,null);this._eventCleanups=this._ngZone.runOutsideAngular(()=>FZ.map(i=>e.listen(this._document,i,this._onInteraction,PZ)))}ngOnDestroy(){let e=this._hosts.keys();for(let i of e)this.destroyRipple(i);this._eventCleanups.forEach(i=>i())}configureRipple(e,i){e.setAttribute(FS,this._globalRippleOptions?.namespace??""),(i.className||!e.hasAttribute(NS))&&e.setAttribute(NS,i.className||""),i.centered&&e.setAttribute(tV,""),i.disabled&&e.setAttribute(ty,"")}setDisabled(e,i){let r=this._hosts.get(e);r?(r.target.rippleDisabled=i,!i&&!r.hasSetUpEvents&&(r.hasSetUpEvents=!0,r.renderer.setupTriggerEvents(e))):i?e.setAttribute(ty,""):e.removeAttribute(ty)}_onInteraction=e=>{let i=or(e);if(i instanceof HTMLElement){let r=i.closest(`[${FS}="${this._globalRippleOptions?.namespace??""}"]`);r&&this._createRipple(r)}};_createRipple(e){if(!this._document||this._hosts.has(e))return;e.querySelector(".mat-ripple")?.remove();let i=this._document.createElement("span");i.classList.add("mat-ripple",e.getAttribute(NS)),e.append(i);let r=this._globalRippleOptions,o=this._animationsDisabled?0:r?.animation?.enterDuration??sh.enterDuration,a=this._animationsDisabled?0:r?.animation?.exitDuration??sh.exitDuration,s={rippleDisabled:this._animationsDisabled||r?.disabled||e.hasAttribute(ty),rippleConfig:{centered:e.hasAttribute(tV),terminateOnPointerUp:r?.terminateOnPointerUp,animation:{enterDuration:o,exitDuration:a}}},l=new Ec(s,this._ngZone,i,this._platform,this._injector),c=!s.rippleDisabled;c&&l.setupTriggerEvents(e),this._hosts.set(e,{target:s,renderer:l,hasSetUpEvents:c}),e.removeAttribute(FS)}destroyRipple(e){let i=this._hosts.get(e);i&&(i.renderer._removeTriggerEvents(),this._hosts.delete(e))}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var NZ=/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))?)?$/,LZ=/^(\d?\d)[:.](\d?\d)(?:[:.](\d?\d))?\s*(AM|PM)?$/i;function LS(t,n){let e=Array(t);for(let i=0;i{class t extends $i{useUtcForDisplay=!1;_matDateLocale=u(Ls,{optional:!0});constructor(){super();let e=u(Ls,{optional:!0});e!==void 0&&(this._matDateLocale=e),super.setLocale(this._matDateLocale)}getYear(e){return e.getFullYear()}getMonth(e){return e.getMonth()}getDate(e){return e.getDate()}getDayOfWeek(e){return e.getDay()}getMonthNames(e){let i=new Intl.DateTimeFormat(this.locale,{month:e,timeZone:"utc"});return LS(12,r=>this._format(i,new Date(2017,r,1)))}getDateNames(){let e=new Intl.DateTimeFormat(this.locale,{day:"numeric",timeZone:"utc"});return LS(31,i=>this._format(e,new Date(2017,0,i+1)))}getDayOfWeekNames(e){let i=new Intl.DateTimeFormat(this.locale,{weekday:e,timeZone:"utc"});return LS(7,r=>this._format(i,new Date(2017,0,r+1)))}getYearName(e){let i=new Intl.DateTimeFormat(this.locale,{year:"numeric",timeZone:"utc"});return this._format(i,e)}getFirstDayOfWeek(){if(typeof Intl<"u"&&Intl.Locale){let e=new Intl.Locale(this.locale),i=(e.getWeekInfo?.()||e.weekInfo)?.firstDay??0;return i===7?0:i}return 0}getNumDaysInMonth(e){return this.getDate(this._createDateWithOverflow(this.getYear(e),this.getMonth(e)+1,0))}clone(e){return new Date(e.getTime())}createDate(e,i,r){let o=this._createDateWithOverflow(e,i,r);return o.getMonth()!=i,o}today(){return new Date}parse(e,i){return typeof e=="number"?new Date(e):e?new Date(Date.parse(e)):null}format(e,i){if(!this.isValid(e))throw Error("NativeDateAdapter: Cannot format invalid date.");let r=new Intl.DateTimeFormat(this.locale,Me(I({},i),{timeZone:"utc"}));return this._format(r,e)}addCalendarYears(e,i){return this.addCalendarMonths(e,i*12)}addCalendarMonths(e,i){let r=this._createDateWithOverflow(this.getYear(e),this.getMonth(e)+i,this.getDate(e));return this.getMonth(r)!=((this.getMonth(e)+i)%12+12)%12&&(r=this._createDateWithOverflow(this.getYear(r),this.getMonth(r),0)),r}addCalendarDays(e,i){return this._createDateWithOverflow(this.getYear(e),this.getMonth(e),this.getDate(e)+i)}toIso8601(e){return[e.getUTCFullYear(),this._2digit(e.getUTCMonth()+1),this._2digit(e.getUTCDate())].join("-")}deserialize(e){if(typeof e=="string"){if(!e)return null;if(NZ.test(e)){let i=new Date(e);if(this.isValid(i))return i}}return super.deserialize(e)}isDateInstance(e){return e instanceof Date}isValid(e){return!isNaN(e.getTime())}invalid(){return new Date(NaN)}setTime(e,i,r,o){let a=this.clone(e);return a.setHours(i,r,o,0),a}getHours(e){return e.getHours()}getMinutes(e){return e.getMinutes()}getSeconds(e){return e.getSeconds()}parseTime(e,i){if(typeof e!="string")return e instanceof Date?new Date(e.getTime()):null;let r=e.trim();if(r.length===0)return null;let o=this._parseTimeString(r);if(o===null){let a=r.replace(/[^0-9:(AM|PM)]/gi,"").trim();a.length>0&&(o=this._parseTimeString(a))}return o||this.invalid()}addSeconds(e,i){return new Date(e.getTime()+i*1e3)}_createDateWithOverflow(e,i,r){let o=new Date;return o.setFullYear(e,i,r),o.setHours(0,0,0,0),o}_2digit(e){return("00"+e).slice(-2)}_format(e,i){let r=new Date;return r.setUTCFullYear(i.getFullYear(),i.getMonth(),i.getDate()),r.setUTCHours(i.getHours(),i.getMinutes(),i.getSeconds(),i.getMilliseconds()),e.format(r)}_parseTimeString(e){let i=e.toUpperCase().match(LZ);if(i){let r=parseInt(i[1]),o=parseInt(i[2]),a=i[3]==null?void 0:parseInt(i[3]),s=i[4];if(r===12?r=s==="AM"?0:r:s==="PM"&&(r+=12),VS(r,0,23)&&VS(o,0,59)&&(a==null||VS(a,0,59)))return this.setTime(this.today(),r,o,a||0)}return null}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();function VS(t,n,e){return!isNaN(t)&&t>=n&&t<=e}var VZ={parse:{dateInput:null,timeInput:null},display:{dateInput:{year:"numeric",month:"numeric",day:"numeric"},timeInput:{hour:"numeric",minute:"numeric"},monthYearLabel:{year:"numeric",month:"short"},dateA11yLabel:{year:"numeric",month:"long",day:"numeric"},monthYearA11yLabel:{year:"numeric",month:"long"},timeOptionLabel:{hour:"numeric",minute:"numeric"}}};var nV=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[BZ()]})}return t})();function BZ(t=VZ){return[{provide:$i,useClass:iV},{provide:Vs,useValue:t}]}var jZ=Math.pow(10,8)*24*60*60*1e3,Y4e=-jZ,ny=6048e5,oV=864e5,au=6e4,su=36e5,aV=1e3;var HZ=3600;var sV=HZ*24,Q4e=sV*7,zZ=sV*365.2425,UZ=zZ/12,K4e=UZ*3,BS=Symbol.for("constructDateFrom");function ct(t,n){return typeof t=="function"?t(n):t&&typeof t=="object"&&BS in t?t[BS](n):t instanceof Date?new t.constructor(n):new Date(n)}function Ie(t,n){return ct(n||t,t)}function dh(t,n,e){let i=Ie(t,e?.in);return isNaN(n)?ct(e?.in||t,NaN):(n&&i.setDate(i.getDate()+n),i)}function uh(t,n,e){let i=Ie(t,e?.in);if(isNaN(n))return ct(e?.in||t,NaN);if(!n)return i;let r=i.getDate(),o=ct(e?.in||t,i.getTime());o.setMonth(i.getMonth()+n+1,0);let a=o.getDate();return r>=a?o:(i.setFullYear(o.getFullYear(),o.getMonth(),r),i)}function ry(t,n,e){return ct(e?.in||t,+Ie(t)+n)}function lV(t,n,e){return ry(t,n*su,e)}var $Z={};function ta(){return $Z}function ar(t,n){let e=ta(),i=n?.weekStartsOn??n?.locale?.options?.weekStartsOn??e.weekStartsOn??e.locale?.options?.weekStartsOn??0,r=Ie(t,n?.in),o=r.getDay(),a=(o=o.getTime()?i+1:e.getTime()>=s.getTime()?i:i-1}function lu(t){let n=Ie(t),e=new Date(Date.UTC(n.getFullYear(),n.getMonth(),n.getDate(),n.getHours(),n.getMinutes(),n.getSeconds(),n.getMilliseconds()));return e.setUTCFullYear(n.getFullYear()),+t-+e}function ay(t,...n){let e=ct.bind(null,t||n.find(i=>typeof i=="object"));return n.map(e)}function jS(t,n){let e=Ie(t,n?.in);return e.setHours(0,0,0,0),e}function cV(t,n,e){let[i,r]=ay(e?.in,t,n),o=jS(i),a=jS(r),s=+o-lu(o),l=+a-lu(a);return Math.round((s-l)/oV)}function dV(t,n){let e=oy(t,n),i=ct(n?.in||t,0);return i.setFullYear(e,0,4),i.setHours(0,0,0,0),Qa(i)}function uV(t,n,e){let i=Ie(t,e?.in);return i.setTime(i.getTime()+n*au),i}function mV(t,n,e){return ry(t,n*1e3,e)}function hV(t,n,e){return uh(t,n*12,e)}function sy(t){return t instanceof Date||typeof t=="object"&&Object.prototype.toString.call(t)==="[object Date]"}function mh(t){return!(!sy(t)&&typeof t!="number"||isNaN(+Ie(t)))}function pV(t,n){let e=Ie(t,n?.in);return e.setDate(1),e.setHours(0,0,0,0),e}function fV(t,n){let e=Ie(t,n?.in);return e.setFullYear(e.getFullYear(),0,1),e.setHours(0,0,0,0),e}var WZ={lessThanXSeconds:{one:"less than a second",other:"less than {{count}} seconds"},xSeconds:{one:"1 second",other:"{{count}} seconds"},halfAMinute:"half a minute",lessThanXMinutes:{one:"less than a minute",other:"less than {{count}} minutes"},xMinutes:{one:"1 minute",other:"{{count}} minutes"},aboutXHours:{one:"about 1 hour",other:"about {{count}} hours"},xHours:{one:"1 hour",other:"{{count}} hours"},xDays:{one:"1 day",other:"{{count}} days"},aboutXWeeks:{one:"about 1 week",other:"about {{count}} weeks"},xWeeks:{one:"1 week",other:"{{count}} weeks"},aboutXMonths:{one:"about 1 month",other:"about {{count}} months"},xMonths:{one:"1 month",other:"{{count}} months"},aboutXYears:{one:"about 1 year",other:"about {{count}} years"},xYears:{one:"1 year",other:"{{count}} years"},overXYears:{one:"over 1 year",other:"over {{count}} years"},almostXYears:{one:"almost 1 year",other:"almost {{count}} years"}},gV=(t,n,e)=>{let i,r=WZ[t];return typeof r=="string"?i=r:n===1?i=r.one:i=r.other.replace("{{count}}",n.toString()),e?.addSuffix?e.comparison&&e.comparison>0?"in "+i:i+" ago":i};function ia(t){return(n={})=>{let e=n.width?String(n.width):t.defaultWidth;return t.formats[e]||t.formats[t.defaultWidth]}}var GZ={full:"EEEE, MMMM do, y",long:"MMMM do, y",medium:"MMM d, y",short:"MM/dd/yyyy"},qZ={full:"h:mm:ss a zzzz",long:"h:mm:ss a z",medium:"h:mm:ss a",short:"h:mm a"},YZ={full:"{{date}} 'at' {{time}}",long:"{{date}} 'at' {{time}}",medium:"{{date}}, {{time}}",short:"{{date}}, {{time}}"},_V={date:ia({formats:GZ,defaultWidth:"full"}),time:ia({formats:qZ,defaultWidth:"full"}),dateTime:ia({formats:YZ,defaultWidth:"full"})};var QZ={lastWeek:"'last' eeee 'at' p",yesterday:"'yesterday at' p",today:"'today at' p",tomorrow:"'tomorrow at' p",nextWeek:"eeee 'at' p",other:"P"},bV=(t,n,e,i)=>QZ[t];function Yn(t){return(n,e)=>{let i=e?.context?String(e.context):"standalone",r;if(i==="formatting"&&t.formattingValues){let a=t.defaultFormattingWidth||t.defaultWidth,s=e?.width?String(e.width):a;r=t.formattingValues[s]||t.formattingValues[a]}else{let a=t.defaultWidth,s=e?.width?String(e.width):t.defaultWidth;r=t.values[s]||t.values[a]}let o=t.argumentCallback?t.argumentCallback(n):n;return r[o]}}var KZ={narrow:["B","A"],abbreviated:["BC","AD"],wide:["Before Christ","Anno Domini"]},ZZ={narrow:["1","2","3","4"],abbreviated:["Q1","Q2","Q3","Q4"],wide:["1st quarter","2nd quarter","3rd quarter","4th quarter"]},XZ={narrow:["J","F","M","A","M","J","J","A","S","O","N","D"],abbreviated:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],wide:["January","February","March","April","May","June","July","August","September","October","November","December"]},JZ={narrow:["S","M","T","W","T","F","S"],short:["Su","Mo","Tu","We","Th","Fr","Sa"],abbreviated:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],wide:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},eX={narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"}},tX={narrow:{am:"a",pm:"p",midnight:"mi",noon:"n",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},abbreviated:{am:"AM",pm:"PM",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"},wide:{am:"a.m.",pm:"p.m.",midnight:"midnight",noon:"noon",morning:"in the morning",afternoon:"in the afternoon",evening:"in the evening",night:"at night"}},iX=(t,n)=>{let e=Number(t),i=e%100;if(i>20||i<10)switch(i%10){case 1:return e+"st";case 2:return e+"nd";case 3:return e+"rd"}return e+"th"},vV={ordinalNumber:iX,era:Yn({values:KZ,defaultWidth:"wide"}),quarter:Yn({values:ZZ,defaultWidth:"wide",argumentCallback:t=>t-1}),month:Yn({values:XZ,defaultWidth:"wide"}),day:Yn({values:JZ,defaultWidth:"wide"}),dayPeriod:Yn({values:eX,defaultWidth:"wide",formattingValues:tX,defaultFormattingWidth:"wide"})};function Qn(t){return(n,e={})=>{let i=e.width,r=i&&t.matchPatterns[i]||t.matchPatterns[t.defaultMatchWidth],o=n.match(r);if(!o)return null;let a=o[0],s=i&&t.parsePatterns[i]||t.parsePatterns[t.defaultParseWidth],l=Array.isArray(s)?rX(s,p=>p.test(a)):nX(s,p=>p.test(a)),c;c=t.valueCallback?t.valueCallback(l):l,c=e.valueCallback?e.valueCallback(c):c;let d=n.slice(a.length);return{value:c,rest:d}}}function nX(t,n){for(let e in t)if(Object.prototype.hasOwnProperty.call(t,e)&&n(t[e]))return e}function rX(t,n){for(let e=0;e{let i=n.match(t.matchPattern);if(!i)return null;let r=i[0],o=n.match(t.parsePattern);if(!o)return null;let a=t.valueCallback?t.valueCallback(o[0]):o[0];a=e.valueCallback?e.valueCallback(a):a;let s=n.slice(r.length);return{value:a,rest:s}}}var oX=/^(\d+)(th|st|nd|rd)?/i,aX=/\d+/i,sX={narrow:/^(b|a)/i,abbreviated:/^(b\.?\s?c\.?|b\.?\s?c\.?\s?e\.?|a\.?\s?d\.?|c\.?\s?e\.?)/i,wide:/^(before christ|before common era|anno domini|common era)/i},lX={any:[/^b/i,/^(a|c)/i]},cX={narrow:/^[1234]/i,abbreviated:/^q[1234]/i,wide:/^[1234](th|st|nd|rd)? quarter/i},dX={any:[/1/i,/2/i,/3/i,/4/i]},uX={narrow:/^[jfmasond]/i,abbreviated:/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i,wide:/^(january|february|march|april|may|june|july|august|september|october|november|december)/i},mX={narrow:[/^j/i,/^f/i,/^m/i,/^a/i,/^m/i,/^j/i,/^j/i,/^a/i,/^s/i,/^o/i,/^n/i,/^d/i],any:[/^ja/i,/^f/i,/^mar/i,/^ap/i,/^may/i,/^jun/i,/^jul/i,/^au/i,/^s/i,/^o/i,/^n/i,/^d/i]},hX={narrow:/^[smtwf]/i,short:/^(su|mo|tu|we|th|fr|sa)/i,abbreviated:/^(sun|mon|tue|wed|thu|fri|sat)/i,wide:/^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)/i},pX={narrow:[/^s/i,/^m/i,/^t/i,/^w/i,/^t/i,/^f/i,/^s/i],any:[/^su/i,/^m/i,/^tu/i,/^w/i,/^th/i,/^f/i,/^sa/i]},fX={narrow:/^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,any:/^([ap]\.?\s?m\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i},gX={any:{am:/^a/i,pm:/^p/i,midnight:/^mi/i,noon:/^no/i,morning:/morning/i,afternoon:/afternoon/i,evening:/evening/i,night:/night/i}},yV={ordinalNumber:hh({matchPattern:oX,parsePattern:aX,valueCallback:t=>parseInt(t,10)}),era:Qn({matchPatterns:sX,defaultMatchWidth:"wide",parsePatterns:lX,defaultParseWidth:"any"}),quarter:Qn({matchPatterns:cX,defaultMatchWidth:"wide",parsePatterns:dX,defaultParseWidth:"any",valueCallback:t=>t+1}),month:Qn({matchPatterns:uX,defaultMatchWidth:"wide",parsePatterns:mX,defaultParseWidth:"any"}),day:Qn({matchPatterns:hX,defaultMatchWidth:"wide",parsePatterns:pX,defaultParseWidth:"any"}),dayPeriod:Qn({matchPatterns:fX,defaultMatchWidth:"any",parsePatterns:gX,defaultParseWidth:"any"})};var cu={code:"en-US",formatDistance:gV,formatLong:_V,formatRelative:bV,localize:vV,match:yV,options:{weekStartsOn:0,firstWeekContainsDate:1}};function xV(t,n){let e=Ie(t,n?.in);return cV(e,fV(e))+1}function ly(t,n){let e=Ie(t,n?.in),i=+Qa(e)-+dV(e);return Math.round(i/ny)+1}function ph(t,n){let e=Ie(t,n?.in),i=e.getFullYear(),r=ta(),o=n?.firstWeekContainsDate??n?.locale?.options?.firstWeekContainsDate??r.firstWeekContainsDate??r.locale?.options?.firstWeekContainsDate??1,a=ct(n?.in||t,0);a.setFullYear(i+1,0,o),a.setHours(0,0,0,0);let s=ar(a,n),l=ct(n?.in||t,0);l.setFullYear(i,0,o),l.setHours(0,0,0,0);let c=ar(l,n);return+e>=+s?i+1:+e>=+c?i:i-1}function CV(t,n){let e=ta(),i=n?.firstWeekContainsDate??n?.locale?.options?.firstWeekContainsDate??e.firstWeekContainsDate??e.locale?.options?.firstWeekContainsDate??1,r=ph(t,n),o=ct(n?.in||t,0);return o.setFullYear(r,0,i),o.setHours(0,0,0,0),ar(o,n)}function fh(t,n){let e=Ie(t,n?.in),i=+ar(e,n)-+CV(e,n);return Math.round(i/ny)+1}function St(t,n){let e=t<0?"-":"",i=Math.abs(t).toString().padStart(n,"0");return e+i}var Tl={y(t,n){let e=t.getFullYear(),i=e>0?e:1-e;return St(n==="yy"?i%100:i,n.length)},M(t,n){let e=t.getMonth();return n==="M"?String(e+1):St(e+1,2)},d(t,n){return St(t.getDate(),n.length)},a(t,n){let e=t.getHours()/12>=1?"pm":"am";switch(n){case"a":case"aa":return e.toUpperCase();case"aaa":return e;case"aaaaa":return e[0];case"aaaa":default:return e==="am"?"a.m.":"p.m."}},h(t,n){return St(t.getHours()%12||12,n.length)},H(t,n){return St(t.getHours(),n.length)},m(t,n){return St(t.getMinutes(),n.length)},s(t,n){return St(t.getSeconds(),n.length)},S(t,n){let e=n.length,i=t.getMilliseconds(),r=Math.trunc(i*Math.pow(10,e-3));return St(r,n.length)}};var gh={am:"am",pm:"pm",midnight:"midnight",noon:"noon",morning:"morning",afternoon:"afternoon",evening:"evening",night:"night"},HS={G:function(t,n,e){let i=t.getFullYear()>0?1:0;switch(n){case"G":case"GG":case"GGG":return e.era(i,{width:"abbreviated"});case"GGGGG":return e.era(i,{width:"narrow"});case"GGGG":default:return e.era(i,{width:"wide"})}},y:function(t,n,e){if(n==="yo"){let i=t.getFullYear(),r=i>0?i:1-i;return e.ordinalNumber(r,{unit:"year"})}return Tl.y(t,n)},Y:function(t,n,e,i){let r=ph(t,i),o=r>0?r:1-r;if(n==="YY"){let a=o%100;return St(a,2)}return n==="Yo"?e.ordinalNumber(o,{unit:"year"}):St(o,n.length)},R:function(t,n){let e=oy(t);return St(e,n.length)},u:function(t,n){let e=t.getFullYear();return St(e,n.length)},Q:function(t,n,e){let i=Math.ceil((t.getMonth()+1)/3);switch(n){case"Q":return String(i);case"QQ":return St(i,2);case"Qo":return e.ordinalNumber(i,{unit:"quarter"});case"QQQ":return e.quarter(i,{width:"abbreviated",context:"formatting"});case"QQQQQ":return e.quarter(i,{width:"narrow",context:"formatting"});case"QQQQ":default:return e.quarter(i,{width:"wide",context:"formatting"})}},q:function(t,n,e){let i=Math.ceil((t.getMonth()+1)/3);switch(n){case"q":return String(i);case"qq":return St(i,2);case"qo":return e.ordinalNumber(i,{unit:"quarter"});case"qqq":return e.quarter(i,{width:"abbreviated",context:"standalone"});case"qqqqq":return e.quarter(i,{width:"narrow",context:"standalone"});case"qqqq":default:return e.quarter(i,{width:"wide",context:"standalone"})}},M:function(t,n,e){let i=t.getMonth();switch(n){case"M":case"MM":return Tl.M(t,n);case"Mo":return e.ordinalNumber(i+1,{unit:"month"});case"MMM":return e.month(i,{width:"abbreviated",context:"formatting"});case"MMMMM":return e.month(i,{width:"narrow",context:"formatting"});case"MMMM":default:return e.month(i,{width:"wide",context:"formatting"})}},L:function(t,n,e){let i=t.getMonth();switch(n){case"L":return String(i+1);case"LL":return St(i+1,2);case"Lo":return e.ordinalNumber(i+1,{unit:"month"});case"LLL":return e.month(i,{width:"abbreviated",context:"standalone"});case"LLLLL":return e.month(i,{width:"narrow",context:"standalone"});case"LLLL":default:return e.month(i,{width:"wide",context:"standalone"})}},w:function(t,n,e,i){let r=fh(t,i);return n==="wo"?e.ordinalNumber(r,{unit:"week"}):St(r,n.length)},I:function(t,n,e){let i=ly(t);return n==="Io"?e.ordinalNumber(i,{unit:"week"}):St(i,n.length)},d:function(t,n,e){return n==="do"?e.ordinalNumber(t.getDate(),{unit:"date"}):Tl.d(t,n)},D:function(t,n,e){let i=xV(t);return n==="Do"?e.ordinalNumber(i,{unit:"dayOfYear"}):St(i,n.length)},E:function(t,n,e){let i=t.getDay();switch(n){case"E":case"EE":case"EEE":return e.day(i,{width:"abbreviated",context:"formatting"});case"EEEEE":return e.day(i,{width:"narrow",context:"formatting"});case"EEEEEE":return e.day(i,{width:"short",context:"formatting"});case"EEEE":default:return e.day(i,{width:"wide",context:"formatting"})}},e:function(t,n,e,i){let r=t.getDay(),o=(r-i.weekStartsOn+8)%7||7;switch(n){case"e":return String(o);case"ee":return St(o,2);case"eo":return e.ordinalNumber(o,{unit:"day"});case"eee":return e.day(r,{width:"abbreviated",context:"formatting"});case"eeeee":return e.day(r,{width:"narrow",context:"formatting"});case"eeeeee":return e.day(r,{width:"short",context:"formatting"});case"eeee":default:return e.day(r,{width:"wide",context:"formatting"})}},c:function(t,n,e,i){let r=t.getDay(),o=(r-i.weekStartsOn+8)%7||7;switch(n){case"c":return String(o);case"cc":return St(o,n.length);case"co":return e.ordinalNumber(o,{unit:"day"});case"ccc":return e.day(r,{width:"abbreviated",context:"standalone"});case"ccccc":return e.day(r,{width:"narrow",context:"standalone"});case"cccccc":return e.day(r,{width:"short",context:"standalone"});case"cccc":default:return e.day(r,{width:"wide",context:"standalone"})}},i:function(t,n,e){let i=t.getDay(),r=i===0?7:i;switch(n){case"i":return String(r);case"ii":return St(r,n.length);case"io":return e.ordinalNumber(r,{unit:"day"});case"iii":return e.day(i,{width:"abbreviated",context:"formatting"});case"iiiii":return e.day(i,{width:"narrow",context:"formatting"});case"iiiiii":return e.day(i,{width:"short",context:"formatting"});case"iiii":default:return e.day(i,{width:"wide",context:"formatting"})}},a:function(t,n,e){let r=t.getHours()/12>=1?"pm":"am";switch(n){case"a":case"aa":return e.dayPeriod(r,{width:"abbreviated",context:"formatting"});case"aaa":return e.dayPeriod(r,{width:"abbreviated",context:"formatting"}).toLowerCase();case"aaaaa":return e.dayPeriod(r,{width:"narrow",context:"formatting"});case"aaaa":default:return e.dayPeriod(r,{width:"wide",context:"formatting"})}},b:function(t,n,e){let i=t.getHours(),r;switch(i===12?r=gh.noon:i===0?r=gh.midnight:r=i/12>=1?"pm":"am",n){case"b":case"bb":return e.dayPeriod(r,{width:"abbreviated",context:"formatting"});case"bbb":return e.dayPeriod(r,{width:"abbreviated",context:"formatting"}).toLowerCase();case"bbbbb":return e.dayPeriod(r,{width:"narrow",context:"formatting"});case"bbbb":default:return e.dayPeriod(r,{width:"wide",context:"formatting"})}},B:function(t,n,e){let i=t.getHours(),r;switch(i>=17?r=gh.evening:i>=12?r=gh.afternoon:i>=4?r=gh.morning:r=gh.night,n){case"B":case"BB":case"BBB":return e.dayPeriod(r,{width:"abbreviated",context:"formatting"});case"BBBBB":return e.dayPeriod(r,{width:"narrow",context:"formatting"});case"BBBB":default:return e.dayPeriod(r,{width:"wide",context:"formatting"})}},h:function(t,n,e){if(n==="ho"){let i=t.getHours()%12;return i===0&&(i=12),e.ordinalNumber(i,{unit:"hour"})}return Tl.h(t,n)},H:function(t,n,e){return n==="Ho"?e.ordinalNumber(t.getHours(),{unit:"hour"}):Tl.H(t,n)},K:function(t,n,e){let i=t.getHours()%12;return n==="Ko"?e.ordinalNumber(i,{unit:"hour"}):St(i,n.length)},k:function(t,n,e){let i=t.getHours();return i===0&&(i=24),n==="ko"?e.ordinalNumber(i,{unit:"hour"}):St(i,n.length)},m:function(t,n,e){return n==="mo"?e.ordinalNumber(t.getMinutes(),{unit:"minute"}):Tl.m(t,n)},s:function(t,n,e){return n==="so"?e.ordinalNumber(t.getSeconds(),{unit:"second"}):Tl.s(t,n)},S:function(t,n){return Tl.S(t,n)},X:function(t,n,e){let i=t.getTimezoneOffset();if(i===0)return"Z";switch(n){case"X":return DV(i);case"XXXX":case"XX":return du(i);case"XXXXX":case"XXX":default:return du(i,":")}},x:function(t,n,e){let i=t.getTimezoneOffset();switch(n){case"x":return DV(i);case"xxxx":case"xx":return du(i);case"xxxxx":case"xxx":default:return du(i,":")}},O:function(t,n,e){let i=t.getTimezoneOffset();switch(n){case"O":case"OO":case"OOO":return"GMT"+wV(i,":");case"OOOO":default:return"GMT"+du(i,":")}},z:function(t,n,e){let i=t.getTimezoneOffset();switch(n){case"z":case"zz":case"zzz":return"GMT"+wV(i,":");case"zzzz":default:return"GMT"+du(i,":")}},t:function(t,n,e){let i=Math.trunc(+t/1e3);return St(i,n.length)},T:function(t,n,e){return St(+t,n.length)}};function wV(t,n=""){let e=t>0?"-":"+",i=Math.abs(t),r=Math.trunc(i/60),o=i%60;return o===0?e+String(r):e+String(r)+n+St(o,2)}function DV(t,n){return t%60===0?(t>0?"-":"+")+St(Math.abs(t)/60,2):du(t,n)}function du(t,n=""){let e=t>0?"-":"+",i=Math.abs(t),r=St(Math.trunc(i/60),2),o=St(i%60,2);return e+r+n+o}var MV=(t,n)=>{switch(t){case"P":return n.date({width:"short"});case"PP":return n.date({width:"medium"});case"PPP":return n.date({width:"long"});case"PPPP":default:return n.date({width:"full"})}},EV=(t,n)=>{switch(t){case"p":return n.time({width:"short"});case"pp":return n.time({width:"medium"});case"ppp":return n.time({width:"long"});case"pppp":default:return n.time({width:"full"})}},_X=(t,n)=>{let e=t.match(/(P+)(p+)?/)||[],i=e[1],r=e[2];if(!r)return MV(t,n);let o;switch(i){case"P":o=n.dateTime({width:"short"});break;case"PP":o=n.dateTime({width:"medium"});break;case"PPP":o=n.dateTime({width:"long"});break;case"PPPP":default:o=n.dateTime({width:"full"});break}return o.replace("{{date}}",MV(i,n)).replace("{{time}}",EV(r,n))},ng={p:EV,P:_X};var bX=/^D+$/,vX=/^Y+$/,yX=["D","DD","YY","YYYY"];function cy(t){return bX.test(t)}function dy(t){return vX.test(t)}function rg(t,n,e){let i=xX(t,n,e);if(console.warn(i),yX.includes(t))throw new RangeError(i)}function xX(t,n,e){let i=t[0]==="Y"?"years":"days of the month";return`Use \`${t.toLowerCase()}\` instead of \`${t}\` (in \`${n}\`) for formatting ${i} to the input \`${e}\`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md`}var CX=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,wX=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,DX=/^'([^]*?)'?$/,MX=/''/g,EX=/[a-zA-Z]/;function SV(t,n,e){let i=ta(),r=e?.locale??i.locale??cu,o=e?.firstWeekContainsDate??e?.locale?.options?.firstWeekContainsDate??i.firstWeekContainsDate??i.locale?.options?.firstWeekContainsDate??1,a=e?.weekStartsOn??e?.locale?.options?.weekStartsOn??i.weekStartsOn??i.locale?.options?.weekStartsOn??0,s=Ie(t,e?.in);if(!mh(s))throw new RangeError("Invalid time value");let l=n.match(wX).map(d=>{let p=d[0];if(p==="p"||p==="P"){let _=ng[p];return _(d,r.formatLong)}return d}).join("").match(CX).map(d=>{if(d==="''")return{isToken:!1,value:"'"};let p=d[0];if(p==="'")return{isToken:!1,value:SX(d)};if(HS[p])return{isToken:!0,value:d};if(p.match(EX))throw new RangeError("Format string contains an unescaped latin alphabet character `"+p+"`");return{isToken:!1,value:d}});r.localize.preprocessor&&(l=r.localize.preprocessor(s,l));let c={firstWeekContainsDate:o,weekStartsOn:a,locale:r};return l.map(d=>{if(!d.isToken)return d.value;let p=d.value;(!e?.useAdditionalWeekYearTokens&&dy(p)||!e?.useAdditionalDayOfYearTokens&&cy(p))&&rg(p,n,String(t));let _=HS[p[0]];return _(s,p,r.localize,c)}).join("")}function SX(t){let n=t.match(DX);return n?n[1].replace(MX,"'"):t}function kV(t,n){let e=Ie(t,n?.in);if(isNaN(+e))throw new RangeError("Invalid time value");let i=n?.format??"extended",r=n?.representation??"complete",o="",a="",s=i==="extended"?"-":"",l=i==="extended"?":":"";if(r!=="time"){let c=St(e.getDate(),2),d=St(e.getMonth()+1,2);o=`${St(e.getFullYear(),4)}${s}${d}${s}${c}`}if(r!=="date"){let c=e.getTimezoneOffset();if(c!==0){let w=Math.abs(c),C=St(Math.trunc(w/60),2),D=St(w%60,2);a=`${c<0?"+":"-"}${C}:${D}`}else a="Z";let d=St(e.getHours(),2),p=St(e.getMinutes(),2),_=St(e.getSeconds(),2),b=o===""?"":"T",y=[d,p,_].join(l);o=`${o}${b}${y}${a}`}return o}function TV(t,n){return Ie(t,n?.in).getDate()}function IV(t,n){return Ie(t,n?.in).getDay()}function uy(t,n){let e=Ie(t,n?.in),i=e.getFullYear(),r=e.getMonth(),o=ct(e,0);return o.setFullYear(i,r+1,0),o.setHours(0,0,0,0),o.getDate()}function AV(){return Object.assign({},ta())}function my(t,n){return Ie(t,n?.in).getHours()}function OV(t,n){let e=Ie(t,n?.in).getDay();return e===0?7:e}function hy(t,n){return Ie(t,n?.in).getMinutes()}function RV(t,n){return Ie(t,n?.in).getMonth()}function PV(t){return Ie(t).getSeconds()}function FV(t,n){return Ie(t,n?.in).getFullYear()}function NV(t,n){let e=kX(n)?new n(0):ct(n,0);return e.setFullYear(t.getFullYear(),t.getMonth(),t.getDate()),e.setHours(t.getHours(),t.getMinutes(),t.getSeconds(),t.getMilliseconds()),e}function kX(t){return typeof t=="function"&&t.prototype?.constructor===t}var TX=10,py=class{subPriority=0;validate(n,e){return!0}},fy=class extends py{constructor(n,e,i,r,o){super(),this.value=n,this.validateValue=e,this.setValue=i,this.priority=r,o&&(this.subPriority=o)}validate(n,e){return this.validateValue(n,this.value,e)}set(n,e,i){return this.setValue(n,e,this.value,i)}},gy=class extends py{priority=TX;subPriority=-1;constructor(n,e){super(),this.context=n||(i=>ct(e,i))}set(n,e){return e.timestampIsSet?n:ct(n,NV(n,this.context))}};var Pe=class{run(n,e,i,r){let o=this.parse(n,e,i,r);return o?{setter:new fy(o.value,this.validate,this.set,this.priority,this.subPriority),rest:o.rest}:null}validate(n,e,i){return!0}};var _y=class extends Pe{priority=140;parse(n,e,i){switch(e){case"G":case"GG":case"GGG":return i.era(n,{width:"abbreviated"})||i.era(n,{width:"narrow"});case"GGGGG":return i.era(n,{width:"narrow"});case"GGGG":default:return i.era(n,{width:"wide"})||i.era(n,{width:"abbreviated"})||i.era(n,{width:"narrow"})}}set(n,e,i){return e.era=i,n.setFullYear(i,0,1),n.setHours(0,0,0,0),n}incompatibleTokens=["R","u","t","T"]};var Qt={month:/^(1[0-2]|0?\d)/,date:/^(3[0-1]|[0-2]?\d)/,dayOfYear:/^(36[0-6]|3[0-5]\d|[0-2]?\d?\d)/,week:/^(5[0-3]|[0-4]?\d)/,hour23h:/^(2[0-3]|[0-1]?\d)/,hour24h:/^(2[0-4]|[0-1]?\d)/,hour11h:/^(1[0-1]|0?\d)/,hour12h:/^(1[0-2]|0?\d)/,minute:/^[0-5]?\d/,second:/^[0-5]?\d/,singleDigit:/^\d/,twoDigits:/^\d{1,2}/,threeDigits:/^\d{1,3}/,fourDigits:/^\d{1,4}/,anyDigitsSigned:/^-?\d+/,singleDigitSigned:/^-?\d/,twoDigitsSigned:/^-?\d{1,2}/,threeDigitsSigned:/^-?\d{1,3}/,fourDigitsSigned:/^-?\d{1,4}/},na={basicOptionalMinutes:/^([+-])(\d{2})(\d{2})?|Z/,basic:/^([+-])(\d{2})(\d{2})|Z/,basicOptionalSeconds:/^([+-])(\d{2})(\d{2})((\d{2}))?|Z/,extended:/^([+-])(\d{2}):(\d{2})|Z/,extendedOptionalSeconds:/^([+-])(\d{2}):(\d{2})(:(\d{2}))?|Z/};function fi(t,n){return t&&{value:n(t.value),rest:t.rest}}function qt(t,n){let e=n.match(t);return e?{value:parseInt(e[0],10),rest:n.slice(e[0].length)}:null}function ra(t,n){let e=n.match(t);if(!e)return null;if(e[0]==="Z")return{value:0,rest:n.slice(1)};let i=e[1]==="+"?1:-1,r=e[2]?parseInt(e[2],10):0,o=e[3]?parseInt(e[3],10):0,a=e[5]?parseInt(e[5],10):0;return{value:i*(r*su+o*au+a*aV),rest:n.slice(e[0].length)}}function by(t){return qt(Qt.anyDigitsSigned,t)}function xt(t,n){switch(t){case 1:return qt(Qt.singleDigit,n);case 2:return qt(Qt.twoDigits,n);case 3:return qt(Qt.threeDigits,n);case 4:return qt(Qt.fourDigits,n);default:return qt(new RegExp("^\\d{1,"+t+"}"),n)}}function _h(t,n){switch(t){case 1:return qt(Qt.singleDigitSigned,n);case 2:return qt(Qt.twoDigitsSigned,n);case 3:return qt(Qt.threeDigitsSigned,n);case 4:return qt(Qt.fourDigitsSigned,n);default:return qt(new RegExp("^-?\\d{1,"+t+"}"),n)}}function bh(t){switch(t){case"morning":return 4;case"evening":return 17;case"pm":case"noon":case"afternoon":return 12;case"am":case"midnight":case"night":default:return 0}}function vy(t,n){let e=n>0,i=e?n:1-n,r;if(i<=50)r=t||100;else{let o=i+50,a=Math.trunc(o/100)*100,s=t>=o%100;r=t+a-(s?100:0)}return e?r:1-r}function yy(t){return t%400===0||t%4===0&&t%100!==0}var xy=class extends Pe{priority=130;incompatibleTokens=["Y","R","u","w","I","i","e","c","t","T"];parse(n,e,i){let r=o=>({year:o,isTwoDigitYear:e==="yy"});switch(e){case"y":return fi(xt(4,n),r);case"yo":return fi(i.ordinalNumber(n,{unit:"year"}),r);default:return fi(xt(e.length,n),r)}}validate(n,e){return e.isTwoDigitYear||e.year>0}set(n,e,i){let r=n.getFullYear();if(i.isTwoDigitYear){let a=vy(i.year,r);return n.setFullYear(a,0,1),n.setHours(0,0,0,0),n}let o=!("era"in e)||e.era===1?i.year:1-i.year;return n.setFullYear(o,0,1),n.setHours(0,0,0,0),n}};var Cy=class extends Pe{priority=130;parse(n,e,i){let r=o=>({year:o,isTwoDigitYear:e==="YY"});switch(e){case"Y":return fi(xt(4,n),r);case"Yo":return fi(i.ordinalNumber(n,{unit:"year"}),r);default:return fi(xt(e.length,n),r)}}validate(n,e){return e.isTwoDigitYear||e.year>0}set(n,e,i,r){let o=ph(n,r);if(i.isTwoDigitYear){let s=vy(i.year,o);return n.setFullYear(s,0,r.firstWeekContainsDate),n.setHours(0,0,0,0),ar(n,r)}let a=!("era"in e)||e.era===1?i.year:1-i.year;return n.setFullYear(a,0,r.firstWeekContainsDate),n.setHours(0,0,0,0),ar(n,r)}incompatibleTokens=["y","R","u","Q","q","M","L","I","d","D","i","t","T"]};var wy=class extends Pe{priority=130;parse(n,e){return e==="R"?_h(4,n):_h(e.length,n)}set(n,e,i){let r=ct(n,0);return r.setFullYear(i,0,4),r.setHours(0,0,0,0),Qa(r)}incompatibleTokens=["G","y","Y","u","Q","q","M","L","w","d","D","e","c","t","T"]};var Dy=class extends Pe{priority=130;parse(n,e){return e==="u"?_h(4,n):_h(e.length,n)}set(n,e,i){return n.setFullYear(i,0,1),n.setHours(0,0,0,0),n}incompatibleTokens=["G","y","Y","R","w","I","i","e","c","t","T"]};var My=class extends Pe{priority=120;parse(n,e,i){switch(e){case"Q":case"QQ":return xt(e.length,n);case"Qo":return i.ordinalNumber(n,{unit:"quarter"});case"QQQ":return i.quarter(n,{width:"abbreviated",context:"formatting"})||i.quarter(n,{width:"narrow",context:"formatting"});case"QQQQQ":return i.quarter(n,{width:"narrow",context:"formatting"});case"QQQQ":default:return i.quarter(n,{width:"wide",context:"formatting"})||i.quarter(n,{width:"abbreviated",context:"formatting"})||i.quarter(n,{width:"narrow",context:"formatting"})}}validate(n,e){return e>=1&&e<=4}set(n,e,i){return n.setMonth((i-1)*3,1),n.setHours(0,0,0,0),n}incompatibleTokens=["Y","R","q","M","L","w","I","d","D","i","e","c","t","T"]};var Ey=class extends Pe{priority=120;parse(n,e,i){switch(e){case"q":case"qq":return xt(e.length,n);case"qo":return i.ordinalNumber(n,{unit:"quarter"});case"qqq":return i.quarter(n,{width:"abbreviated",context:"standalone"})||i.quarter(n,{width:"narrow",context:"standalone"});case"qqqqq":return i.quarter(n,{width:"narrow",context:"standalone"});case"qqqq":default:return i.quarter(n,{width:"wide",context:"standalone"})||i.quarter(n,{width:"abbreviated",context:"standalone"})||i.quarter(n,{width:"narrow",context:"standalone"})}}validate(n,e){return e>=1&&e<=4}set(n,e,i){return n.setMonth((i-1)*3,1),n.setHours(0,0,0,0),n}incompatibleTokens=["Y","R","Q","M","L","w","I","d","D","i","e","c","t","T"]};var Sy=class extends Pe{incompatibleTokens=["Y","R","q","Q","L","w","I","D","i","e","c","t","T"];priority=110;parse(n,e,i){let r=o=>o-1;switch(e){case"M":return fi(qt(Qt.month,n),r);case"MM":return fi(xt(2,n),r);case"Mo":return fi(i.ordinalNumber(n,{unit:"month"}),r);case"MMM":return i.month(n,{width:"abbreviated",context:"formatting"})||i.month(n,{width:"narrow",context:"formatting"});case"MMMMM":return i.month(n,{width:"narrow",context:"formatting"});case"MMMM":default:return i.month(n,{width:"wide",context:"formatting"})||i.month(n,{width:"abbreviated",context:"formatting"})||i.month(n,{width:"narrow",context:"formatting"})}}validate(n,e){return e>=0&&e<=11}set(n,e,i){return n.setMonth(i,1),n.setHours(0,0,0,0),n}};var ky=class extends Pe{priority=110;parse(n,e,i){let r=o=>o-1;switch(e){case"L":return fi(qt(Qt.month,n),r);case"LL":return fi(xt(2,n),r);case"Lo":return fi(i.ordinalNumber(n,{unit:"month"}),r);case"LLL":return i.month(n,{width:"abbreviated",context:"standalone"})||i.month(n,{width:"narrow",context:"standalone"});case"LLLLL":return i.month(n,{width:"narrow",context:"standalone"});case"LLLL":default:return i.month(n,{width:"wide",context:"standalone"})||i.month(n,{width:"abbreviated",context:"standalone"})||i.month(n,{width:"narrow",context:"standalone"})}}validate(n,e){return e>=0&&e<=11}set(n,e,i){return n.setMonth(i,1),n.setHours(0,0,0,0),n}incompatibleTokens=["Y","R","q","Q","M","w","I","D","i","e","c","t","T"]};function LV(t,n,e){let i=Ie(t,e?.in),r=fh(i,e)-n;return i.setDate(i.getDate()-r*7),Ie(i,e?.in)}var Ty=class extends Pe{priority=100;parse(n,e,i){switch(e){case"w":return qt(Qt.week,n);case"wo":return i.ordinalNumber(n,{unit:"week"});default:return xt(e.length,n)}}validate(n,e){return e>=1&&e<=53}set(n,e,i,r){return ar(LV(n,i,r),r)}incompatibleTokens=["y","R","u","q","Q","M","L","I","d","D","i","t","T"]};function VV(t,n,e){let i=Ie(t,e?.in),r=ly(i,e)-n;return i.setDate(i.getDate()-r*7),i}var Iy=class extends Pe{priority=100;parse(n,e,i){switch(e){case"I":return qt(Qt.week,n);case"Io":return i.ordinalNumber(n,{unit:"week"});default:return xt(e.length,n)}}validate(n,e){return e>=1&&e<=53}set(n,e,i){return Qa(VV(n,i))}incompatibleTokens=["y","Y","u","q","Q","M","L","w","d","D","e","c","t","T"]};var IX=[31,28,31,30,31,30,31,31,30,31,30,31],AX=[31,29,31,30,31,30,31,31,30,31,30,31],Ay=class extends Pe{priority=90;subPriority=1;parse(n,e,i){switch(e){case"d":return qt(Qt.date,n);case"do":return i.ordinalNumber(n,{unit:"date"});default:return xt(e.length,n)}}validate(n,e){let i=n.getFullYear(),r=yy(i),o=n.getMonth();return r?e>=1&&e<=AX[o]:e>=1&&e<=IX[o]}set(n,e,i){return n.setDate(i),n.setHours(0,0,0,0),n}incompatibleTokens=["Y","R","q","Q","w","I","D","i","e","c","t","T"]};var Oy=class extends Pe{priority=90;subpriority=1;parse(n,e,i){switch(e){case"D":case"DD":return qt(Qt.dayOfYear,n);case"Do":return i.ordinalNumber(n,{unit:"date"});default:return xt(e.length,n)}}validate(n,e){let i=n.getFullYear();return yy(i)?e>=1&&e<=366:e>=1&&e<=365}set(n,e,i){return n.setMonth(0,i),n.setHours(0,0,0,0),n}incompatibleTokens=["Y","R","q","Q","M","L","w","I","d","E","i","e","c","t","T"]};function vh(t,n,e){let i=ta(),r=e?.weekStartsOn??e?.locale?.options?.weekStartsOn??i.weekStartsOn??i.locale?.options?.weekStartsOn??0,o=Ie(t,e?.in),a=o.getDay(),l=(n%7+7)%7,c=7-r,d=n<0||n>6?n-(a+c)%7:(l+c)%7-(a+c)%7;return dh(o,d,e)}var Ry=class extends Pe{priority=90;parse(n,e,i){switch(e){case"E":case"EE":case"EEE":return i.day(n,{width:"abbreviated",context:"formatting"})||i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"});case"EEEEE":return i.day(n,{width:"narrow",context:"formatting"});case"EEEEEE":return i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"});case"EEEE":default:return i.day(n,{width:"wide",context:"formatting"})||i.day(n,{width:"abbreviated",context:"formatting"})||i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"})}}validate(n,e){return e>=0&&e<=6}set(n,e,i,r){return n=vh(n,i,r),n.setHours(0,0,0,0),n}incompatibleTokens=["D","i","e","c","t","T"]};var Py=class extends Pe{priority=90;parse(n,e,i,r){let o=a=>{let s=Math.floor((a-1)/7)*7;return(a+r.weekStartsOn+6)%7+s};switch(e){case"e":case"ee":return fi(xt(e.length,n),o);case"eo":return fi(i.ordinalNumber(n,{unit:"day"}),o);case"eee":return i.day(n,{width:"abbreviated",context:"formatting"})||i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"});case"eeeee":return i.day(n,{width:"narrow",context:"formatting"});case"eeeeee":return i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"});case"eeee":default:return i.day(n,{width:"wide",context:"formatting"})||i.day(n,{width:"abbreviated",context:"formatting"})||i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"})}}validate(n,e){return e>=0&&e<=6}set(n,e,i,r){return n=vh(n,i,r),n.setHours(0,0,0,0),n}incompatibleTokens=["y","R","u","q","Q","M","L","I","d","D","E","i","c","t","T"]};var Fy=class extends Pe{priority=90;parse(n,e,i,r){let o=a=>{let s=Math.floor((a-1)/7)*7;return(a+r.weekStartsOn+6)%7+s};switch(e){case"c":case"cc":return fi(xt(e.length,n),o);case"co":return fi(i.ordinalNumber(n,{unit:"day"}),o);case"ccc":return i.day(n,{width:"abbreviated",context:"standalone"})||i.day(n,{width:"short",context:"standalone"})||i.day(n,{width:"narrow",context:"standalone"});case"ccccc":return i.day(n,{width:"narrow",context:"standalone"});case"cccccc":return i.day(n,{width:"short",context:"standalone"})||i.day(n,{width:"narrow",context:"standalone"});case"cccc":default:return i.day(n,{width:"wide",context:"standalone"})||i.day(n,{width:"abbreviated",context:"standalone"})||i.day(n,{width:"short",context:"standalone"})||i.day(n,{width:"narrow",context:"standalone"})}}validate(n,e){return e>=0&&e<=6}set(n,e,i,r){return n=vh(n,i,r),n.setHours(0,0,0,0),n}incompatibleTokens=["y","R","u","q","Q","M","L","I","d","D","E","i","e","t","T"]};function BV(t,n,e){let i=Ie(t,e?.in),r=OV(i,e),o=n-r;return dh(i,o,e)}var Ny=class extends Pe{priority=90;parse(n,e,i){let r=o=>o===0?7:o;switch(e){case"i":case"ii":return xt(e.length,n);case"io":return i.ordinalNumber(n,{unit:"day"});case"iii":return fi(i.day(n,{width:"abbreviated",context:"formatting"})||i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"}),r);case"iiiii":return fi(i.day(n,{width:"narrow",context:"formatting"}),r);case"iiiiii":return fi(i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"}),r);case"iiii":default:return fi(i.day(n,{width:"wide",context:"formatting"})||i.day(n,{width:"abbreviated",context:"formatting"})||i.day(n,{width:"short",context:"formatting"})||i.day(n,{width:"narrow",context:"formatting"}),r)}}validate(n,e){return e>=1&&e<=7}set(n,e,i){return n=BV(n,i),n.setHours(0,0,0,0),n}incompatibleTokens=["y","Y","u","q","Q","M","L","w","d","D","E","e","c","t","T"]};var Ly=class extends Pe{priority=80;parse(n,e,i){switch(e){case"a":case"aa":case"aaa":return i.dayPeriod(n,{width:"abbreviated",context:"formatting"})||i.dayPeriod(n,{width:"narrow",context:"formatting"});case"aaaaa":return i.dayPeriod(n,{width:"narrow",context:"formatting"});case"aaaa":default:return i.dayPeriod(n,{width:"wide",context:"formatting"})||i.dayPeriod(n,{width:"abbreviated",context:"formatting"})||i.dayPeriod(n,{width:"narrow",context:"formatting"})}}set(n,e,i){return n.setHours(bh(i),0,0,0),n}incompatibleTokens=["b","B","H","k","t","T"]};var Vy=class extends Pe{priority=80;parse(n,e,i){switch(e){case"b":case"bb":case"bbb":return i.dayPeriod(n,{width:"abbreviated",context:"formatting"})||i.dayPeriod(n,{width:"narrow",context:"formatting"});case"bbbbb":return i.dayPeriod(n,{width:"narrow",context:"formatting"});case"bbbb":default:return i.dayPeriod(n,{width:"wide",context:"formatting"})||i.dayPeriod(n,{width:"abbreviated",context:"formatting"})||i.dayPeriod(n,{width:"narrow",context:"formatting"})}}set(n,e,i){return n.setHours(bh(i),0,0,0),n}incompatibleTokens=["a","B","H","k","t","T"]};var By=class extends Pe{priority=80;parse(n,e,i){switch(e){case"B":case"BB":case"BBB":return i.dayPeriod(n,{width:"abbreviated",context:"formatting"})||i.dayPeriod(n,{width:"narrow",context:"formatting"});case"BBBBB":return i.dayPeriod(n,{width:"narrow",context:"formatting"});case"BBBB":default:return i.dayPeriod(n,{width:"wide",context:"formatting"})||i.dayPeriod(n,{width:"abbreviated",context:"formatting"})||i.dayPeriod(n,{width:"narrow",context:"formatting"})}}set(n,e,i){return n.setHours(bh(i),0,0,0),n}incompatibleTokens=["a","b","t","T"]};var jy=class extends Pe{priority=70;parse(n,e,i){switch(e){case"h":return qt(Qt.hour12h,n);case"ho":return i.ordinalNumber(n,{unit:"hour"});default:return xt(e.length,n)}}validate(n,e){return e>=1&&e<=12}set(n,e,i){let r=n.getHours()>=12;return r&&i<12?n.setHours(i+12,0,0,0):!r&&i===12?n.setHours(0,0,0,0):n.setHours(i,0,0,0),n}incompatibleTokens=["H","K","k","t","T"]};var Hy=class extends Pe{priority=70;parse(n,e,i){switch(e){case"H":return qt(Qt.hour23h,n);case"Ho":return i.ordinalNumber(n,{unit:"hour"});default:return xt(e.length,n)}}validate(n,e){return e>=0&&e<=23}set(n,e,i){return n.setHours(i,0,0,0),n}incompatibleTokens=["a","b","h","K","k","t","T"]};var zy=class extends Pe{priority=70;parse(n,e,i){switch(e){case"K":return qt(Qt.hour11h,n);case"Ko":return i.ordinalNumber(n,{unit:"hour"});default:return xt(e.length,n)}}validate(n,e){return e>=0&&e<=11}set(n,e,i){return n.getHours()>=12&&i<12?n.setHours(i+12,0,0,0):n.setHours(i,0,0,0),n}incompatibleTokens=["h","H","k","t","T"]};var Uy=class extends Pe{priority=70;parse(n,e,i){switch(e){case"k":return qt(Qt.hour24h,n);case"ko":return i.ordinalNumber(n,{unit:"hour"});default:return xt(e.length,n)}}validate(n,e){return e>=1&&e<=24}set(n,e,i){let r=i<=24?i%24:i;return n.setHours(r,0,0,0),n}incompatibleTokens=["a","b","h","H","K","t","T"]};var $y=class extends Pe{priority=60;parse(n,e,i){switch(e){case"m":return qt(Qt.minute,n);case"mo":return i.ordinalNumber(n,{unit:"minute"});default:return xt(e.length,n)}}validate(n,e){return e>=0&&e<=59}set(n,e,i){return n.setMinutes(i,0,0),n}incompatibleTokens=["t","T"]};var Wy=class extends Pe{priority=50;parse(n,e,i){switch(e){case"s":return qt(Qt.second,n);case"so":return i.ordinalNumber(n,{unit:"second"});default:return xt(e.length,n)}}validate(n,e){return e>=0&&e<=59}set(n,e,i){return n.setSeconds(i,0),n}incompatibleTokens=["t","T"]};var Gy=class extends Pe{priority=30;parse(n,e){let i=r=>Math.trunc(r*Math.pow(10,-e.length+3));return fi(xt(e.length,n),i)}set(n,e,i){return n.setMilliseconds(i),n}incompatibleTokens=["t","T"]};var qy=class extends Pe{priority=10;parse(n,e){switch(e){case"X":return ra(na.basicOptionalMinutes,n);case"XX":return ra(na.basic,n);case"XXXX":return ra(na.basicOptionalSeconds,n);case"XXXXX":return ra(na.extendedOptionalSeconds,n);case"XXX":default:return ra(na.extended,n)}}set(n,e,i){return e.timestampIsSet?n:ct(n,n.getTime()-lu(n)-i)}incompatibleTokens=["t","T","x"]};var Yy=class extends Pe{priority=10;parse(n,e){switch(e){case"x":return ra(na.basicOptionalMinutes,n);case"xx":return ra(na.basic,n);case"xxxx":return ra(na.basicOptionalSeconds,n);case"xxxxx":return ra(na.extendedOptionalSeconds,n);case"xxx":default:return ra(na.extended,n)}}set(n,e,i){return e.timestampIsSet?n:ct(n,n.getTime()-lu(n)-i)}incompatibleTokens=["t","T","X"]};var Qy=class extends Pe{priority=40;parse(n){return by(n)}set(n,e,i){return[ct(n,i*1e3),{timestampIsSet:!0}]}incompatibleTokens="*"};var Ky=class extends Pe{priority=20;parse(n){return by(n)}set(n,e,i){return[ct(n,i),{timestampIsSet:!0}]}incompatibleTokens="*"};var jV={G:new _y,y:new xy,Y:new Cy,R:new wy,u:new Dy,Q:new My,q:new Ey,M:new Sy,L:new ky,w:new Ty,I:new Iy,d:new Ay,D:new Oy,E:new Ry,e:new Py,c:new Fy,i:new Ny,a:new Ly,b:new Vy,B:new By,h:new jy,H:new Hy,K:new zy,k:new Uy,m:new $y,s:new Wy,S:new Gy,X:new qy,x:new Yy,t:new Qy,T:new Ky};var OX=/[yYQqMLwIdDecihHKkms]o|(\w)\1*|''|'(''|[^'])+('|$)|./g,RX=/P+p+|P+|p+|''|'(''|[^'])+('|$)|./g,PX=/^'([^]*?)'?$/,FX=/''/g,NX=/\S/,LX=/[a-zA-Z]/;function HV(t,n,e,i){let r=()=>ct(i?.in||e,NaN),o=AV(),a=i?.locale??o.locale??cu,s=i?.firstWeekContainsDate??i?.locale?.options?.firstWeekContainsDate??o.firstWeekContainsDate??o.locale?.options?.firstWeekContainsDate??1,l=i?.weekStartsOn??i?.locale?.options?.weekStartsOn??o.weekStartsOn??o.locale?.options?.weekStartsOn??0;if(!n)return t?r():Ie(e,i?.in);let c={firstWeekContainsDate:s,weekStartsOn:l,locale:a},d=[new gy(i?.in,e)],p=n.match(RX).map(C=>{let D=C[0];if(D in ng){let F=ng[D];return F(C,a.formatLong)}return C}).join("").match(OX),_=[];for(let C of p){!i?.useAdditionalWeekYearTokens&&dy(C)&&rg(C,n,t),!i?.useAdditionalDayOfYearTokens&&cy(C)&&rg(C,n,t);let D=C[0],F=jV[D];if(F){let{incompatibleTokens:W}=F;if(Array.isArray(W)){let K=_.find(oe=>W.includes(oe.token)||oe.token===D);if(K)throw new RangeError(`The format string mustn't contain \`${K.fullToken}\` and \`${C}\` at the same time`)}else if(F.incompatibleTokens==="*"&&_.length>0)throw new RangeError(`The format string mustn't contain \`${C}\` and any other token at the same time`);_.push({token:D,fullToken:C});let Z=F.run(t,C,a.match,c);if(!Z)return r();d.push(Z.setter),t=Z.rest}else{if(D.match(LX))throw new RangeError("Format string contains an unescaped latin alphabet character `"+D+"`");if(C==="''"?C="'":D==="'"&&(C=VX(C)),t.indexOf(C)===0)t=t.slice(C.length);else return r()}}if(t.length>0&&NX.test(t))return r();let b=d.map(C=>C.priority).sort((C,D)=>D-C).filter((C,D,F)=>F.indexOf(C)===D).map(C=>d.filter(D=>D.priority===C).sort((D,F)=>F.subPriority-D.subPriority)).map(C=>C[0]),y=Ie(e,i?.in);if(isNaN(+y))return r();let w={};for(let C of b){if(!C.validate(y,c))return r();let D=C.set(y,w,c);Array.isArray(D)?(y=D[0],Object.assign(w,D[1])):y=D}return y}function VX(t){return t.match(PX)[1].replace(FX,"'")}function zV(t,n,e){let[i,r]=ay(e?.in,t,n);return+ar(i,e)==+ar(r,e)}function US(t,n){let e=()=>ct(n?.in,NaN),i=n?.additionalDigits??2,r=zX(t),o;if(r.date){let c=UX(r.date,i);o=$X(c.restDateString,c.year)}if(!o||isNaN(+o))return e();let a=+o,s=0,l;if(r.time&&(s=WX(r.time),isNaN(s)))return e();if(r.timezone){if(l=GX(r.timezone),isNaN(l))return e()}else{let c=new Date(a+s),d=Ie(0,n?.in);return d.setFullYear(c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()),d.setHours(c.getUTCHours(),c.getUTCMinutes(),c.getUTCSeconds(),c.getUTCMilliseconds()),d}return Ie(a+s+l,n?.in)}var Zy={dateTimeDelimiter:/[T ]/,timeZoneDelimiter:/[Z ]/i,timezone:/([Z+-].*)$/},BX=/^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/,jX=/^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/,HX=/^([+-])(\d{2})(?::?(\d{2}))?$/;function zX(t){let n={},e=t.split(Zy.dateTimeDelimiter),i;if(e.length>2)return n;if(/:/.test(e[0])?i=e[0]:(n.date=e[0],i=e[1],Zy.timeZoneDelimiter.test(n.date)&&(n.date=t.split(Zy.timeZoneDelimiter)[0],i=t.substr(n.date.length,t.length))),i){let r=Zy.timezone.exec(i);r?(n.time=i.replace(r[1],""),n.timezone=r[1]):n.time=i}return n}function UX(t,n){let e=new RegExp("^(?:(\\d{4}|[+-]\\d{"+(4+n)+"})|(\\d{2}|[+-]\\d{"+(2+n)+"})$)"),i=t.match(e);if(!i)return{year:NaN,restDateString:""};let r=i[1]?parseInt(i[1]):null,o=i[2]?parseInt(i[2]):null;return{year:o===null?r:o*100,restDateString:t.slice((i[1]||i[2]).length)}}function $X(t,n){if(n===null)return new Date(NaN);let e=t.match(BX);if(!e)return new Date(NaN);let i=!!e[4],r=og(e[1]),o=og(e[2])-1,a=og(e[3]),s=og(e[4]),l=og(e[5])-1;if(i)return ZX(n,s,l)?qX(n,s,l):new Date(NaN);{let c=new Date(0);return!QX(n,o,a)||!KX(n,r)?new Date(NaN):(c.setUTCFullYear(n,o,Math.max(r,a)),c)}}function og(t){return t?parseInt(t):1}function WX(t){let n=t.match(jX);if(!n)return NaN;let e=zS(n[1]),i=zS(n[2]),r=zS(n[3]);return XX(e,i,r)?e*su+i*au+r*1e3:NaN}function zS(t){return t&&parseFloat(t.replace(",","."))||0}function GX(t){if(t==="Z")return 0;let n=t.match(HX);if(!n)return 0;let e=n[1]==="+"?-1:1,i=parseInt(n[2]),r=n[3]&&parseInt(n[3])||0;return JX(i,r)?e*(i*su+r*au):NaN}function qX(t,n,e){let i=new Date(0);i.setUTCFullYear(t,0,4);let r=i.getUTCDay()||7,o=(n-1)*7+e+1-r;return i.setUTCDate(i.getUTCDate()+o),i}var YX=[31,null,31,30,31,30,31,31,30,31,30,31];function UV(t){return t%400===0||t%4===0&&t%100!==0}function QX(t,n,e){return n>=0&&n<=11&&e>=1&&e<=(YX[n]||(UV(t)?29:28))}function KX(t,n){return n>=1&&n<=(UV(t)?366:365)}function ZX(t,n,e){return n>=1&&n<=53&&e>=0&&e<=6}function XX(t,n,e){return t===24?n===0&&e===0:e>=0&&e<60&&n>=0&&n<60&&t>=0&&t<25}function JX(t,n){return n>=0&&n<=59}function $V(t,n,e){let i=Ie(t,e?.in),r=i.getFullYear(),o=i.getDate(),a=ct(e?.in||t,0);a.setFullYear(r,n,15),a.setHours(0,0,0,0);let s=uy(a);return i.setMonth(n,Math.min(o,s)),i}function WV(t,n,e){let i=Ie(t,e?.in);return isNaN(+i)?ct(e?.in||t,NaN):(n.year!=null&&i.setFullYear(n.year),n.month!=null&&(i=$V(i,n.month)),n.date!=null&&i.setDate(n.date),n.hours!=null&&i.setHours(n.hours),n.minutes!=null&&i.setMinutes(n.minutes),n.seconds!=null&&i.setSeconds(n.seconds),n.milliseconds!=null&&i.setMilliseconds(n.milliseconds),i)}function $S(t,n){let e=Array(t);for(let i=0;i{class t extends $i{constructor(){super();let e=u(Ls,{optional:!0});this.setLocale(e)}getYear(e){return FV(e)}getMonth(e){return RV(e)}getDate(e){return TV(e)}getDayOfWeek(e){return IV(e)}getMonthNames(e){let i=eJ[e];return $S(12,r=>this.format(new Date(2017,r,1),i))}getDateNames(){let e=typeof Intl<"u"?new Intl.DateTimeFormat(this.locale.code,{day:"numeric",timeZone:"utc"}):null;return $S(31,i=>{if(e){let r=new Date;return r.setUTCFullYear(2017,0,i+1),r.setUTCHours(0,0,0,0),e.format(r).replace(/[\u200e\u200f]/g,"")}return i+""})}getDayOfWeekNames(e){let i=tJ[e];return $S(7,r=>this.format(new Date(2017,0,r+1),i))}getYearName(e){return this.format(e,"y")}getFirstDayOfWeek(){return this.locale.options?.weekStartsOn??0}getNumDaysInMonth(e){return uy(e)}clone(e){return new Date(e.getTime())}createDate(e,i,r){let o=new Date;return o.setFullYear(e,i,r),o.setHours(0,0,0,0),o.getMonth()!=i,o}today(){return new Date}parse(e,i){return this._parse(e,i)}format(e,i){if(!this.isValid(e))throw Error("DateFnsAdapter: Cannot format invalid date.");return SV(e,i,{locale:this.locale})}addCalendarYears(e,i){return hV(e,i)}addCalendarMonths(e,i){return uh(e,i)}addCalendarDays(e,i){return dh(e,i)}toIso8601(e){return kV(e,{representation:"date"})}deserialize(e){if(typeof e=="string"){if(!e)return null;let i=US(e);if(this.isValid(i))return i}return super.deserialize(e)}isDateInstance(e){return sy(e)}isValid(e){return mh(e)}invalid(){return new Date(NaN)}setTime(e,i,r,o){return WV(this.clone(e),{hours:i,minutes:r,seconds:o,milliseconds:0})}getHours(e){return my(e)}getMinutes(e){return hy(e)}getSeconds(e){return PV(e)}parseTime(e,i){return this._parse(e,i,!1)}addSeconds(e,i){return mV(e,i)}_parse(e,i,r=!0){if(typeof e=="string"&&e.length>0){if(r){let a=US(e);if(this.isValid(a))return a}let o=Array.isArray(i)?i:[i];if(!i.length)throw Error("Formats array must not be empty.");for(let a of o){let s=HV(e,a,new Date,{locale:this.locale});if(this.isValid(s))return s}return this.invalid()}else{if(typeof e=="number")return new Date(e);if(e instanceof Date)return this.clone(e)}return null}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),iJ={parse:{dateInput:"P",timeInput:"p"},display:{dateInput:"P",timeInput:"p",monthYearLabel:"LLL uuuu",dateA11yLabel:"PP",monthYearA11yLabel:"LLLL uuuu",timeOptionLabel:"p"}};function GV(t=iJ){return[{provide:$i,useClass:WS,deps:[Ls]},{provide:Vs,useValue:t}]}var nJ=["*"];var rJ=[[["","mat-card-avatar",""],["","matCardAvatar",""]],[["mat-card-title"],["mat-card-subtitle"],["","mat-card-title",""],["","mat-card-subtitle",""],["","matCardTitle",""],["","matCardSubtitle",""]],"*"],oJ=["[mat-card-avatar], [matCardAvatar]",`mat-card-title, mat-card-subtitle, + [mat-card-title], [mat-card-subtitle], + [matCardTitle], [matCardSubtitle]`,"*"],GS=new O("MAT_CARD_CONFIG"),kt=(()=>{class t{appearance;constructor(){let e=u(GS,{optional:!0});this.appearance=e?.appearance||"raised"}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-card"]],hostAttrs:[1,"mat-mdc-card","mdc-card"],hostVars:8,hostBindings:function(i,r){i&2&&G("mat-mdc-card-outlined",r.appearance==="outlined")("mdc-card--outlined",r.appearance==="outlined")("mat-mdc-card-filled",r.appearance==="filled")("mdc-card--filled",r.appearance==="filled")},inputs:{appearance:"appearance"},exportAs:["matCard"],ngContentSelectors:nJ,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),ne(0))},styles:[`.mat-mdc-card{display:flex;flex-direction:column;box-sizing:border-box;position:relative;border-style:solid;border-width:0;background-color:var(--mat-card-elevated-container-color, var(--mat-sys-surface-container-low));border-color:var(--mat-card-elevated-container-color, var(--mat-sys-surface-container-low));border-radius:var(--mat-card-elevated-container-shape, var(--mat-sys-corner-medium));box-shadow:var(--mat-card-elevated-container-elevation, var(--mat-sys-level1))}.mat-mdc-card::after{position:absolute;top:0;left:0;width:100%;height:100%;border:solid 1px rgba(0,0,0,0);content:"";display:block;pointer-events:none;box-sizing:border-box;border-radius:var(--mat-card-elevated-container-shape, var(--mat-sys-corner-medium))}.mat-mdc-card-outlined{background-color:var(--mat-card-outlined-container-color, var(--mat-sys-surface));border-radius:var(--mat-card-outlined-container-shape, var(--mat-sys-corner-medium));border-width:var(--mat-card-outlined-outline-width, 1px);border-color:var(--mat-card-outlined-outline-color, var(--mat-sys-outline-variant));box-shadow:var(--mat-card-outlined-container-elevation, var(--mat-sys-level0))}.mat-mdc-card-outlined::after{border:none}.mat-mdc-card-filled{background-color:var(--mat-card-filled-container-color, var(--mat-sys-surface-container-highest));border-radius:var(--mat-card-filled-container-shape, var(--mat-sys-corner-medium));box-shadow:var(--mat-card-filled-container-elevation, var(--mat-sys-level0))}.mdc-card__media{position:relative;box-sizing:border-box;background-repeat:no-repeat;background-position:center;background-size:cover}.mdc-card__media::before{display:block;content:""}.mdc-card__media:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.mdc-card__media:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.mat-mdc-card-actions{display:flex;flex-direction:row;align-items:center;box-sizing:border-box;min-height:52px;padding:8px}.mat-mdc-card-title{font-family:var(--mat-card-title-text-font, var(--mat-sys-title-large-font));line-height:var(--mat-card-title-text-line-height, var(--mat-sys-title-large-line-height));font-size:var(--mat-card-title-text-size, var(--mat-sys-title-large-size));letter-spacing:var(--mat-card-title-text-tracking, var(--mat-sys-title-large-tracking));font-weight:var(--mat-card-title-text-weight, var(--mat-sys-title-large-weight))}.mat-mdc-card-subtitle{color:var(--mat-card-subtitle-text-color, var(--mat-sys-on-surface));font-family:var(--mat-card-subtitle-text-font, var(--mat-sys-title-medium-font));line-height:var(--mat-card-subtitle-text-line-height, var(--mat-sys-title-medium-line-height));font-size:var(--mat-card-subtitle-text-size, var(--mat-sys-title-medium-size));letter-spacing:var(--mat-card-subtitle-text-tracking, var(--mat-sys-title-medium-tracking));font-weight:var(--mat-card-subtitle-text-weight, var(--mat-sys-title-medium-weight))}.mat-mdc-card-title,.mat-mdc-card-subtitle{display:block;margin:0}.mat-mdc-card-avatar~.mat-mdc-card-header-text .mat-mdc-card-title,.mat-mdc-card-avatar~.mat-mdc-card-header-text .mat-mdc-card-subtitle{padding:16px 16px 0}.mat-mdc-card-header{display:flex;padding:16px 16px 0}.mat-mdc-card-content{display:block;padding:0 16px}.mat-mdc-card-content:first-child{padding-top:16px}.mat-mdc-card-content:last-child{padding-bottom:16px}.mat-mdc-card-title-group{display:flex;justify-content:space-between;width:100%}.mat-mdc-card-avatar{height:40px;width:40px;border-radius:50%;flex-shrink:0;margin-bottom:16px;object-fit:cover}.mat-mdc-card-avatar~.mat-mdc-card-header-text .mat-mdc-card-subtitle,.mat-mdc-card-avatar~.mat-mdc-card-header-text .mat-mdc-card-title{line-height:normal}.mat-mdc-card-sm-image{width:80px;height:80px}.mat-mdc-card-md-image{width:112px;height:112px}.mat-mdc-card-lg-image{width:152px;height:152px}.mat-mdc-card-xl-image{width:240px;height:240px}.mat-mdc-card-subtitle~.mat-mdc-card-title,.mat-mdc-card-title~.mat-mdc-card-subtitle,.mat-mdc-card-header .mat-mdc-card-header-text .mat-mdc-card-title,.mat-mdc-card-header .mat-mdc-card-header-text .mat-mdc-card-subtitle,.mat-mdc-card-title-group .mat-mdc-card-title,.mat-mdc-card-title-group .mat-mdc-card-subtitle{padding-top:0}.mat-mdc-card-content>:last-child:not(.mat-mdc-card-footer){margin-bottom:0}.mat-mdc-card-actions-align-end{justify-content:flex-end} +`],encapsulation:2,changeDetection:0})}return t})(),Ot=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-card-title"],["","mat-card-title",""],["","matCardTitle",""]],hostAttrs:[1,"mat-mdc-card-title"]})}return t})();var Tt=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-card-content"]],hostAttrs:[1,"mat-mdc-card-content"]})}return t})(),js=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-card-subtitle"],["","mat-card-subtitle",""],["","matCardSubtitle",""]],hostAttrs:[1,"mat-mdc-card-subtitle"]})}return t})(),Xy=(()=>{class t{align="start";static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-card-actions"]],hostAttrs:[1,"mat-mdc-card-actions","mdc-card__actions"],hostVars:2,hostBindings:function(i,r){i&2&&G("mat-mdc-card-actions-align-end",r.align==="end")},inputs:{align:"align"},exportAs:["matCardActions"]})}return t})(),Rt=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-card-header"]],hostAttrs:[1,"mat-mdc-card-header"],ngContentSelectors:oJ,decls:4,vars:0,consts:[[1,"mat-mdc-card-header-text"]],template:function(i,r){i&1&&(Ee(rJ),ne(0),gt(1,"div",0),ne(2,1),yt(),ne(3,2))},encapsulation:2,changeDetection:0})}return t})();var Hs=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","mat-card-avatar",""],["","matCardAvatar",""]],hostAttrs:[1,"mat-mdc-card-avatar"]})}return t})();var It=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,De]})}return t})();var qS=class{_box;_destroyed=new z;_resizeSubject=new z;_resizeObserver;_elementObservables=new Map;constructor(n){this._box=n,typeof ResizeObserver<"u"&&(this._resizeObserver=new ResizeObserver(e=>this._resizeSubject.next(e)))}observe(n){return this._elementObservables.has(n)||this._elementObservables.set(n,new Ne(e=>{let i=this._resizeSubject.subscribe(e);return this._resizeObserver?.observe(n,{box:this._box}),()=>{this._resizeObserver?.unobserve(n),i.unsubscribe(),this._elementObservables.delete(n)}}).pipe(ce(e=>e.some(i=>i.target===n)),vd({bufferSize:1,refCount:!0}),xe(this._destroyed))),this._elementObservables.get(n)}destroy(){this._destroyed.next(),this._destroyed.complete(),this._resizeSubject.complete(),this._elementObservables.clear()}},Jy=(()=>{class t{_cleanupErrorListener;_observers=new Map;_ngZone=u(ae);constructor(){typeof ResizeObserver<"u"}ngOnDestroy(){for(let[,e]of this._observers)e.destroy();this._observers.clear(),this._cleanupErrorListener?.()}observe(e,i){let r=i?.box||"content-box";return this._observers.has(r)||this._observers.set(r,new qS(r)),this._observers.get(r).observe(e)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var aJ=["notch"],sJ=["matFormFieldNotchedOutline",""],lJ=["*"],qV=["iconPrefixContainer"],YV=["textPrefixContainer"],QV=["iconSuffixContainer"],KV=["textSuffixContainer"],cJ=["textField"],dJ=["*",[["mat-label"]],[["","matPrefix",""],["","matIconPrefix",""]],[["","matTextPrefix",""]],[["","matTextSuffix",""]],[["","matSuffix",""],["","matIconSuffix",""]],[["mat-error"],["","matError",""]],[["mat-hint",3,"align","end"]],[["mat-hint","align","end"]]],uJ=["*","mat-label","[matPrefix], [matIconPrefix]","[matTextPrefix]","[matTextSuffix]","[matSuffix], [matIconSuffix]","mat-error, [matError]","mat-hint:not([align='end'])","mat-hint[align='end']"];function mJ(t,n){t&1&&M(0,"span",21)}function hJ(t,n){if(t&1&&(m(0,"label",20),ne(1,1),L(2,mJ,1,0,"span",21),h()),t&2){let e=x(2);v("floating",e._shouldLabelFloat())("monitorResize",e._hasOutline())("id",e._labelId),X("for",e._control.disableAutomaticLabeling?null:e._control.id),g(2),V(!e.hideRequiredMarker&&e._control.required?2:-1)}}function pJ(t,n){if(t&1&&L(0,hJ,3,5,"label",20),t&2){let e=x();V(e._hasFloatingLabel()?0:-1)}}function fJ(t,n){t&1&&M(0,"div",7)}function gJ(t,n){}function _J(t,n){if(t&1&&A(0,gJ,0,0,"ng-template",13),t&2){x(2);let e=Te(1);v("ngTemplateOutlet",e)}}function bJ(t,n){if(t&1&&(m(0,"div",9),L(1,_J,1,1,null,13),h()),t&2){let e=x();v("matFormFieldNotchedOutlineOpen",e._shouldLabelFloat()),g(),V(e._forceDisplayInfixLabel()?-1:1)}}function vJ(t,n){t&1&&(m(0,"div",10,2),ne(2,2),h())}function yJ(t,n){t&1&&(m(0,"div",11,3),ne(2,3),h())}function xJ(t,n){}function CJ(t,n){if(t&1&&A(0,xJ,0,0,"ng-template",13),t&2){x();let e=Te(1);v("ngTemplateOutlet",e)}}function wJ(t,n){t&1&&(m(0,"div",14,4),ne(2,4),h())}function DJ(t,n){t&1&&(m(0,"div",15,5),ne(2,5),h())}function MJ(t,n){t&1&&M(0,"div",16)}function EJ(t,n){t&1&&(m(0,"div",18),ne(1,6),h())}function SJ(t,n){if(t&1&&(m(0,"mat-hint",22),f(1),h()),t&2){let e=x(2);v("id",e._hintLabelId),g(),N(e.hintLabel)}}function kJ(t,n){if(t&1&&(m(0,"div",19),L(1,SJ,2,2,"mat-hint",22),ne(2,7),M(3,"div",23),ne(4,8),h()),t&2){let e=x();g(),V(e.hintLabel?1:-1)}}var gi=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-label"]]})}return t})(),YS=new O("MatError"),Ao=(()=>{class t{id=u(et).getId("mat-mdc-error-");constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-error"],["","matError",""]],hostAttrs:[1,"mat-mdc-form-field-error","mat-mdc-form-field-bottom-align"],hostVars:1,hostBindings:function(i,r){i&2&&pi("id",r.id)},inputs:{id:"id"},features:[we([{provide:YS,useExisting:t}])]})}return t})(),ag=(()=>{class t{align="start";id=u(et).getId("mat-mdc-hint-");static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-hint"]],hostAttrs:[1,"mat-mdc-form-field-hint","mat-mdc-form-field-bottom-align"],hostVars:4,hostBindings:function(i,r){i&2&&(pi("id",r.id),X("align",null),G("mat-mdc-form-field-hint-end",r.align==="end"))},inputs:{align:"align",id:"id"}})}return t})(),QS=new O("MatPrefix"),uu=(()=>{class t{set _isTextSelector(e){this._isText=!0}_isText=!1;static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matPrefix",""],["","matIconPrefix",""],["","matTextPrefix",""]],inputs:{_isTextSelector:[0,"matTextPrefix","_isTextSelector"]},features:[we([{provide:QS,useExisting:t}])]})}return t})(),KS=new O("MatSuffix"),Ka=(()=>{class t{set _isTextSelector(e){this._isText=!0}_isText=!1;static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matSuffix",""],["","matIconSuffix",""],["","matTextSuffix",""]],inputs:{_isTextSelector:[0,"matTextSuffix","_isTextSelector"]},features:[we([{provide:KS,useExisting:t}])]})}return t})(),nB=new O("FloatingLabelParent"),ZV=(()=>{class t{_elementRef=u(Y);get floating(){return this._floating}set floating(e){this._floating=e,this.monitorResize&&this._handleResize()}_floating=!1;get monitorResize(){return this._monitorResize}set monitorResize(e){this._monitorResize=e,this._monitorResize?this._subscribeToResize():this._resizeSubscription.unsubscribe()}_monitorResize=!1;_resizeObserver=u(Jy);_ngZone=u(ae);_parent=u(nB);_resizeSubscription=new ke;constructor(){}ngOnDestroy(){this._resizeSubscription.unsubscribe()}getWidth(){return TJ(this._elementRef.nativeElement)}get element(){return this._elementRef.nativeElement}_handleResize(){setTimeout(()=>this._parent._handleLabelResized())}_subscribeToResize(){this._resizeSubscription.unsubscribe(),this._ngZone.runOutsideAngular(()=>{this._resizeSubscription=this._resizeObserver.observe(this._elementRef.nativeElement,{box:"border-box"}).subscribe(()=>this._handleResize())})}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["label","matFormFieldFloatingLabel",""]],hostAttrs:[1,"mdc-floating-label","mat-mdc-floating-label"],hostVars:2,hostBindings:function(i,r){i&2&&G("mdc-floating-label--float-above",r.floating)},inputs:{floating:"floating",monitorResize:"monitorResize"}})}return t})();function TJ(t){let n=t;if(n.offsetParent!==null)return n.scrollWidth;let e=n.cloneNode(!0);e.style.setProperty("position","absolute"),e.style.setProperty("transform","translate(-9999px, -9999px)"),document.documentElement.appendChild(e);let i=e.scrollWidth;return e.remove(),i}var XV="mdc-line-ripple--active",ex="mdc-line-ripple--deactivating",JV=(()=>{class t{_elementRef=u(Y);_cleanupTransitionEnd;constructor(){let e=u(ae),i=u(ze);e.runOutsideAngular(()=>{this._cleanupTransitionEnd=i.listen(this._elementRef.nativeElement,"transitionend",this._handleTransitionEnd)})}activate(){let e=this._elementRef.nativeElement.classList;e.remove(ex),e.add(XV)}deactivate(){this._elementRef.nativeElement.classList.add(ex)}_handleTransitionEnd=e=>{let i=this._elementRef.nativeElement.classList,r=i.contains(ex);e.propertyName==="opacity"&&r&&i.remove(XV,ex)};ngOnDestroy(){this._cleanupTransitionEnd()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["div","matFormFieldLineRipple",""]],hostAttrs:[1,"mdc-line-ripple"]})}return t})(),eB=(()=>{class t{_elementRef=u(Y);_ngZone=u(ae);open=!1;_notch;ngAfterViewInit(){let e=this._elementRef.nativeElement,i=e.querySelector(".mdc-floating-label");i?(e.classList.add("mdc-notched-outline--upgraded"),typeof requestAnimationFrame=="function"&&(i.style.transitionDuration="0s",this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>i.style.transitionDuration="")}))):e.classList.add("mdc-notched-outline--no-label")}_setNotchWidth(e){let i=this._notch.nativeElement;!this.open||!e?i.style.width="":i.style.width=`calc(${e}px * var(--mat-mdc-form-field-floating-label-scale, 0.75) + 9px)`}_setMaxWidth(e){this._notch.nativeElement.style.setProperty("--mat-form-field-notch-max-width",`calc(100% - ${e}px)`)}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["div","matFormFieldNotchedOutline",""]],viewQuery:function(i,r){if(i&1&&ie(aJ,5),i&2){let o;j(o=H())&&(r._notch=o.first)}},hostAttrs:[1,"mdc-notched-outline"],hostVars:2,hostBindings:function(i,r){i&2&&G("mdc-notched-outline--notched",r.open)},inputs:{open:[0,"matFormFieldNotchedOutlineOpen","open"]},attrs:sJ,ngContentSelectors:lJ,decls:5,vars:0,consts:[["notch",""],[1,"mat-mdc-notch-piece","mdc-notched-outline__leading"],[1,"mat-mdc-notch-piece","mdc-notched-outline__notch"],[1,"mat-mdc-notch-piece","mdc-notched-outline__trailing"]],template:function(i,r){i&1&&(Ee(),ni(0,"div",1),gt(1,"div",2,0),ne(3),yt(),ni(4,"div",3))},encapsulation:2,changeDetection:0})}return t})(),Za=(()=>{class t{value;stateChanges;id;placeholder;ngControl;focused;empty;shouldLabelFloat;required;disabled;errorState;controlType;autofilled;userAriaDescribedBy;disableAutomaticLabeling;describedByIds;static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t})}return t})();var oa=new O("MatFormField"),rB=new O("MAT_FORM_FIELD_DEFAULT_OPTIONS"),tB="fill",IJ="auto",iB="fixed",AJ="translateY(-50%)",Xt=(()=>{class t{_elementRef=u(Y);_changeDetectorRef=u(ye);_platform=u(Ye);_idGenerator=u(et);_ngZone=u(ae);_defaults=u(rB,{optional:!0});_currentDirection;_textField;_iconPrefixContainer;_textPrefixContainer;_iconSuffixContainer;_textSuffixContainer;_floatingLabel;_notchedOutline;_lineRipple;_iconPrefixContainerSignal=ir("iconPrefixContainer");_textPrefixContainerSignal=ir("textPrefixContainer");_iconSuffixContainerSignal=ir("iconSuffixContainer");_textSuffixContainerSignal=ir("textSuffixContainer");_prefixSuffixContainers=ci(()=>[this._iconPrefixContainerSignal(),this._textPrefixContainerSignal(),this._iconSuffixContainerSignal(),this._textSuffixContainerSignal()].map(e=>e?.nativeElement).filter(e=>e!==void 0));_formFieldControl;_prefixChildren;_suffixChildren;_errorChildren;_hintChildren;_labelChild=Mr(gi);get hideRequiredMarker(){return this._hideRequiredMarker}set hideRequiredMarker(e){this._hideRequiredMarker=Vi(e)}_hideRequiredMarker=!1;color="primary";get floatLabel(){return this._floatLabel||this._defaults?.floatLabel||IJ}set floatLabel(e){e!==this._floatLabel&&(this._floatLabel=e,this._changeDetectorRef.markForCheck())}_floatLabel;get appearance(){return this._appearanceSignal()}set appearance(e){let i=e||this._defaults?.appearance||tB;this._appearanceSignal.set(i)}_appearanceSignal=he(tB);get subscriptSizing(){return this._subscriptSizing||this._defaults?.subscriptSizing||iB}set subscriptSizing(e){this._subscriptSizing=e||this._defaults?.subscriptSizing||iB}_subscriptSizing=null;get hintLabel(){return this._hintLabel}set hintLabel(e){this._hintLabel=e,this._processHints()}_hintLabel="";_hasIconPrefix=!1;_hasTextPrefix=!1;_hasIconSuffix=!1;_hasTextSuffix=!1;_labelId=this._idGenerator.getId("mat-mdc-form-field-label-");_hintLabelId=this._idGenerator.getId("mat-mdc-hint-");_describedByIds;get _control(){return this._explicitFormFieldControl||this._formFieldControl}set _control(e){this._explicitFormFieldControl=e}_destroyed=new z;_isFocused=null;_explicitFormFieldControl;_previousControl=null;_previousControlValidatorFn=null;_stateChanges;_valueChanges;_describedByChanges;_outlineLabelOffsetResizeObserver=null;_animationsDisabled=Qe();constructor(){let e=this._defaults,i=u(Yt);e&&(e.appearance&&(this.appearance=e.appearance),this._hideRequiredMarker=!!e?.hideRequiredMarker,e.color&&(this.color=e.color)),zr(()=>this._currentDirection=i.valueSignal()),this._syncOutlineLabelOffset()}ngAfterViewInit(){this._updateFocusState(),this._animationsDisabled||this._ngZone.runOutsideAngular(()=>{setTimeout(()=>{this._elementRef.nativeElement.classList.add("mat-form-field-animations-enabled")},300)}),this._changeDetectorRef.detectChanges()}ngAfterContentInit(){this._assertFormFieldControl(),this._initializeSubscript(),this._initializePrefixAndSuffix()}ngAfterContentChecked(){this._assertFormFieldControl(),this._control!==this._previousControl&&(this._initializeControl(this._previousControl),this._control.ngControl&&this._control.ngControl.control&&(this._previousControlValidatorFn=this._control.ngControl.control.validator),this._previousControl=this._control),this._control.ngControl&&this._control.ngControl.control&&this._control.ngControl.control.validator!==this._previousControlValidatorFn&&this._changeDetectorRef.markForCheck()}ngOnDestroy(){this._outlineLabelOffsetResizeObserver?.disconnect(),this._stateChanges?.unsubscribe(),this._valueChanges?.unsubscribe(),this._describedByChanges?.unsubscribe(),this._destroyed.next(),this._destroyed.complete()}getLabelId=ci(()=>this._hasFloatingLabel()?this._labelId:null);getConnectedOverlayOrigin(){return this._textField||this._elementRef}_animateAndLockLabel(){this._hasFloatingLabel()&&(this.floatLabel="always")}_initializeControl(e){let i=this._control,r="mat-mdc-form-field-type-";e&&this._elementRef.nativeElement.classList.remove(r+e.controlType),i.controlType&&this._elementRef.nativeElement.classList.add(r+i.controlType),this._stateChanges?.unsubscribe(),this._stateChanges=i.stateChanges.subscribe(()=>{this._updateFocusState(),this._changeDetectorRef.markForCheck()}),this._describedByChanges?.unsubscribe(),this._describedByChanges=i.stateChanges.pipe(Ue([void 0,void 0]),se(()=>[i.errorState,i.userAriaDescribedBy]),fb(),ce(([[o,a],[s,l]])=>o!==s||a!==l)).subscribe(()=>this._syncDescribedByIds()),this._valueChanges?.unsubscribe(),i.ngControl&&i.ngControl.valueChanges&&(this._valueChanges=i.ngControl.valueChanges.pipe(xe(this._destroyed)).subscribe(()=>this._changeDetectorRef.markForCheck()))}_checkPrefixAndSuffixTypes(){this._hasIconPrefix=!!this._prefixChildren.find(e=>!e._isText),this._hasTextPrefix=!!this._prefixChildren.find(e=>e._isText),this._hasIconSuffix=!!this._suffixChildren.find(e=>!e._isText),this._hasTextSuffix=!!this._suffixChildren.find(e=>e._isText)}_initializePrefixAndSuffix(){this._checkPrefixAndSuffixTypes(),it(this._prefixChildren.changes,this._suffixChildren.changes).subscribe(()=>{this._checkPrefixAndSuffixTypes(),this._changeDetectorRef.markForCheck()})}_initializeSubscript(){this._hintChildren.changes.subscribe(()=>{this._processHints(),this._changeDetectorRef.markForCheck()}),this._errorChildren.changes.subscribe(()=>{this._syncDescribedByIds(),this._changeDetectorRef.markForCheck()}),this._validateHints(),this._syncDescribedByIds()}_assertFormFieldControl(){this._control}_updateFocusState(){let e=this._control.focused;e&&!this._isFocused?(this._isFocused=!0,this._lineRipple?.activate()):!e&&(this._isFocused||this._isFocused===null)&&(this._isFocused=!1,this._lineRipple?.deactivate()),this._elementRef.nativeElement.classList.toggle("mat-focused",e),this._textField?.nativeElement.classList.toggle("mdc-text-field--focused",e)}_syncOutlineLabelOffset(){f2({earlyRead:()=>{if(this._appearanceSignal()!=="outline")return this._outlineLabelOffsetResizeObserver?.disconnect(),null;if(globalThis.ResizeObserver){this._outlineLabelOffsetResizeObserver||=new globalThis.ResizeObserver(()=>{this._writeOutlinedLabelStyles(this._getOutlinedLabelOffset())});for(let e of this._prefixSuffixContainers())this._outlineLabelOffsetResizeObserver.observe(e,{box:"border-box"})}return this._getOutlinedLabelOffset()},write:e=>this._writeOutlinedLabelStyles(e())})}_shouldAlwaysFloat(){return this.floatLabel==="always"}_hasOutline(){return this.appearance==="outline"}_forceDisplayInfixLabel(){return!this._platform.isBrowser&&this._prefixChildren.length&&!this._shouldLabelFloat()}_hasFloatingLabel=ci(()=>!!this._labelChild());_shouldLabelFloat(){return this._hasFloatingLabel()?this._control.shouldLabelFloat||this._shouldAlwaysFloat():!1}_shouldForward(e){let i=this._control?this._control.ngControl:null;return i&&i[e]}_getSubscriptMessageType(){return this._errorChildren&&this._errorChildren.length>0&&this._control.errorState?"error":"hint"}_handleLabelResized(){this._refreshOutlineNotchWidth()}_refreshOutlineNotchWidth(){!this._hasOutline()||!this._floatingLabel||!this._shouldLabelFloat()?this._notchedOutline?._setNotchWidth(0):this._notchedOutline?._setNotchWidth(this._floatingLabel.getWidth())}_processHints(){this._validateHints(),this._syncDescribedByIds()}_validateHints(){this._hintChildren}_syncDescribedByIds(){if(this._control){let e=[];if(this._control.userAriaDescribedBy&&typeof this._control.userAriaDescribedBy=="string"&&e.push(...this._control.userAriaDescribedBy.split(" ")),this._getSubscriptMessageType()==="hint"){let o=this._hintChildren?this._hintChildren.find(s=>s.align==="start"):null,a=this._hintChildren?this._hintChildren.find(s=>s.align==="end"):null;o?e.push(o.id):this._hintLabel&&e.push(this._hintLabelId),a&&e.push(a.id)}else this._errorChildren&&e.push(...this._errorChildren.map(o=>o.id));let i=this._control.describedByIds,r;if(i){let o=this._describedByIds||e;r=e.concat(i.filter(a=>a&&!o.includes(a)))}else r=e;this._control.setDescribedByIds(r),this._describedByIds=e}}_getOutlinedLabelOffset(){if(!this._hasOutline()||!this._floatingLabel)return null;if(!this._iconPrefixContainer&&!this._textPrefixContainer)return["",null];if(!this._isAttachedToDom())return null;let e=this._iconPrefixContainer?.nativeElement,i=this._textPrefixContainer?.nativeElement,r=this._iconSuffixContainer?.nativeElement,o=this._textSuffixContainer?.nativeElement,a=e?.getBoundingClientRect().width??0,s=i?.getBoundingClientRect().width??0,l=r?.getBoundingClientRect().width??0,c=o?.getBoundingClientRect().width??0,d=this._currentDirection==="rtl"?"-1":"1",p=`${a+s}px`,b=`calc(${d} * (${p} + var(--mat-mdc-form-field-label-offset-x, 0px)))`,y=`var(--mat-mdc-form-field-label-transform, ${AJ} translateX(${b}))`,w=a+s+l+c;return[y,w]}_writeOutlinedLabelStyles(e){if(e!==null){let[i,r]=e;this._floatingLabel&&(this._floatingLabel.element.style.transform=i),r!==null&&this._notchedOutline?._setMaxWidth(r)}}_isAttachedToDom(){let e=this._elementRef.nativeElement;if(e.getRootNode){let i=e.getRootNode();return i&&i!==e}return document.documentElement.contains(e)}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-form-field"]],contentQueries:function(i,r,o){if(i&1&&(Hn(o,r._labelChild,gi,5),Ce(o,Za,5),Ce(o,QS,5),Ce(o,KS,5),Ce(o,YS,5),Ce(o,ag,5)),i&2){Ko();let a;j(a=H())&&(r._formFieldControl=a.first),j(a=H())&&(r._prefixChildren=a),j(a=H())&&(r._suffixChildren=a),j(a=H())&&(r._errorChildren=a),j(a=H())&&(r._hintChildren=a)}},viewQuery:function(i,r){if(i&1&&(zn(r._iconPrefixContainerSignal,qV,5),zn(r._textPrefixContainerSignal,YV,5),zn(r._iconSuffixContainerSignal,QV,5),zn(r._textSuffixContainerSignal,KV,5),ie(cJ,5),ie(qV,5),ie(YV,5),ie(QV,5),ie(KV,5),ie(ZV,5),ie(eB,5),ie(JV,5)),i&2){Ko(4);let o;j(o=H())&&(r._textField=o.first),j(o=H())&&(r._iconPrefixContainer=o.first),j(o=H())&&(r._textPrefixContainer=o.first),j(o=H())&&(r._iconSuffixContainer=o.first),j(o=H())&&(r._textSuffixContainer=o.first),j(o=H())&&(r._floatingLabel=o.first),j(o=H())&&(r._notchedOutline=o.first),j(o=H())&&(r._lineRipple=o.first)}},hostAttrs:[1,"mat-mdc-form-field"],hostVars:38,hostBindings:function(i,r){i&2&&G("mat-mdc-form-field-label-always-float",r._shouldAlwaysFloat())("mat-mdc-form-field-has-icon-prefix",r._hasIconPrefix)("mat-mdc-form-field-has-icon-suffix",r._hasIconSuffix)("mat-form-field-invalid",r._control.errorState)("mat-form-field-disabled",r._control.disabled)("mat-form-field-autofilled",r._control.autofilled)("mat-form-field-appearance-fill",r.appearance=="fill")("mat-form-field-appearance-outline",r.appearance=="outline")("mat-form-field-hide-placeholder",r._hasFloatingLabel()&&!r._shouldLabelFloat())("mat-primary",r.color!=="accent"&&r.color!=="warn")("mat-accent",r.color==="accent")("mat-warn",r.color==="warn")("ng-untouched",r._shouldForward("untouched"))("ng-touched",r._shouldForward("touched"))("ng-pristine",r._shouldForward("pristine"))("ng-dirty",r._shouldForward("dirty"))("ng-valid",r._shouldForward("valid"))("ng-invalid",r._shouldForward("invalid"))("ng-pending",r._shouldForward("pending"))},inputs:{hideRequiredMarker:"hideRequiredMarker",color:"color",floatLabel:"floatLabel",appearance:"appearance",subscriptSizing:"subscriptSizing",hintLabel:"hintLabel"},exportAs:["matFormField"],features:[we([{provide:oa,useExisting:t},{provide:nB,useExisting:t}])],ngContentSelectors:uJ,decls:18,vars:21,consts:[["labelTemplate",""],["textField",""],["iconPrefixContainer",""],["textPrefixContainer",""],["textSuffixContainer",""],["iconSuffixContainer",""],[1,"mat-mdc-text-field-wrapper","mdc-text-field",3,"click"],[1,"mat-mdc-form-field-focus-overlay"],[1,"mat-mdc-form-field-flex"],["matFormFieldNotchedOutline","",3,"matFormFieldNotchedOutlineOpen"],[1,"mat-mdc-form-field-icon-prefix"],[1,"mat-mdc-form-field-text-prefix"],[1,"mat-mdc-form-field-infix"],[3,"ngTemplateOutlet"],[1,"mat-mdc-form-field-text-suffix"],[1,"mat-mdc-form-field-icon-suffix"],["matFormFieldLineRipple",""],["aria-atomic","true","aria-live","polite",1,"mat-mdc-form-field-subscript-wrapper","mat-mdc-form-field-bottom-align"],[1,"mat-mdc-form-field-error-wrapper"],[1,"mat-mdc-form-field-hint-wrapper"],["matFormFieldFloatingLabel","",3,"floating","monitorResize","id"],["aria-hidden","true",1,"mat-mdc-form-field-required-marker","mdc-floating-label--required"],[3,"id"],[1,"mat-mdc-form-field-hint-spacer"]],template:function(i,r){if(i&1){let o=q();Ee(dJ),A(0,pJ,1,1,"ng-template",null,0,Mi),m(2,"div",6,1),S("click",function(s){return k(o),T(r._control.onContainerClick(s))}),L(4,fJ,1,0,"div",7),m(5,"div",8),L(6,bJ,2,2,"div",9),L(7,vJ,3,0,"div",10),L(8,yJ,3,0,"div",11),m(9,"div",12),L(10,CJ,1,1,null,13),ne(11),h(),L(12,wJ,3,0,"div",14),L(13,DJ,3,0,"div",15),h(),L(14,MJ,1,0,"div",16),h(),m(15,"div",17),L(16,EJ,2,0,"div",18)(17,kJ,5,1,"div",19),h()}if(i&2){let o;g(2),G("mdc-text-field--filled",!r._hasOutline())("mdc-text-field--outlined",r._hasOutline())("mdc-text-field--no-label",!r._hasFloatingLabel())("mdc-text-field--disabled",r._control.disabled)("mdc-text-field--invalid",r._control.errorState),g(2),V(!r._hasOutline()&&!r._control.disabled?4:-1),g(2),V(r._hasOutline()?6:-1),g(),V(r._hasIconPrefix?7:-1),g(),V(r._hasTextPrefix?8:-1),g(2),V(!r._hasOutline()||r._forceDisplayInfixLabel()?10:-1),g(2),V(r._hasTextSuffix?12:-1),g(),V(r._hasIconSuffix?13:-1),g(),V(r._hasOutline()?-1:14),g(),G("mat-mdc-form-field-subscript-dynamic-size",r.subscriptSizing==="dynamic");let a=r._getSubscriptMessageType();g(),V((o=a)==="error"?16:o==="hint"?17:-1)}},dependencies:[ZV,eB,$n,JV,ag],styles:[`.mdc-text-field{display:inline-flex;align-items:baseline;padding:0 16px;position:relative;box-sizing:border-box;overflow:hidden;will-change:opacity,transform,color;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.mdc-text-field__input{width:100%;min-width:0;border:none;border-radius:0;background:none;padding:0;-moz-appearance:none;-webkit-appearance:none;height:28px}.mdc-text-field__input::-webkit-calendar-picker-indicator,.mdc-text-field__input::-webkit-search-cancel-button{display:none}.mdc-text-field__input::-ms-clear{display:none}.mdc-text-field__input:focus{outline:none}.mdc-text-field__input:invalid{box-shadow:none}.mdc-text-field__input::placeholder{opacity:0}.mdc-text-field__input::-moz-placeholder{opacity:0}.mdc-text-field__input::-webkit-input-placeholder{opacity:0}.mdc-text-field__input:-ms-input-placeholder{opacity:0}.mdc-text-field--no-label .mdc-text-field__input::placeholder,.mdc-text-field--focused .mdc-text-field__input::placeholder{opacity:1}.mdc-text-field--no-label .mdc-text-field__input::-moz-placeholder,.mdc-text-field--focused .mdc-text-field__input::-moz-placeholder{opacity:1}.mdc-text-field--no-label .mdc-text-field__input::-webkit-input-placeholder,.mdc-text-field--focused .mdc-text-field__input::-webkit-input-placeholder{opacity:1}.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder,.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder{opacity:1}.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::placeholder{opacity:0}.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::-moz-placeholder{opacity:0}.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::-webkit-input-placeholder{opacity:0}.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive:-ms-input-placeholder{opacity:0}.mdc-text-field--outlined .mdc-text-field__input,.mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input{height:100%}.mdc-text-field--outlined .mdc-text-field__input{display:flex;border:none !important;background-color:rgba(0,0,0,0)}.mdc-text-field--disabled .mdc-text-field__input{pointer-events:auto}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input{color:var(--mat-form-field-filled-input-text-color, var(--mat-sys-on-surface));caret-color:var(--mat-form-field-filled-caret-color, var(--mat-sys-primary))}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder{color:var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::-moz-placeholder{color:var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::-webkit-input-placeholder{color:var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder{color:var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input{color:var(--mat-form-field-outlined-input-text-color, var(--mat-sys-on-surface));caret-color:var(--mat-form-field-outlined-caret-color, var(--mat-sys-primary))}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder{color:var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::-moz-placeholder{color:var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::-webkit-input-placeholder{color:var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder{color:var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--filled.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__input{caret-color:var(--mat-form-field-filled-error-caret-color, var(--mat-sys-error))}.mdc-text-field--outlined.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__input{caret-color:var(--mat-form-field-outlined-error-caret-color, var(--mat-sys-error))}.mdc-text-field--filled.mdc-text-field--disabled .mdc-text-field__input{color:var(--mat-form-field-filled-disabled-input-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mdc-text-field--outlined.mdc-text-field--disabled .mdc-text-field__input{color:var(--mat-form-field-outlined-disabled-input-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}@media(forced-colors: active){.mdc-text-field--disabled .mdc-text-field__input{background-color:Window}}.mdc-text-field--filled{height:56px;border-bottom-right-radius:0;border-bottom-left-radius:0;border-top-left-radius:var(--mat-form-field-filled-container-shape, var(--mat-sys-corner-extra-small));border-top-right-radius:var(--mat-form-field-filled-container-shape, var(--mat-sys-corner-extra-small))}.mdc-text-field--filled:not(.mdc-text-field--disabled){background-color:var(--mat-form-field-filled-container-color, var(--mat-sys-surface-variant))}.mdc-text-field--filled.mdc-text-field--disabled{background-color:var(--mat-form-field-filled-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 4%, transparent))}.mdc-text-field--outlined{height:56px;overflow:visible;padding-right:max(16px,var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)));padding-left:max(16px,var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)) + 4px)}[dir=rtl] .mdc-text-field--outlined{padding-right:max(16px,var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)) + 4px);padding-left:max(16px,var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)))}.mdc-floating-label{position:absolute;left:0;transform-origin:left top;line-height:1.15rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text;overflow:hidden;will-change:transform}[dir=rtl] .mdc-floating-label{right:0;left:auto;transform-origin:right top;text-align:right}.mdc-text-field .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-text-field--outlined .mdc-floating-label{left:4px;right:auto}[dir=rtl] .mdc-text-field--outlined .mdc-floating-label{left:auto;right:4px}.mdc-text-field--filled .mdc-floating-label{left:16px;right:auto}[dir=rtl] .mdc-text-field--filled .mdc-floating-label{left:auto;right:16px}.mdc-text-field--disabled .mdc-floating-label{cursor:default}@media(forced-colors: active){.mdc-text-field--disabled .mdc-floating-label{z-index:1}}.mdc-text-field--filled.mdc-text-field--no-label .mdc-floating-label{display:none}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-floating-label{color:var(--mat-form-field-filled-label-text-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-floating-label{color:var(--mat-form-field-filled-focus-label-text-color, var(--mat-sys-primary))}.mdc-text-field--filled:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-floating-label{color:var(--mat-form-field-filled-hover-label-text-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--filled.mdc-text-field--disabled .mdc-floating-label{color:var(--mat-form-field-filled-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-floating-label{color:var(--mat-form-field-filled-error-label-text-color, var(--mat-sys-error))}.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mdc-floating-label{color:var(--mat-form-field-filled-error-focus-label-text-color, var(--mat-sys-error))}.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-floating-label{color:var(--mat-form-field-filled-error-hover-label-text-color, var(--mat-sys-on-error-container))}.mdc-text-field--filled .mdc-floating-label{font-family:var(--mat-form-field-filled-label-text-font, var(--mat-sys-body-large-font));font-size:var(--mat-form-field-filled-label-text-size, var(--mat-sys-body-large-size));font-weight:var(--mat-form-field-filled-label-text-weight, var(--mat-sys-body-large-weight));letter-spacing:var(--mat-form-field-filled-label-text-tracking, var(--mat-sys-body-large-tracking))}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-floating-label{color:var(--mat-form-field-outlined-label-text-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-floating-label{color:var(--mat-form-field-outlined-focus-label-text-color, var(--mat-sys-primary))}.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-floating-label{color:var(--mat-form-field-outlined-hover-label-text-color, var(--mat-sys-on-surface))}.mdc-text-field--outlined.mdc-text-field--disabled .mdc-floating-label{color:var(--mat-form-field-outlined-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-floating-label{color:var(--mat-form-field-outlined-error-label-text-color, var(--mat-sys-error))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mdc-floating-label{color:var(--mat-form-field-outlined-error-focus-label-text-color, var(--mat-sys-error))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-floating-label{color:var(--mat-form-field-outlined-error-hover-label-text-color, var(--mat-sys-on-error-container))}.mdc-text-field--outlined .mdc-floating-label{font-family:var(--mat-form-field-outlined-label-text-font, var(--mat-sys-body-large-font));font-size:var(--mat-form-field-outlined-label-text-size, var(--mat-sys-body-large-size));font-weight:var(--mat-form-field-outlined-label-text-weight, var(--mat-sys-body-large-weight));letter-spacing:var(--mat-form-field-outlined-label-text-tracking, var(--mat-sys-body-large-tracking))}.mdc-floating-label--float-above{cursor:auto;transform:translateY(-106%) scale(0.75)}.mdc-text-field--filled .mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-text-field--outlined .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1);font-size:.75rem}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:133.3333333333%}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-floating-label--required:not(.mdc-floating-label--hide-required-marker)::after{margin-left:1px;margin-right:0;content:"*"}[dir=rtl] .mdc-floating-label--required:not(.mdc-floating-label--hide-required-marker)::after{margin-left:0;margin-right:1px}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline{text-align:right}.mdc-text-field--outlined .mdc-notched-outline{z-index:1}.mat-mdc-notch-piece{box-sizing:border-box;height:100%;pointer-events:none;border-top:1px solid;border-bottom:1px solid}.mdc-text-field--focused .mat-mdc-notch-piece{border-width:2px}.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mat-mdc-notch-piece{border-color:var(--mat-form-field-outlined-outline-color, var(--mat-sys-outline));border-width:var(--mat-form-field-outlined-outline-width, 1px)}.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mat-mdc-notch-piece{border-color:var(--mat-form-field-outlined-hover-outline-color, var(--mat-sys-on-surface))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mat-mdc-notch-piece{border-color:var(--mat-form-field-outlined-focus-outline-color, var(--mat-sys-primary))}.mdc-text-field--outlined.mdc-text-field--disabled .mat-mdc-notch-piece{border-color:var(--mat-form-field-outlined-disabled-outline-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid .mat-mdc-notch-piece{border-color:var(--mat-form-field-outlined-error-outline-color, var(--mat-sys-error))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--focused):hover .mdc-notched-outline .mat-mdc-notch-piece{border-color:var(--mat-form-field-outlined-error-hover-outline-color, var(--mat-sys-on-error-container))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mat-mdc-notch-piece{border-color:var(--mat-form-field-outlined-error-focus-outline-color, var(--mat-sys-error))}.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline .mat-mdc-notch-piece{border-width:var(--mat-form-field-outlined-focus-outline-width, 2px)}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small));border-bottom-left-radius:var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading{width:max(12px,var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)))}[dir=rtl] .mdc-notched-outline__leading{border-left:none;border-right:1px solid;border-bottom-left-radius:0;border-top-left-radius:0;border-top-right-radius:var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small));border-bottom-right-radius:var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))}.mdc-notched-outline__trailing{flex-grow:1;border-left:none;border-right:1px solid;border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small));border-bottom-right-radius:var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))}[dir=rtl] .mdc-notched-outline__trailing{border-left:1px solid;border-right:none;border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small));border-bottom-left-radius:var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))}.mdc-notched-outline__notch{flex:0 0 auto;width:auto}.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch{max-width:min(var(--mat-form-field-notch-max-width, 100%),calc(100% - max(12px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))) * 2))}.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{max-width:min(100%,calc(100% - max(12px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))) * 2))}.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-text-field--focused.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}.mdc-line-ripple::before,.mdc-line-ripple::after{position:absolute;bottom:0;left:0;width:100%;border-bottom-style:solid;content:""}.mdc-line-ripple::before{z-index:1;border-bottom-width:var(--mat-form-field-filled-active-indicator-height, 1px)}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before{border-bottom-color:var(--mat-form-field-filled-active-indicator-color, var(--mat-sys-on-surface-variant))}.mdc-text-field--filled:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-line-ripple::before{border-bottom-color:var(--mat-form-field-filled-hover-active-indicator-color, var(--mat-sys-on-surface))}.mdc-text-field--filled.mdc-text-field--disabled .mdc-line-ripple::before{border-bottom-color:var(--mat-form-field-filled-disabled-active-indicator-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-line-ripple::before{border-bottom-color:var(--mat-form-field-filled-error-active-indicator-color, var(--mat-sys-error))}.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--focused):hover .mdc-line-ripple::before{border-bottom-color:var(--mat-form-field-filled-error-hover-active-indicator-color, var(--mat-sys-on-error-container))}.mdc-line-ripple::after{transform:scaleX(0);opacity:0;z-index:2}.mdc-text-field--filled .mdc-line-ripple::after{border-bottom-width:var(--mat-form-field-filled-focus-active-indicator-height, 2px)}.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after{border-bottom-color:var(--mat-form-field-filled-focus-active-indicator-color, var(--mat-sys-primary))}.mdc-text-field--filled.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::after{border-bottom-color:var(--mat-form-field-filled-error-focus-active-indicator-color, var(--mat-sys-error))}.mdc-line-ripple--active::after{transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}.mdc-text-field--disabled{pointer-events:none}.mat-mdc-form-field-textarea-control{vertical-align:middle;resize:vertical;box-sizing:border-box;height:auto;margin:0;padding:0;border:none;overflow:auto}.mat-mdc-form-field-input-control.mat-mdc-form-field-input-control{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font:inherit;letter-spacing:inherit;text-decoration:inherit;text-transform:inherit;border:none}.mat-mdc-form-field .mat-mdc-floating-label.mdc-floating-label{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;line-height:normal;pointer-events:all;will-change:auto}.mat-mdc-form-field:not(.mat-form-field-disabled) .mat-mdc-floating-label.mdc-floating-label{cursor:inherit}.mdc-text-field--no-label:not(.mdc-text-field--textarea) .mat-mdc-form-field-input-control.mdc-text-field__input,.mat-mdc-text-field-wrapper .mat-mdc-form-field-input-control{height:auto}.mat-mdc-text-field-wrapper .mat-mdc-form-field-input-control.mdc-text-field__input[type=color]{height:23px}.mat-mdc-text-field-wrapper{height:auto;flex:auto;will-change:auto}.mat-mdc-form-field-has-icon-prefix .mat-mdc-text-field-wrapper{padding-left:0;--mat-mdc-form-field-label-offset-x: -16px}.mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper{padding-right:0}[dir=rtl] .mat-mdc-text-field-wrapper{padding-left:16px;padding-right:16px}[dir=rtl] .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper{padding-left:0}[dir=rtl] .mat-mdc-form-field-has-icon-prefix .mat-mdc-text-field-wrapper{padding-right:0}.mat-form-field-disabled .mdc-text-field__input::placeholder{color:var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-form-field-disabled .mdc-text-field__input::-moz-placeholder{color:var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-form-field-disabled .mdc-text-field__input::-webkit-input-placeholder{color:var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-form-field-disabled .mdc-text-field__input:-ms-input-placeholder{color:var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-mdc-form-field-label-always-float .mdc-text-field__input::placeholder{transition-delay:40ms;transition-duration:110ms;opacity:1}.mat-mdc-text-field-wrapper .mat-mdc-form-field-infix .mat-mdc-floating-label{left:auto;right:auto}.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-text-field__input{display:inline-block}.mat-mdc-form-field .mat-mdc-text-field-wrapper.mdc-text-field .mdc-notched-outline__notch{padding-top:0}.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch{border-left:1px solid rgba(0,0,0,0)}[dir=rtl] .mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch{border-left:none;border-right:1px solid rgba(0,0,0,0)}.mat-mdc-form-field-infix{min-height:var(--mat-form-field-container-height, 56px);padding-top:var(--mat-form-field-filled-with-label-container-padding-top, 24px);padding-bottom:var(--mat-form-field-filled-with-label-container-padding-bottom, 8px)}.mdc-text-field--outlined .mat-mdc-form-field-infix,.mdc-text-field--no-label .mat-mdc-form-field-infix{padding-top:var(--mat-form-field-container-vertical-padding, 16px);padding-bottom:var(--mat-form-field-container-vertical-padding, 16px)}.mat-mdc-text-field-wrapper .mat-mdc-form-field-flex .mat-mdc-floating-label{top:calc(var(--mat-form-field-container-height, 56px)/2)}.mdc-text-field--filled .mat-mdc-floating-label{display:var(--mat-form-field-filled-label-display, block)}.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{--mat-mdc-form-field-label-transform: translateY(calc(calc(6.75px + var(--mat-form-field-container-height, 56px) / 2) * -1)) scale(var(--mat-mdc-form-field-floating-label-scale, 0.75));transform:var(--mat-mdc-form-field-label-transform)}@keyframes _mat-form-field-subscript-animation{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:translateY(0)}}.mat-mdc-form-field-subscript-wrapper{box-sizing:border-box;width:100%;position:relative}.mat-mdc-form-field-hint-wrapper,.mat-mdc-form-field-error-wrapper{position:absolute;top:0;left:0;right:0;padding:0 16px;opacity:1;transform:translateY(0);animation:_mat-form-field-subscript-animation 0ms cubic-bezier(0.55, 0, 0.55, 0.2)}.mat-mdc-form-field-subscript-dynamic-size .mat-mdc-form-field-hint-wrapper,.mat-mdc-form-field-subscript-dynamic-size .mat-mdc-form-field-error-wrapper{position:static}.mat-mdc-form-field-bottom-align::before{content:"";display:inline-block;height:16px}.mat-mdc-form-field-bottom-align.mat-mdc-form-field-subscript-dynamic-size::before{content:unset}.mat-mdc-form-field-hint-end{order:1}.mat-mdc-form-field-hint-wrapper{display:flex}.mat-mdc-form-field-hint-spacer{flex:1 0 1em}.mat-mdc-form-field-error{display:block;color:var(--mat-form-field-error-text-color, var(--mat-sys-error))}.mat-mdc-form-field-subscript-wrapper,.mat-mdc-form-field-bottom-align::before{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:var(--mat-form-field-subscript-text-font, var(--mat-sys-body-small-font));line-height:var(--mat-form-field-subscript-text-line-height, var(--mat-sys-body-small-line-height));font-size:var(--mat-form-field-subscript-text-size, var(--mat-sys-body-small-size));letter-spacing:var(--mat-form-field-subscript-text-tracking, var(--mat-sys-body-small-tracking));font-weight:var(--mat-form-field-subscript-text-weight, var(--mat-sys-body-small-weight))}.mat-mdc-form-field-focus-overlay{top:0;left:0;right:0;bottom:0;position:absolute;opacity:0;pointer-events:none;background-color:var(--mat-form-field-state-layer-color, var(--mat-sys-on-surface))}.mat-mdc-text-field-wrapper:hover .mat-mdc-form-field-focus-overlay{opacity:var(--mat-form-field-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-form-field.mat-focused .mat-mdc-form-field-focus-overlay{opacity:var(--mat-form-field-focus-state-layer-opacity, 0)}select.mat-mdc-form-field-input-control{-moz-appearance:none;-webkit-appearance:none;background-color:rgba(0,0,0,0);display:inline-flex;box-sizing:border-box}select.mat-mdc-form-field-input-control:not(:disabled){cursor:pointer}select.mat-mdc-form-field-input-control:not(.mat-mdc-native-select-inline) option{color:var(--mat-form-field-select-option-text-color, var(--mat-sys-neutral10))}select.mat-mdc-form-field-input-control:not(.mat-mdc-native-select-inline) option:disabled{color:var(--mat-form-field-select-disabled-option-text-color, color-mix(in srgb, var(--mat-sys-neutral10) 38%, transparent))}.mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-infix::after{content:"";width:0;height:0;border-left:5px solid rgba(0,0,0,0);border-right:5px solid rgba(0,0,0,0);border-top:5px solid;position:absolute;right:0;top:50%;margin-top:-2.5px;pointer-events:none;color:var(--mat-form-field-enabled-select-arrow-color, var(--mat-sys-on-surface-variant))}[dir=rtl] .mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-infix::after{right:auto;left:0}.mat-mdc-form-field-type-mat-native-select.mat-focused .mat-mdc-form-field-infix::after{color:var(--mat-form-field-focus-select-arrow-color, var(--mat-sys-primary))}.mat-mdc-form-field-type-mat-native-select.mat-form-field-disabled .mat-mdc-form-field-infix::after{color:var(--mat-form-field-disabled-select-arrow-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-input-control{padding-right:15px}[dir=rtl] .mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-input-control{padding-right:0;padding-left:15px}@media(forced-colors: active){.mat-form-field-appearance-fill .mat-mdc-text-field-wrapper{outline:solid 1px}}@media(forced-colors: active){.mat-form-field-appearance-fill.mat-form-field-disabled .mat-mdc-text-field-wrapper{outline-color:GrayText}}@media(forced-colors: active){.mat-form-field-appearance-fill.mat-focused .mat-mdc-text-field-wrapper{outline:dashed 3px}}@media(forced-colors: active){.mat-mdc-form-field.mat-focused .mdc-notched-outline{border:dashed 3px}}.mat-mdc-form-field-input-control[type=date],.mat-mdc-form-field-input-control[type=datetime],.mat-mdc-form-field-input-control[type=datetime-local],.mat-mdc-form-field-input-control[type=month],.mat-mdc-form-field-input-control[type=week],.mat-mdc-form-field-input-control[type=time]{line-height:1}.mat-mdc-form-field-input-control::-webkit-datetime-edit{line-height:1;padding:0;margin-bottom:-2px}.mat-mdc-form-field{--mat-mdc-form-field-floating-label-scale: 0.75;display:inline-flex;flex-direction:column;min-width:0;text-align:left;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:var(--mat-form-field-container-text-font, var(--mat-sys-body-large-font));line-height:var(--mat-form-field-container-text-line-height, var(--mat-sys-body-large-line-height));font-size:var(--mat-form-field-container-text-size, var(--mat-sys-body-large-size));letter-spacing:var(--mat-form-field-container-text-tracking, var(--mat-sys-body-large-tracking));font-weight:var(--mat-form-field-container-text-weight, var(--mat-sys-body-large-weight))}.mat-mdc-form-field .mdc-text-field--outlined .mdc-floating-label--float-above{font-size:calc(var(--mat-form-field-outlined-label-text-populated-size)*var(--mat-mdc-form-field-floating-label-scale))}.mat-mdc-form-field .mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:var(--mat-form-field-outlined-label-text-populated-size)}[dir=rtl] .mat-mdc-form-field{text-align:right}.mat-mdc-form-field-flex{display:inline-flex;align-items:baseline;box-sizing:border-box;width:100%}.mat-mdc-text-field-wrapper{width:100%;z-index:0}.mat-mdc-form-field-icon-prefix,.mat-mdc-form-field-icon-suffix{align-self:center;line-height:0;pointer-events:auto;position:relative;z-index:1}.mat-mdc-form-field-icon-prefix>.mat-icon,.mat-mdc-form-field-icon-suffix>.mat-icon{padding:0 12px;box-sizing:content-box}.mat-mdc-form-field-icon-prefix{color:var(--mat-form-field-leading-icon-color, var(--mat-sys-on-surface-variant))}.mat-form-field-disabled .mat-mdc-form-field-icon-prefix{color:var(--mat-form-field-disabled-leading-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-mdc-form-field-icon-suffix{color:var(--mat-form-field-trailing-icon-color, var(--mat-sys-on-surface-variant))}.mat-form-field-disabled .mat-mdc-form-field-icon-suffix{color:var(--mat-form-field-disabled-trailing-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-form-field-invalid .mat-mdc-form-field-icon-suffix{color:var(--mat-form-field-error-trailing-icon-color, var(--mat-sys-error))}.mat-form-field-invalid:not(.mat-focused):not(.mat-form-field-disabled) .mat-mdc-text-field-wrapper:hover .mat-mdc-form-field-icon-suffix{color:var(--mat-form-field-error-hover-trailing-icon-color, var(--mat-sys-on-error-container))}.mat-form-field-invalid.mat-focused .mat-mdc-text-field-wrapper .mat-mdc-form-field-icon-suffix{color:var(--mat-form-field-error-focus-trailing-icon-color, var(--mat-sys-error))}.mat-mdc-form-field-icon-prefix,[dir=rtl] .mat-mdc-form-field-icon-suffix{padding:0 4px 0 0}.mat-mdc-form-field-icon-suffix,[dir=rtl] .mat-mdc-form-field-icon-prefix{padding:0 0 0 4px}.mat-mdc-form-field-subscript-wrapper .mat-icon,.mat-mdc-form-field label .mat-icon{width:1em;height:1em;font-size:inherit}.mat-mdc-form-field-infix{flex:auto;min-width:0;width:180px;position:relative;box-sizing:border-box}.mat-mdc-form-field-infix:has(textarea[cols]){width:auto}.mat-mdc-form-field .mdc-notched-outline__notch{margin-left:-1px;-webkit-clip-path:inset(-9em -999em -9em 1px);clip-path:inset(-9em -999em -9em 1px)}[dir=rtl] .mat-mdc-form-field .mdc-notched-outline__notch{margin-left:0;margin-right:-1px;-webkit-clip-path:inset(-9em 1px -9em -999em);clip-path:inset(-9em 1px -9em -999em)}.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-floating-label{transition:transform 150ms cubic-bezier(0.4, 0, 0.2, 1),color 150ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input{transition:opacity 150ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::-moz-placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::-webkit-input-placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input:-ms-input-placeholder{transition:opacity 67ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::placeholder,.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::placeholder{transition-delay:40ms;transition-duration:110ms}.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::-moz-placeholder,.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::-moz-placeholder{transition-delay:40ms;transition-duration:110ms}.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::-webkit-input-placeholder,.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::-webkit-input-placeholder{transition-delay:40ms;transition-duration:110ms}.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder,.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder{transition-delay:40ms;transition-duration:110ms}.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field--filled:not(.mdc-ripple-upgraded):focus .mdc-text-field__ripple::before{transition-duration:75ms}.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-line-ripple::after{transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-mdc-form-field.mat-form-field-animations-enabled .mat-mdc-form-field-hint-wrapper,.mat-mdc-form-field.mat-form-field-animations-enabled .mat-mdc-form-field-error-wrapper{animation-duration:300ms}.mdc-notched-outline .mdc-floating-label{max-width:calc(100% + 1px)}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(133.3333333333% + 1px)} +`],encapsulation:2,changeDetection:0})}return t})();var sg=class{};function lg(t){return t&&typeof t.connect=="function"&&!(t instanceof Ql)}var Sc=(function(t){return t[t.REPLACED=0]="REPLACED",t[t.INSERTED=1]="INSERTED",t[t.MOVED=2]="MOVED",t[t.REMOVED=3]="REMOVED",t})(Sc||{}),yh=new O("_ViewRepeater");var OJ=20,zs=(()=>{class t{_ngZone=u(ae);_platform=u(Ye);_renderer=u(hn).createRenderer(null,null);_cleanupGlobalListener;constructor(){}_scrolled=new z;_scrolledCount=0;scrollContainers=new Map;register(e){this.scrollContainers.has(e)||this.scrollContainers.set(e,e.elementScrolled().subscribe(()=>this._scrolled.next(e)))}deregister(e){let i=this.scrollContainers.get(e);i&&(i.unsubscribe(),this.scrollContainers.delete(e))}scrolled(e=OJ){return this._platform.isBrowser?new Ne(i=>{this._cleanupGlobalListener||(this._cleanupGlobalListener=this._ngZone.runOutsideAngular(()=>this._renderer.listen("document","scroll",()=>this._scrolled.next())));let r=e>0?this._scrolled.pipe(Xl(e)).subscribe(i):this._scrolled.subscribe(i);return this._scrolledCount++,()=>{r.unsubscribe(),this._scrolledCount--,this._scrolledCount||(this._cleanupGlobalListener?.(),this._cleanupGlobalListener=void 0)}}):Q()}ngOnDestroy(){this._cleanupGlobalListener?.(),this._cleanupGlobalListener=void 0,this.scrollContainers.forEach((e,i)=>this.deregister(i)),this._scrolled.complete()}ancestorScrolled(e,i){let r=this.getAncestorScrollContainers(e);return this.scrolled(i).pipe(ce(o=>!o||r.indexOf(o)>-1))}getAncestorScrollContainers(e){let i=[];return this.scrollContainers.forEach((r,o)=>{this._scrollableContainsElement(o,e)&&i.push(o)}),i}_scrollableContainsElement(e,i){let r=Wr(i),o=e.getElementRef().nativeElement;do if(r==o)return!0;while(r=r.parentElement);return!1}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Xa=(()=>{class t{elementRef=u(Y);scrollDispatcher=u(zs);ngZone=u(ae);dir=u(Yt,{optional:!0});_scrollElement=this.elementRef.nativeElement;_destroyed=new z;_renderer=u(ze);_cleanupScroll;_elementScrolled=new z;constructor(){}ngOnInit(){this._cleanupScroll=this.ngZone.runOutsideAngular(()=>this._renderer.listen(this._scrollElement,"scroll",e=>this._elementScrolled.next(e))),this.scrollDispatcher.register(this)}ngOnDestroy(){this._cleanupScroll?.(),this._elementScrolled.complete(),this.scrollDispatcher.deregister(this),this._destroyed.next(),this._destroyed.complete()}elementScrolled(){return this._elementScrolled}getElementRef(){return this.elementRef}scrollTo(e){let i=this.elementRef.nativeElement,r=this.dir&&this.dir.value=="rtl";e.left==null&&(e.left=r?e.end:e.start),e.right==null&&(e.right=r?e.start:e.end),e.bottom!=null&&(e.top=i.scrollHeight-i.clientHeight-e.bottom),r&&oh()!=Ya.NORMAL?(e.left!=null&&(e.right=i.scrollWidth-i.clientWidth-e.left),oh()==Ya.INVERTED?e.left=e.right:oh()==Ya.NEGATED&&(e.left=e.right?-e.right:e.right)):e.right!=null&&(e.left=i.scrollWidth-i.clientWidth-e.right),this._applyScrollToOptions(e)}_applyScrollToOptions(e){let i=this.elementRef.nativeElement;Xv()?i.scrollTo(e):(e.top!=null&&(i.scrollTop=e.top),e.left!=null&&(i.scrollLeft=e.left))}measureScrollOffset(e){let i="left",r="right",o=this.elementRef.nativeElement;if(e=="top")return o.scrollTop;if(e=="bottom")return o.scrollHeight-o.clientHeight-o.scrollTop;let a=this.dir&&this.dir.value=="rtl";return e=="start"?e=a?r:i:e=="end"&&(e=a?i:r),a&&oh()==Ya.INVERTED?e==i?o.scrollWidth-o.clientWidth-o.scrollLeft:o.scrollLeft:a&&oh()==Ya.NEGATED?e==i?o.scrollLeft+o.scrollWidth-o.clientWidth:-o.scrollLeft:e==i?o.scrollLeft:o.scrollWidth-o.clientWidth-o.scrollLeft}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdk-scrollable",""],["","cdkScrollable",""]]})}return t})(),RJ=20,sr=(()=>{class t{_platform=u(Ye);_listeners;_viewportSize;_change=new z;_document=u(_e);constructor(){let e=u(ae),i=u(hn).createRenderer(null,null);e.runOutsideAngular(()=>{if(this._platform.isBrowser){let r=o=>this._change.next(o);this._listeners=[i.listen("window","resize",r),i.listen("window","orientationchange",r)]}this.change().subscribe(()=>this._viewportSize=null)})}ngOnDestroy(){this._listeners?.forEach(e=>e()),this._change.complete()}getViewportSize(){this._viewportSize||this._updateViewportSize();let e={width:this._viewportSize.width,height:this._viewportSize.height};return this._platform.isBrowser||(this._viewportSize=null),e}getViewportRect(){let e=this.getViewportScrollPosition(),{width:i,height:r}=this.getViewportSize();return{top:e.top,left:e.left,bottom:e.top+r,right:e.left+i,height:r,width:i}}getViewportScrollPosition(){if(!this._platform.isBrowser)return{top:0,left:0};let e=this._document,i=this._getWindow(),r=e.documentElement,o=r.getBoundingClientRect(),a=-o.top||e.body.scrollTop||i.scrollY||r.scrollTop||0,s=-o.left||e.body.scrollLeft||i.scrollX||r.scrollLeft||0;return{top:a,left:s}}change(e=RJ){return e>0?this._change.pipe(Xl(e)):this._change}_getWindow(){return this._document.defaultView||window}_updateViewportSize(){let e=this._getWindow();this._viewportSize=this._platform.isBrowser?{width:e.innerWidth,height:e.innerHeight}:{width:0,height:0}}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var Tr=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})(),cg=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[Ns,Tr,Ns,Tr]})}return t})();var dg=class{_attachedHost;attach(n){return this._attachedHost=n,n.attach(this)}detach(){let n=this._attachedHost;n!=null&&(this._attachedHost=null,n.detach())}get isAttached(){return this._attachedHost!=null}setAttachedHost(n){this._attachedHost=n}},ao=class extends dg{component;viewContainerRef;injector;projectableNodes;constructor(n,e,i,r){super(),this.component=n,this.viewContainerRef=e,this.injector=i,this.projectableNodes=r}},kn=class extends dg{templateRef;viewContainerRef;context;injector;constructor(n,e,i,r){super(),this.templateRef=n,this.viewContainerRef=e,this.context=i,this.injector=r}get origin(){return this.templateRef.elementRef}attach(n,e=this.context){return this.context=e,super.attach(n)}detach(){return this.context=void 0,super.detach()}},ZS=class extends dg{element;constructor(n){super(),this.element=n instanceof Y?n.nativeElement:n}},kc=class{_attachedPortal;_disposeFn;_isDisposed=!1;hasAttached(){return!!this._attachedPortal}attach(n){if(n instanceof ao)return this._attachedPortal=n,this.attachComponentPortal(n);if(n instanceof kn)return this._attachedPortal=n,this.attachTemplatePortal(n);if(this.attachDomPortal&&n instanceof ZS)return this._attachedPortal=n,this.attachDomPortal(n)}attachDomPortal=null;detach(){this._attachedPortal&&(this._attachedPortal.setAttachedHost(null),this._attachedPortal=null),this._invokeDisposeFn()}dispose(){this.hasAttached()&&this.detach(),this._invokeDisposeFn(),this._isDisposed=!0}setDisposeFn(n){this._disposeFn=n}_invokeDisposeFn(){this._disposeFn&&(this._disposeFn(),this._disposeFn=null)}},ug=class extends kc{outletElement;_appRef;_defaultInjector;constructor(n,e,i){super(),this.outletElement=n,this._appRef=e,this._defaultInjector=i}attachComponentPortal(n){let e;if(n.viewContainerRef){let i=n.injector||n.viewContainerRef.injector,r=i.get(ws,null,{optional:!0})||void 0;e=n.viewContainerRef.createComponent(n.component,{index:n.viewContainerRef.length,injector:i,ngModuleRef:r,projectableNodes:n.projectableNodes||void 0}),this.setDisposeFn(()=>e.destroy())}else{let i=this._appRef,r=n.injector||this._defaultInjector||de.NULL,o=r.get(ti,i.injector);e=Am(n.component,{elementInjector:r,environmentInjector:o,projectableNodes:n.projectableNodes||void 0}),i.attachView(e.hostView),this.setDisposeFn(()=>{i.viewCount>0&&i.detachView(e.hostView),e.destroy()})}return this.outletElement.appendChild(this._getComponentRootNode(e)),this._attachedPortal=n,e}attachTemplatePortal(n){let e=n.viewContainerRef,i=e.createEmbeddedView(n.templateRef,n.context,{injector:n.injector});return i.rootNodes.forEach(r=>this.outletElement.appendChild(r)),i.detectChanges(),this.setDisposeFn(()=>{let r=e.indexOf(i);r!==-1&&e.remove(r)}),this._attachedPortal=n,i}attachDomPortal=n=>{let e=n.element;e.parentNode;let i=this.outletElement.ownerDocument.createComment("dom-portal");e.parentNode.insertBefore(i,e),this.outletElement.appendChild(e),this._attachedPortal=n,super.setDisposeFn(()=>{i.parentNode&&i.parentNode.replaceChild(e,i)})};dispose(){super.dispose(),this.outletElement.remove()}_getComponentRootNode(n){return n.hostView.rootNodes[0]}},oB=(()=>{class t extends kn{constructor(){let e=u(te),i=u(st);super(e,i)}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkPortal",""]],exportAs:["cdkPortal"],features:[le]})}return t})();var Ir=(()=>{class t extends kc{_moduleRef=u(ws,{optional:!0});_document=u(_e);_viewContainerRef=u(st);_isInitialized=!1;_attachedRef;constructor(){super()}get portal(){return this._attachedPortal}set portal(e){this.hasAttached()&&!e&&!this._isInitialized||(this.hasAttached()&&super.detach(),e&&super.attach(e),this._attachedPortal=e||null)}attached=new U;get attachedRef(){return this._attachedRef}ngOnInit(){this._isInitialized=!0}ngOnDestroy(){super.dispose(),this._attachedRef=this._attachedPortal=null}attachComponentPortal(e){e.setAttachedHost(this);let i=e.viewContainerRef!=null?e.viewContainerRef:this._viewContainerRef,r=i.createComponent(e.component,{index:i.length,injector:e.injector||i.injector,projectableNodes:e.projectableNodes||void 0,ngModuleRef:this._moduleRef||void 0});return i!==this._viewContainerRef&&this._getRootNode().appendChild(r.hostView.rootNodes[0]),super.setDisposeFn(()=>r.destroy()),this._attachedPortal=e,this._attachedRef=r,this.attached.emit(r),r}attachTemplatePortal(e){e.setAttachedHost(this);let i=this._viewContainerRef.createEmbeddedView(e.templateRef,e.context,{injector:e.injector});return super.setDisposeFn(()=>this._viewContainerRef.clear()),this._attachedPortal=e,this._attachedRef=i,this.attached.emit(i),i}attachDomPortal=e=>{let i=e.element;i.parentNode;let r=this._document.createComment("dom-portal");e.setAttachedHost(this),i.parentNode.insertBefore(r,i),this._getRootNode().appendChild(i),this._attachedPortal=e,super.setDisposeFn(()=>{r.parentNode&&r.parentNode.replaceChild(i,r)})};_getRootNode(){let e=this._viewContainerRef.element.nativeElement;return e.nodeType===e.ELEMENT_NODE?e:e.parentNode}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkPortalOutlet",""]],inputs:{portal:[0,"cdkPortalOutlet","portal"]},outputs:{attached:"attached"},exportAs:["cdkPortalOutlet"],features:[le]})}return t})();var Oo=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})();var aB=Xv();function Tc(t){return new tx(t.get(sr),t.get(_e))}var tx=class{_viewportRuler;_previousHTMLStyles={top:"",left:""};_previousScrollPosition;_isEnabled=!1;_document;constructor(n,e){this._viewportRuler=n,this._document=e}attach(){}enable(){if(this._canBeEnabled()){let n=this._document.documentElement;this._previousScrollPosition=this._viewportRuler.getViewportScrollPosition(),this._previousHTMLStyles.left=n.style.left||"",this._previousHTMLStyles.top=n.style.top||"",n.style.left=dn(-this._previousScrollPosition.left),n.style.top=dn(-this._previousScrollPosition.top),n.classList.add("cdk-global-scrollblock"),this._isEnabled=!0}}disable(){if(this._isEnabled){let n=this._document.documentElement,e=this._document.body,i=n.style,r=e.style,o=i.scrollBehavior||"",a=r.scrollBehavior||"";this._isEnabled=!1,i.left=this._previousHTMLStyles.left,i.top=this._previousHTMLStyles.top,n.classList.remove("cdk-global-scrollblock"),aB&&(i.scrollBehavior=r.scrollBehavior="auto"),window.scroll(this._previousScrollPosition.left,this._previousScrollPosition.top),aB&&(i.scrollBehavior=o,r.scrollBehavior=a)}}_canBeEnabled(){if(this._document.documentElement.classList.contains("cdk-global-scrollblock")||this._isEnabled)return!1;let e=this._document.documentElement,i=this._viewportRuler.getViewportSize();return e.scrollHeight>i.height||e.scrollWidth>i.width}};function hB(t,n){return new ix(t.get(zs),t.get(ae),t.get(sr),n)}var ix=class{_scrollDispatcher;_ngZone;_viewportRuler;_config;_scrollSubscription=null;_overlayRef;_initialScrollPosition;constructor(n,e,i,r){this._scrollDispatcher=n,this._ngZone=e,this._viewportRuler=i,this._config=r}attach(n){this._overlayRef,this._overlayRef=n}enable(){if(this._scrollSubscription)return;let n=this._scrollDispatcher.scrolled(0).pipe(ce(e=>!e||!this._overlayRef.overlayElement.contains(e.getElementRef().nativeElement)));this._config&&this._config.threshold&&this._config.threshold>1?(this._initialScrollPosition=this._viewportRuler.getViewportScrollPosition().top,this._scrollSubscription=n.subscribe(()=>{let e=this._viewportRuler.getViewportScrollPosition().top;Math.abs(e-this._initialScrollPosition)>this._config.threshold?this._detach():this._overlayRef.updatePosition()})):this._scrollSubscription=n.subscribe(this._detach)}disable(){this._scrollSubscription&&(this._scrollSubscription.unsubscribe(),this._scrollSubscription=null)}detach(){this.disable(),this._overlayRef=null}_detach=()=>{this.disable(),this._overlayRef.hasAttached()&&this._ngZone.run(()=>this._overlayRef.detach())}};var mg=class{enable(){}disable(){}attach(){}};function XS(t,n){return n.some(e=>{let i=t.bottome.bottom,o=t.righte.right;return i||r||o||a})}function sB(t,n){return n.some(e=>{let i=t.tope.bottom,o=t.lefte.right;return i||r||o||a})}function Tn(t,n){return new nx(t.get(zs),t.get(sr),t.get(ae),n)}var nx=class{_scrollDispatcher;_viewportRuler;_ngZone;_config;_scrollSubscription=null;_overlayRef;constructor(n,e,i,r){this._scrollDispatcher=n,this._viewportRuler=e,this._ngZone=i,this._config=r}attach(n){this._overlayRef,this._overlayRef=n}enable(){if(!this._scrollSubscription){let n=this._config?this._config.scrollThrottle:0;this._scrollSubscription=this._scrollDispatcher.scrolled(n).subscribe(()=>{if(this._overlayRef.updatePosition(),this._config&&this._config.autoClose){let e=this._overlayRef.overlayElement.getBoundingClientRect(),{width:i,height:r}=this._viewportRuler.getViewportSize();XS(e,[{width:i,height:r,bottom:r,right:i,top:0,left:0}])&&(this.disable(),this._ngZone.run(()=>this._overlayRef.detach()))}})}}disable(){this._scrollSubscription&&(this._scrollSubscription.unsubscribe(),this._scrollSubscription=null)}detach(){this.disable(),this._overlayRef=null}},pB=(()=>{class t{_injector=u(de);constructor(){}noop=()=>new mg;close=e=>hB(this._injector,e);block=()=>Tc(this._injector);reposition=e=>Tn(this._injector,e);static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Gr=class{positionStrategy;scrollStrategy=new mg;panelClass="";hasBackdrop=!1;backdropClass="cdk-overlay-dark-backdrop";disableAnimations;width;height;minWidth;minHeight;maxWidth;maxHeight;direction;disposeOnNavigation=!1;constructor(n){if(n){let e=Object.keys(n);for(let i of e)n[i]!==void 0&&(this[i]=n[i])}}};var rx=class{connectionPair;scrollableViewProperties;constructor(n,e){this.connectionPair=n,this.scrollableViewProperties=e}};var fB=(()=>{class t{_attachedOverlays=[];_document=u(_e);_isAttached;constructor(){}ngOnDestroy(){this.detach()}add(e){this.remove(e),this._attachedOverlays.push(e)}remove(e){let i=this._attachedOverlays.indexOf(e);i>-1&&this._attachedOverlays.splice(i,1),this._attachedOverlays.length===0&&this.detach()}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),gB=(()=>{class t extends fB{_ngZone=u(ae);_renderer=u(hn).createRenderer(null,null);_cleanupKeydown;add(e){super.add(e),this._isAttached||(this._ngZone.runOutsideAngular(()=>{this._cleanupKeydown=this._renderer.listen("body","keydown",this._keydownListener)}),this._isAttached=!0)}detach(){this._isAttached&&(this._cleanupKeydown?.(),this._isAttached=!1)}_keydownListener=e=>{let i=this._attachedOverlays;for(let r=i.length-1;r>-1;r--)if(i[r]._keydownEvents.observers.length>0){this._ngZone.run(()=>i[r]._keydownEvents.next(e));break}};static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),_B=(()=>{class t extends fB{_platform=u(Ye);_ngZone=u(ae);_renderer=u(hn).createRenderer(null,null);_cursorOriginalValue;_cursorStyleIsSet=!1;_pointerDownEventTarget;_cleanups;add(e){if(super.add(e),!this._isAttached){let i=this._document.body,r={capture:!0},o=this._renderer;this._cleanups=this._ngZone.runOutsideAngular(()=>[o.listen(i,"pointerdown",this._pointerDownListener,r),o.listen(i,"click",this._clickListener,r),o.listen(i,"auxclick",this._clickListener,r),o.listen(i,"contextmenu",this._clickListener,r)]),this._platform.IOS&&!this._cursorStyleIsSet&&(this._cursorOriginalValue=i.style.cursor,i.style.cursor="pointer",this._cursorStyleIsSet=!0),this._isAttached=!0}}detach(){this._isAttached&&(this._cleanups?.forEach(e=>e()),this._cleanups=void 0,this._platform.IOS&&this._cursorStyleIsSet&&(this._document.body.style.cursor=this._cursorOriginalValue,this._cursorStyleIsSet=!1),this._isAttached=!1)}_pointerDownListener=e=>{this._pointerDownEventTarget=or(e)};_clickListener=e=>{let i=or(e),r=e.type==="click"&&this._pointerDownEventTarget?this._pointerDownEventTarget:i;this._pointerDownEventTarget=null;let o=this._attachedOverlays.slice();for(let a=o.length-1;a>-1;a--){let s=o[a];if(s._outsidePointerEvents.observers.length<1||!s.hasAttached())continue;if(lB(s.overlayElement,i)||lB(s.overlayElement,r))break;let l=s._outsidePointerEvents;this._ngZone?this._ngZone.run(()=>l.next(e)):l.next(e)}};static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function lB(t,n){let e=typeof ShadowRoot<"u"&&ShadowRoot,i=n;for(;i;){if(i===t)return!0;i=e&&i instanceof ShadowRoot?i.host:i.parentNode}return!1}var bB=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["ng-component"]],hostAttrs:["cdk-overlay-style-loader",""],decls:0,vars:0,template:function(i,r){},styles:[`.cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed}@layer cdk-overlay{.cdk-overlay-container{z-index:1000}}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper{display:flex;position:absolute}@layer cdk-overlay{.cdk-global-overlay-wrapper{z-index:1000}}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;display:flex;max-width:100%;max-height:100%}@layer cdk-overlay{.cdk-overlay-pane{z-index:1000}}.cdk-overlay-backdrop{position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:auto;-webkit-tap-highlight-color:rgba(0,0,0,0);opacity:0;touch-action:manipulation}@layer cdk-overlay{.cdk-overlay-backdrop{z-index:1000;transition:opacity 400ms cubic-bezier(0.25, 0.8, 0.25, 1)}}@media(prefers-reduced-motion){.cdk-overlay-backdrop{transition-duration:1ms}}.cdk-overlay-backdrop-showing{opacity:1}@media(forced-colors: active){.cdk-overlay-backdrop-showing{opacity:.6}}@layer cdk-overlay{.cdk-overlay-dark-backdrop{background:rgba(0,0,0,.32)}}.cdk-overlay-transparent-backdrop{transition:visibility 1ms linear,opacity 1ms linear;visibility:hidden;opacity:1}.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing,.cdk-high-contrast-active .cdk-overlay-transparent-backdrop{opacity:0;visibility:visible}.cdk-overlay-backdrop-noop-animation{transition:none}.cdk-overlay-connected-position-bounding-box{position:absolute;display:flex;flex-direction:column;min-width:1px;min-height:1px}@layer cdk-overlay{.cdk-overlay-connected-position-bounding-box{z-index:1000}}.cdk-global-scrollblock{position:fixed;width:100%;overflow-y:scroll} +`],encapsulation:2,changeDetection:0})}return t})(),ax=(()=>{class t{_platform=u(Ye);_containerElement;_document=u(_e);_styleLoader=u(ft);constructor(){}ngOnDestroy(){this._containerElement?.remove()}getContainerElement(){return this._loadStyles(),this._containerElement||this._createContainer(),this._containerElement}_createContainer(){let e="cdk-overlay-container";if(this._platform.isBrowser||AS()){let r=this._document.querySelectorAll(`.${e}[platform="server"], .${e}[platform="test"]`);for(let o=0;o{let n=this.element;clearTimeout(this._fallbackTimeout),this._cleanupTransitionEnd?.(),this._cleanupTransitionEnd=this._renderer.listen(n,"transitionend",this.dispose),this._fallbackTimeout=setTimeout(this.dispose,500),n.style.pointerEvents="none",n.classList.remove("cdk-overlay-backdrop-showing")})}dispose=()=>{clearTimeout(this._fallbackTimeout),this._cleanupClick?.(),this._cleanupTransitionEnd?.(),this._cleanupClick=this._cleanupTransitionEnd=this._fallbackTimeout=void 0,this.element.remove()}},xh=class{_portalOutlet;_host;_pane;_config;_ngZone;_keyboardDispatcher;_document;_location;_outsideClickDispatcher;_animationsDisabled;_injector;_renderer;_backdropClick=new z;_attachments=new z;_detachments=new z;_positionStrategy;_scrollStrategy;_locationChanges=ke.EMPTY;_backdropRef=null;_detachContentMutationObserver;_detachContentAfterRenderRef;_previousHostParent;_keydownEvents=new z;_outsidePointerEvents=new z;_afterNextRenderRef;constructor(n,e,i,r,o,a,s,l,c,d=!1,p,_){this._portalOutlet=n,this._host=e,this._pane=i,this._config=r,this._ngZone=o,this._keyboardDispatcher=a,this._document=s,this._location=l,this._outsideClickDispatcher=c,this._animationsDisabled=d,this._injector=p,this._renderer=_,r.scrollStrategy&&(this._scrollStrategy=r.scrollStrategy,this._scrollStrategy.attach(this)),this._positionStrategy=r.positionStrategy}get overlayElement(){return this._pane}get backdropElement(){return this._backdropRef?.element||null}get hostElement(){return this._host}attach(n){!this._host.parentElement&&this._previousHostParent&&this._previousHostParent.appendChild(this._host);let e=this._portalOutlet.attach(n);return this._positionStrategy&&this._positionStrategy.attach(this),this._updateStackingOrder(),this._updateElementSize(),this._updateElementDirection(),this._scrollStrategy&&this._scrollStrategy.enable(),this._afterNextRenderRef?.destroy(),this._afterNextRenderRef=vt(()=>{this.hasAttached()&&this.updatePosition()},{injector:this._injector}),this._togglePointerEvents(!0),this._config.hasBackdrop&&this._attachBackdrop(),this._config.panelClass&&this._toggleClasses(this._pane,this._config.panelClass,!0),this._attachments.next(),this._completeDetachContent(),this._keyboardDispatcher.add(this),this._config.disposeOnNavigation&&(this._locationChanges=this._location.subscribe(()=>this.dispose())),this._outsideClickDispatcher.add(this),typeof e?.onDestroy=="function"&&e.onDestroy(()=>{this.hasAttached()&&this._ngZone.runOutsideAngular(()=>Promise.resolve().then(()=>this.detach()))}),e}detach(){if(!this.hasAttached())return;this.detachBackdrop(),this._togglePointerEvents(!1),this._positionStrategy&&this._positionStrategy.detach&&this._positionStrategy.detach(),this._scrollStrategy&&this._scrollStrategy.disable();let n=this._portalOutlet.detach();return this._detachments.next(),this._completeDetachContent(),this._keyboardDispatcher.remove(this),this._detachContentWhenEmpty(),this._locationChanges.unsubscribe(),this._outsideClickDispatcher.remove(this),n}dispose(){let n=this.hasAttached();this._positionStrategy&&this._positionStrategy.dispose(),this._disposeScrollStrategy(),this._backdropRef?.dispose(),this._locationChanges.unsubscribe(),this._keyboardDispatcher.remove(this),this._portalOutlet.dispose(),this._attachments.complete(),this._backdropClick.complete(),this._keydownEvents.complete(),this._outsidePointerEvents.complete(),this._outsideClickDispatcher.remove(this),this._host?.remove(),this._afterNextRenderRef?.destroy(),this._previousHostParent=this._pane=this._host=this._backdropRef=null,n&&this._detachments.next(),this._detachments.complete(),this._completeDetachContent()}hasAttached(){return this._portalOutlet.hasAttached()}backdropClick(){return this._backdropClick}attachments(){return this._attachments}detachments(){return this._detachments}keydownEvents(){return this._keydownEvents}outsidePointerEvents(){return this._outsidePointerEvents}getConfig(){return this._config}updatePosition(){this._positionStrategy&&this._positionStrategy.apply()}updatePositionStrategy(n){n!==this._positionStrategy&&(this._positionStrategy&&this._positionStrategy.dispose(),this._positionStrategy=n,this.hasAttached()&&(n.attach(this),this.updatePosition()))}updateSize(n){this._config=I(I({},this._config),n),this._updateElementSize()}setDirection(n){this._config=Me(I({},this._config),{direction:n}),this._updateElementDirection()}addPanelClass(n){this._pane&&this._toggleClasses(this._pane,n,!0)}removePanelClass(n){this._pane&&this._toggleClasses(this._pane,n,!1)}getDirection(){let n=this._config.direction;return n?typeof n=="string"?n:n.value:"ltr"}updateScrollStrategy(n){n!==this._scrollStrategy&&(this._disposeScrollStrategy(),this._scrollStrategy=n,this.hasAttached()&&(n.attach(this),n.enable()))}_updateElementDirection(){this._host.setAttribute("dir",this.getDirection())}_updateElementSize(){if(!this._pane)return;let n=this._pane.style;n.width=dn(this._config.width),n.height=dn(this._config.height),n.minWidth=dn(this._config.minWidth),n.minHeight=dn(this._config.minHeight),n.maxWidth=dn(this._config.maxWidth),n.maxHeight=dn(this._config.maxHeight)}_togglePointerEvents(n){this._pane.style.pointerEvents=n?"":"none"}_attachBackdrop(){let n="cdk-overlay-backdrop-showing";this._backdropRef?.dispose(),this._backdropRef=new JS(this._document,this._renderer,this._ngZone,e=>{this._backdropClick.next(e)}),this._animationsDisabled&&this._backdropRef.element.classList.add("cdk-overlay-backdrop-noop-animation"),this._config.backdropClass&&this._toggleClasses(this._backdropRef.element,this._config.backdropClass,!0),this._host.parentElement.insertBefore(this._backdropRef.element,this._host),!this._animationsDisabled&&typeof requestAnimationFrame<"u"?this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>this._backdropRef?.element.classList.add(n))}):this._backdropRef.element.classList.add(n)}_updateStackingOrder(){this._host.nextSibling&&this._host.parentNode.appendChild(this._host)}detachBackdrop(){this._animationsDisabled?(this._backdropRef?.dispose(),this._backdropRef=null):this._backdropRef?.detach()}_toggleClasses(n,e,i){let r=Dl(e||[]).filter(o=>!!o);r.length&&(i?n.classList.add(...r):n.classList.remove(...r))}_detachContentWhenEmpty(){let n=!1;try{this._detachContentAfterRenderRef=vt(()=>{n=!0,this._detachContent()},{injector:this._injector})}catch(e){if(n)throw e;this._detachContent()}globalThis.MutationObserver&&this._pane&&(this._detachContentMutationObserver||=new globalThis.MutationObserver(()=>{this._detachContent()}),this._detachContentMutationObserver.observe(this._pane,{childList:!0}))}_detachContent(){(!this._pane||!this._host||this._pane.children.length===0)&&(this._pane&&this._config.panelClass&&this._toggleClasses(this._pane,this._config.panelClass,!1),this._host&&this._host.parentElement&&(this._previousHostParent=this._host.parentElement,this._host.remove()),this._completeDetachContent())}_completeDetachContent(){this._detachContentAfterRenderRef?.destroy(),this._detachContentAfterRenderRef=void 0,this._detachContentMutationObserver?.disconnect()}_disposeScrollStrategy(){let n=this._scrollStrategy;n?.disable(),n?.detach?.()}},cB="cdk-overlay-connected-position-bounding-box",FJ=/([A-Za-z%]+)$/;function Ja(t,n){return new Ch(n,t.get(sr),t.get(_e),t.get(Ye),t.get(ax))}var Ch=class{_viewportRuler;_document;_platform;_overlayContainer;_overlayRef;_isInitialRender;_lastBoundingBoxSize={width:0,height:0};_isPushed=!1;_canPush=!0;_growAfterOpen=!1;_hasFlexibleDimensions=!0;_positionLocked=!1;_originRect;_overlayRect;_viewportRect;_containerRect;_viewportMargin=0;_scrollables=[];_preferredPositions=[];_origin;_pane;_isDisposed;_boundingBox;_lastPosition;_lastScrollVisibility;_positionChanges=new z;_resizeSubscription=ke.EMPTY;_offsetX=0;_offsetY=0;_transformOriginSelector;_appliedPanelClasses=[];_previousPushAmount;positionChanges=this._positionChanges;get positions(){return this._preferredPositions}constructor(n,e,i,r,o){this._viewportRuler=e,this._document=i,this._platform=r,this._overlayContainer=o,this.setOrigin(n)}attach(n){this._overlayRef&&this._overlayRef,this._validatePositions(),n.hostElement.classList.add(cB),this._overlayRef=n,this._boundingBox=n.hostElement,this._pane=n.overlayElement,this._isDisposed=!1,this._isInitialRender=!0,this._lastPosition=null,this._resizeSubscription.unsubscribe(),this._resizeSubscription=this._viewportRuler.change().subscribe(()=>{this._isInitialRender=!0,this.apply()})}apply(){if(this._isDisposed||!this._platform.isBrowser)return;if(!this._isInitialRender&&this._positionLocked&&this._lastPosition){this.reapplyLastPosition();return}this._clearPanelClasses(),this._resetOverlayElementStyles(),this._resetBoundingBoxStyles(),this._viewportRect=this._getNarrowedViewportRect(),this._originRect=this._getOriginRect(),this._overlayRect=this._pane.getBoundingClientRect(),this._containerRect=this._overlayContainer.getContainerElement().getBoundingClientRect();let n=this._originRect,e=this._overlayRect,i=this._viewportRect,r=this._containerRect,o=[],a;for(let s of this._preferredPositions){let l=this._getOriginPoint(n,r,s),c=this._getOverlayPoint(l,e,s),d=this._getOverlayFit(c,e,i,s);if(d.isCompletelyWithinViewport){this._isPushed=!1,this._applyPosition(s,l);return}if(this._canFitWithFlexibleDimensions(d,c,i)){o.push({position:s,origin:l,overlayRect:e,boundingBoxRect:this._calculateBoundingBoxRect(l,s)});continue}(!a||a.overlayFit.visibleAreal&&(l=d,s=c)}this._isPushed=!1,this._applyPosition(s.position,s.origin);return}if(this._canPush){this._isPushed=!0,this._applyPosition(a.position,a.originPoint);return}this._applyPosition(a.position,a.originPoint)}detach(){this._clearPanelClasses(),this._lastPosition=null,this._previousPushAmount=null,this._resizeSubscription.unsubscribe()}dispose(){this._isDisposed||(this._boundingBox&&mu(this._boundingBox.style,{top:"",left:"",right:"",bottom:"",height:"",width:"",alignItems:"",justifyContent:""}),this._pane&&this._resetOverlayElementStyles(),this._overlayRef&&this._overlayRef.hostElement.classList.remove(cB),this.detach(),this._positionChanges.complete(),this._overlayRef=this._boundingBox=null,this._isDisposed=!0)}reapplyLastPosition(){if(this._isDisposed||!this._platform.isBrowser)return;let n=this._lastPosition;if(n){this._originRect=this._getOriginRect(),this._overlayRect=this._pane.getBoundingClientRect(),this._viewportRect=this._getNarrowedViewportRect(),this._containerRect=this._overlayContainer.getContainerElement().getBoundingClientRect();let e=this._getOriginPoint(this._originRect,this._containerRect,n);this._applyPosition(n,e)}else this.apply()}withScrollableContainers(n){return this._scrollables=n,this}withPositions(n){return this._preferredPositions=n,n.indexOf(this._lastPosition)===-1&&(this._lastPosition=null),this._validatePositions(),this}withViewportMargin(n){return this._viewportMargin=n,this}withFlexibleDimensions(n=!0){return this._hasFlexibleDimensions=n,this}withGrowAfterOpen(n=!0){return this._growAfterOpen=n,this}withPush(n=!0){return this._canPush=n,this}withLockedPosition(n=!0){return this._positionLocked=n,this}setOrigin(n){return this._origin=n,this}withDefaultOffsetX(n){return this._offsetX=n,this}withDefaultOffsetY(n){return this._offsetY=n,this}withTransformOriginOn(n){return this._transformOriginSelector=n,this}_getOriginPoint(n,e,i){let r;if(i.originX=="center")r=n.left+n.width/2;else{let a=this._isRtl()?n.right:n.left,s=this._isRtl()?n.left:n.right;r=i.originX=="start"?a:s}e.left<0&&(r-=e.left);let o;return i.originY=="center"?o=n.top+n.height/2:o=i.originY=="top"?n.top:n.bottom,e.top<0&&(o-=e.top),{x:r,y:o}}_getOverlayPoint(n,e,i){let r;i.overlayX=="center"?r=-e.width/2:i.overlayX==="start"?r=this._isRtl()?-e.width:0:r=this._isRtl()?0:-e.width;let o;return i.overlayY=="center"?o=-e.height/2:o=i.overlayY=="top"?0:-e.height,{x:n.x+r,y:n.y+o}}_getOverlayFit(n,e,i,r){let o=uB(e),{x:a,y:s}=n,l=this._getOffset(r,"x"),c=this._getOffset(r,"y");l&&(a+=l),c&&(s+=c);let d=0-a,p=a+o.width-i.width,_=0-s,b=s+o.height-i.height,y=this._subtractOverflows(o.width,d,p),w=this._subtractOverflows(o.height,_,b),C=y*w;return{visibleArea:C,isCompletelyWithinViewport:o.width*o.height===C,fitsInViewportVertically:w===o.height,fitsInViewportHorizontally:y==o.width}}_canFitWithFlexibleDimensions(n,e,i){if(this._hasFlexibleDimensions){let r=i.bottom-e.y,o=i.right-e.x,a=dB(this._overlayRef.getConfig().minHeight),s=dB(this._overlayRef.getConfig().minWidth),l=n.fitsInViewportVertically||a!=null&&a<=r,c=n.fitsInViewportHorizontally||s!=null&&s<=o;return l&&c}return!1}_pushOverlayOnScreen(n,e,i){if(this._previousPushAmount&&this._positionLocked)return{x:n.x+this._previousPushAmount.x,y:n.y+this._previousPushAmount.y};let r=uB(e),o=this._viewportRect,a=Math.max(n.x+r.width-o.width,0),s=Math.max(n.y+r.height-o.height,0),l=Math.max(o.top-i.top-n.y,0),c=Math.max(o.left-i.left-n.x,0),d=0,p=0;return r.width<=o.width?d=c||-a:d=n.xy&&!this._isInitialRender&&!this._growAfterOpen&&(a=n.y-y/2)}let l=e.overlayX==="start"&&!r||e.overlayX==="end"&&r,c=e.overlayX==="end"&&!r||e.overlayX==="start"&&r,d,p,_;if(c)_=i.width-n.x+this._viewportMargin*2,d=n.x-this._viewportMargin;else if(l)p=n.x,d=i.right-n.x;else{let b=Math.min(i.right-n.x+i.left,n.x),y=this._lastBoundingBoxSize.width;d=b*2,p=n.x-b,d>y&&!this._isInitialRender&&!this._growAfterOpen&&(p=n.x-y/2)}return{top:a,left:p,bottom:s,right:_,width:d,height:o}}_setBoundingBoxStyles(n,e){let i=this._calculateBoundingBoxRect(n,e);!this._isInitialRender&&!this._growAfterOpen&&(i.height=Math.min(i.height,this._lastBoundingBoxSize.height),i.width=Math.min(i.width,this._lastBoundingBoxSize.width));let r={};if(this._hasExactPosition())r.top=r.left="0",r.bottom=r.right=r.maxHeight=r.maxWidth="",r.width=r.height="100%";else{let o=this._overlayRef.getConfig().maxHeight,a=this._overlayRef.getConfig().maxWidth;r.height=dn(i.height),r.top=dn(i.top),r.bottom=dn(i.bottom),r.width=dn(i.width),r.left=dn(i.left),r.right=dn(i.right),e.overlayX==="center"?r.alignItems="center":r.alignItems=e.overlayX==="end"?"flex-end":"flex-start",e.overlayY==="center"?r.justifyContent="center":r.justifyContent=e.overlayY==="bottom"?"flex-end":"flex-start",o&&(r.maxHeight=dn(o)),a&&(r.maxWidth=dn(a))}this._lastBoundingBoxSize=i,mu(this._boundingBox.style,r)}_resetBoundingBoxStyles(){mu(this._boundingBox.style,{top:"0",left:"0",right:"0",bottom:"0",height:"",width:"",alignItems:"",justifyContent:""})}_resetOverlayElementStyles(){mu(this._pane.style,{top:"",left:"",bottom:"",right:"",position:"",transform:""})}_setOverlayElementStyles(n,e){let i={},r=this._hasExactPosition(),o=this._hasFlexibleDimensions,a=this._overlayRef.getConfig();if(r){let d=this._viewportRuler.getViewportScrollPosition();mu(i,this._getExactOverlayY(e,n,d)),mu(i,this._getExactOverlayX(e,n,d))}else i.position="static";let s="",l=this._getOffset(e,"x"),c=this._getOffset(e,"y");l&&(s+=`translateX(${l}px) `),c&&(s+=`translateY(${c}px)`),i.transform=s.trim(),a.maxHeight&&(r?i.maxHeight=dn(a.maxHeight):o&&(i.maxHeight="")),a.maxWidth&&(r?i.maxWidth=dn(a.maxWidth):o&&(i.maxWidth="")),mu(this._pane.style,i)}_getExactOverlayY(n,e,i){let r={top:"",bottom:""},o=this._getOverlayPoint(e,this._overlayRect,n);if(this._isPushed&&(o=this._pushOverlayOnScreen(o,this._overlayRect,i)),n.overlayY==="bottom"){let a=this._document.documentElement.clientHeight;r.bottom=`${a-(o.y+this._overlayRect.height)}px`}else r.top=dn(o.y);return r}_getExactOverlayX(n,e,i){let r={left:"",right:""},o=this._getOverlayPoint(e,this._overlayRect,n);this._isPushed&&(o=this._pushOverlayOnScreen(o,this._overlayRect,i));let a;if(this._isRtl()?a=n.overlayX==="end"?"left":"right":a=n.overlayX==="end"?"right":"left",a==="right"){let s=this._document.documentElement.clientWidth;r.right=`${s-(o.x+this._overlayRect.width)}px`}else r.left=dn(o.x);return r}_getScrollVisibility(){let n=this._getOriginRect(),e=this._pane.getBoundingClientRect(),i=this._scrollables.map(r=>r.getElementRef().nativeElement.getBoundingClientRect());return{isOriginClipped:sB(n,i),isOriginOutsideView:XS(n,i),isOverlayClipped:sB(e,i),isOverlayOutsideView:XS(e,i)}}_subtractOverflows(n,...e){return e.reduce((i,r)=>i-Math.max(r,0),n)}_getNarrowedViewportRect(){let n=this._document.documentElement.clientWidth,e=this._document.documentElement.clientHeight,i=this._viewportRuler.getViewportScrollPosition();return{top:i.top+this._viewportMargin,left:i.left+this._viewportMargin,right:i.left+n-this._viewportMargin,bottom:i.top+e-this._viewportMargin,width:n-2*this._viewportMargin,height:e-2*this._viewportMargin}}_isRtl(){return this._overlayRef.getDirection()==="rtl"}_hasExactPosition(){return!this._hasFlexibleDimensions||this._isPushed}_getOffset(n,e){return e==="x"?n.offsetX==null?this._offsetX:n.offsetX:n.offsetY==null?this._offsetY:n.offsetY}_validatePositions(){}_addPanelClasses(n){this._pane&&Dl(n).forEach(e=>{e!==""&&this._appliedPanelClasses.indexOf(e)===-1&&(this._appliedPanelClasses.push(e),this._pane.classList.add(e))})}_clearPanelClasses(){this._pane&&(this._appliedPanelClasses.forEach(n=>{this._pane.classList.remove(n)}),this._appliedPanelClasses=[])}_getOriginRect(){let n=this._origin;if(n instanceof Y)return n.nativeElement.getBoundingClientRect();if(n instanceof Element)return n.getBoundingClientRect();let e=n.width||0,i=n.height||0;return{top:n.y,bottom:n.y+i,left:n.x,right:n.x+e,height:i,width:e}}};function mu(t,n){for(let e in n)n.hasOwnProperty(e)&&(t[e]=n[e]);return t}function dB(t){if(typeof t!="number"&&t!=null){let[n,e]=t.split(FJ);return!e||e==="px"?parseFloat(n):null}return t||null}function uB(t){return{top:Math.floor(t.top),right:Math.floor(t.right),bottom:Math.floor(t.bottom),left:Math.floor(t.left),width:Math.floor(t.width),height:Math.floor(t.height)}}function NJ(t,n){return t===n?!0:t.isOriginClipped===n.isOriginClipped&&t.isOriginOutsideView===n.isOriginOutsideView&&t.isOverlayClipped===n.isOverlayClipped&&t.isOverlayOutsideView===n.isOverlayOutsideView}var mB="cdk-global-overlay-wrapper";function Us(t){return new ox}var ox=class{_overlayRef;_cssPosition="static";_topOffset="";_bottomOffset="";_alignItems="";_xPosition="";_xOffset="";_width="";_height="";_isDisposed=!1;attach(n){let e=n.getConfig();this._overlayRef=n,this._width&&!e.width&&n.updateSize({width:this._width}),this._height&&!e.height&&n.updateSize({height:this._height}),n.hostElement.classList.add(mB),this._isDisposed=!1}top(n=""){return this._bottomOffset="",this._topOffset=n,this._alignItems="flex-start",this}left(n=""){return this._xOffset=n,this._xPosition="left",this}bottom(n=""){return this._topOffset="",this._bottomOffset=n,this._alignItems="flex-end",this}right(n=""){return this._xOffset=n,this._xPosition="right",this}start(n=""){return this._xOffset=n,this._xPosition="start",this}end(n=""){return this._xOffset=n,this._xPosition="end",this}width(n=""){return this._overlayRef?this._overlayRef.updateSize({width:n}):this._width=n,this}height(n=""){return this._overlayRef?this._overlayRef.updateSize({height:n}):this._height=n,this}centerHorizontally(n=""){return this.left(n),this._xPosition="center",this}centerVertically(n=""){return this.top(n),this._alignItems="center",this}apply(){if(!this._overlayRef||!this._overlayRef.hasAttached())return;let n=this._overlayRef.overlayElement.style,e=this._overlayRef.hostElement.style,i=this._overlayRef.getConfig(),{width:r,height:o,maxWidth:a,maxHeight:s}=i,l=(r==="100%"||r==="100vw")&&(!a||a==="100%"||a==="100vw"),c=(o==="100%"||o==="100vh")&&(!s||s==="100%"||s==="100vh"),d=this._xPosition,p=this._xOffset,_=this._overlayRef.getConfig().direction==="rtl",b="",y="",w="";l?w="flex-start":d==="center"?(w="center",_?y=p:b=p):_?d==="left"||d==="end"?(w="flex-end",b=p):(d==="right"||d==="start")&&(w="flex-start",y=p):d==="left"||d==="start"?(w="flex-start",b=p):(d==="right"||d==="end")&&(w="flex-end",y=p),n.position=this._cssPosition,n.marginLeft=l?"0":b,n.marginTop=c?"0":this._topOffset,n.marginBottom=this._bottomOffset,n.marginRight=l?"0":y,e.justifyContent=w,e.alignItems=c?"flex-start":this._alignItems}dispose(){if(this._isDisposed||!this._overlayRef)return;let n=this._overlayRef.overlayElement.style,e=this._overlayRef.hostElement,i=e.style;e.classList.remove(mB),i.justifyContent=i.alignItems=n.marginTop=n.marginBottom=n.marginLeft=n.marginRight=n.position="",this._overlayRef=null,this._isDisposed=!0}},vB=(()=>{class t{_injector=u(de);constructor(){}global(){return Us()}flexibleConnectedTo(e){return Ja(this._injector,e)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function qr(t,n){t.get(ft).load(bB);let e=t.get(ax),i=t.get(_e),r=t.get(et),o=t.get(tr),a=t.get(Yt),s=i.createElement("div"),l=i.createElement("div");l.id=r.getId("cdk-overlay-"),l.classList.add("cdk-overlay-pane"),s.appendChild(l),e.getContainerElement().appendChild(s);let c=new ug(l,o,t),d=new Gr(n),p=t.get(ze,null,{optional:!0})||t.get(hn).createRenderer(null,null);return d.direction=d.direction||a.value,new xh(c,s,l,d,t.get(ae),t.get(gB),i,t.get(ks),t.get(_B),n?.disableAnimations??t.get(ef,null,{optional:!0})==="NoopAnimations",t.get(ti),p)}var yB=(()=>{class t{scrollStrategies=u(pB);_positionBuilder=u(vB);_injector=u(de);constructor(){}create(e){return qr(this._injector,e)}position(){return this._positionBuilder}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),LJ=[{originX:"start",originY:"bottom",overlayX:"start",overlayY:"top"},{originX:"start",originY:"top",overlayX:"start",overlayY:"bottom"},{originX:"end",originY:"top",overlayX:"end",overlayY:"bottom"},{originX:"end",originY:"bottom",overlayX:"end",overlayY:"top"}],xB=new O("cdk-connected-overlay-scroll-strategy",{providedIn:"root",factory:()=>{let t=u(de);return()=>Tn(t)}}),wh=(()=>{class t{elementRef=u(Y);constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdk-overlay-origin",""],["","overlay-origin",""],["","cdkOverlayOrigin",""]],exportAs:["cdkOverlayOrigin"]})}return t})(),sx=(()=>{class t{_dir=u(Yt,{optional:!0});_injector=u(de);_overlayRef;_templatePortal;_backdropSubscription=ke.EMPTY;_attachSubscription=ke.EMPTY;_detachSubscription=ke.EMPTY;_positionSubscription=ke.EMPTY;_offsetX;_offsetY;_position;_scrollStrategyFactory=u(xB);_disposeOnNavigation=!1;_ngZone=u(ae);origin;positions;positionStrategy;get offsetX(){return this._offsetX}set offsetX(e){this._offsetX=e,this._position&&this._updatePositionStrategy(this._position)}get offsetY(){return this._offsetY}set offsetY(e){this._offsetY=e,this._position&&this._updatePositionStrategy(this._position)}width;height;minWidth;minHeight;backdropClass;panelClass;viewportMargin=0;scrollStrategy;open=!1;disableClose=!1;transformOriginSelector;hasBackdrop=!1;lockPosition=!1;flexibleDimensions=!1;growAfterOpen=!1;push=!1;get disposeOnNavigation(){return this._disposeOnNavigation}set disposeOnNavigation(e){this._disposeOnNavigation=e}backdropClick=new U;positionChange=new U;attach=new U;detach=new U;overlayKeydown=new U;overlayOutsideClick=new U;constructor(){let e=u(te),i=u(st);this._templatePortal=new kn(e,i),this.scrollStrategy=this._scrollStrategyFactory()}get overlayRef(){return this._overlayRef}get dir(){return this._dir?this._dir.value:"ltr"}ngOnDestroy(){this._attachSubscription.unsubscribe(),this._detachSubscription.unsubscribe(),this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this._overlayRef?.dispose()}ngOnChanges(e){this._position&&(this._updatePositionStrategy(this._position),this._overlayRef?.updateSize({width:this.width,minWidth:this.minWidth,height:this.height,minHeight:this.minHeight}),e.origin&&this.open&&this._position.apply()),e.open&&(this.open?this.attachOverlay():this.detachOverlay())}_createOverlay(){(!this.positions||!this.positions.length)&&(this.positions=LJ);let e=this._overlayRef=qr(this._injector,this._buildConfig());this._attachSubscription=e.attachments().subscribe(()=>this.attach.emit()),this._detachSubscription=e.detachments().subscribe(()=>this.detach.emit()),e.keydownEvents().subscribe(i=>{this.overlayKeydown.next(i),i.keyCode===27&&!this.disableClose&&!Gt(i)&&(i.preventDefault(),this.detachOverlay())}),this._overlayRef.outsidePointerEvents().subscribe(i=>{let r=this._getOriginElement(),o=or(i);(!r||r!==o&&!r.contains(o))&&this.overlayOutsideClick.next(i)})}_buildConfig(){let e=this._position=this.positionStrategy||this._createPositionStrategy(),i=new Gr({direction:this._dir||"ltr",positionStrategy:e,scrollStrategy:this.scrollStrategy,hasBackdrop:this.hasBackdrop,disposeOnNavigation:this.disposeOnNavigation});return(this.width||this.width===0)&&(i.width=this.width),(this.height||this.height===0)&&(i.height=this.height),(this.minWidth||this.minWidth===0)&&(i.minWidth=this.minWidth),(this.minHeight||this.minHeight===0)&&(i.minHeight=this.minHeight),this.backdropClass&&(i.backdropClass=this.backdropClass),this.panelClass&&(i.panelClass=this.panelClass),i}_updatePositionStrategy(e){let i=this.positions.map(r=>({originX:r.originX,originY:r.originY,overlayX:r.overlayX,overlayY:r.overlayY,offsetX:r.offsetX||this.offsetX,offsetY:r.offsetY||this.offsetY,panelClass:r.panelClass||void 0}));return e.setOrigin(this._getOrigin()).withPositions(i).withFlexibleDimensions(this.flexibleDimensions).withPush(this.push).withGrowAfterOpen(this.growAfterOpen).withViewportMargin(this.viewportMargin).withLockedPosition(this.lockPosition).withTransformOriginOn(this.transformOriginSelector)}_createPositionStrategy(){let e=Ja(this._injector,this._getOrigin());return this._updatePositionStrategy(e),e}_getOrigin(){return this.origin instanceof wh?this.origin.elementRef:this.origin}_getOriginElement(){return this.origin instanceof wh?this.origin.elementRef.nativeElement:this.origin instanceof Y?this.origin.nativeElement:typeof Element<"u"&&this.origin instanceof Element?this.origin:null}attachOverlay(){this._overlayRef?this._overlayRef.getConfig().hasBackdrop=this.hasBackdrop:this._createOverlay(),this._overlayRef.hasAttached()||this._overlayRef.attach(this._templatePortal),this.hasBackdrop?this._backdropSubscription=this._overlayRef.backdropClick().subscribe(e=>{this.backdropClick.emit(e)}):this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this.positionChange.observers.length>0&&(this._positionSubscription=this._position.positionChanges.pipe(oD(()=>this.positionChange.observers.length>0)).subscribe(e=>{this._ngZone.run(()=>this.positionChange.emit(e)),this.positionChange.observers.length===0&&this._positionSubscription.unsubscribe()})),this.open=!0}detachOverlay(){this._overlayRef?.detach(),this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this.open=!1}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdk-connected-overlay",""],["","connected-overlay",""],["","cdkConnectedOverlay",""]],inputs:{origin:[0,"cdkConnectedOverlayOrigin","origin"],positions:[0,"cdkConnectedOverlayPositions","positions"],positionStrategy:[0,"cdkConnectedOverlayPositionStrategy","positionStrategy"],offsetX:[0,"cdkConnectedOverlayOffsetX","offsetX"],offsetY:[0,"cdkConnectedOverlayOffsetY","offsetY"],width:[0,"cdkConnectedOverlayWidth","width"],height:[0,"cdkConnectedOverlayHeight","height"],minWidth:[0,"cdkConnectedOverlayMinWidth","minWidth"],minHeight:[0,"cdkConnectedOverlayMinHeight","minHeight"],backdropClass:[0,"cdkConnectedOverlayBackdropClass","backdropClass"],panelClass:[0,"cdkConnectedOverlayPanelClass","panelClass"],viewportMargin:[0,"cdkConnectedOverlayViewportMargin","viewportMargin"],scrollStrategy:[0,"cdkConnectedOverlayScrollStrategy","scrollStrategy"],open:[0,"cdkConnectedOverlayOpen","open"],disableClose:[0,"cdkConnectedOverlayDisableClose","disableClose"],transformOriginSelector:[0,"cdkConnectedOverlayTransformOriginOn","transformOriginSelector"],hasBackdrop:[2,"cdkConnectedOverlayHasBackdrop","hasBackdrop",B],lockPosition:[2,"cdkConnectedOverlayLockPosition","lockPosition",B],flexibleDimensions:[2,"cdkConnectedOverlayFlexibleDimensions","flexibleDimensions",B],growAfterOpen:[2,"cdkConnectedOverlayGrowAfterOpen","growAfterOpen",B],push:[2,"cdkConnectedOverlayPush","push",B],disposeOnNavigation:[2,"cdkConnectedOverlayDisposeOnNavigation","disposeOnNavigation",B]},outputs:{backdropClick:"backdropClick",positionChange:"positionChange",attach:"attach",detach:"detach",overlayKeydown:"overlayKeydown",overlayOutsideClick:"overlayOutsideClick"},exportAs:["cdkConnectedOverlay"],features:[Oe]})}return t})();function VJ(t){let n=u(de);return()=>Tn(n)}var BJ={provide:xB,useFactory:VJ},cr=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[yB,BJ],imports:[Ns,Oo,cg,cg]})}return t})();var hu=(()=>{class t{_listeners=[];notify(e,i){for(let r of this._listeners)r(e,i)}listen(e){return this._listeners.push(e),()=>{this._listeners=this._listeners.filter(i=>e!==i)}}ngOnDestroy(){this._listeners=[]}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var pu=class{applyChanges(n,e,i,r,o){n.forEachOperation((a,s,l)=>{let c,d;if(a.previousIndex==null){let p=i(a,s,l);c=e.createEmbeddedView(p.templateRef,p.context,p.index),d=Sc.INSERTED}else l==null?(e.remove(s),d=Sc.REMOVED):(c=e.get(s),e.move(c,l),d=Sc.MOVED);o&&o({context:c?.context,operation:d,record:a})})}detach(){}};var hg=class{_multiple;_emitChanges;compareWith;_selection=new Set;_deselectedToEmit=[];_selectedToEmit=[];_selected;get selected(){return this._selected||(this._selected=Array.from(this._selection.values())),this._selected}changed=new z;constructor(n=!1,e,i=!0,r){this._multiple=n,this._emitChanges=i,this.compareWith=r,e&&e.length&&(n?e.forEach(o=>this._markSelected(o)):this._markSelected(e[0]),this._selectedToEmit.length=0)}select(...n){this._verifyValueAssignment(n),n.forEach(i=>this._markSelected(i));let e=this._hasQueuedChanges();return this._emitChangeEvent(),e}deselect(...n){this._verifyValueAssignment(n),n.forEach(i=>this._unmarkSelected(i));let e=this._hasQueuedChanges();return this._emitChangeEvent(),e}setSelection(...n){this._verifyValueAssignment(n);let e=this.selected,i=new Set(n.map(o=>this._getConcreteValue(o)));n.forEach(o=>this._markSelected(o)),e.filter(o=>!i.has(this._getConcreteValue(o,i))).forEach(o=>this._unmarkSelected(o));let r=this._hasQueuedChanges();return this._emitChangeEvent(),r}toggle(n){return this.isSelected(n)?this.deselect(n):this.select(n)}clear(n=!0){this._unmarkAll();let e=this._hasQueuedChanges();return n&&this._emitChangeEvent(),e}isSelected(n){return this._selection.has(this._getConcreteValue(n))}isEmpty(){return this._selection.size===0}hasValue(){return!this.isEmpty()}sort(n){this._multiple&&this.selected&&this._selected.sort(n)}isMultipleSelection(){return this._multiple}_emitChangeEvent(){this._selected=null,(this._selectedToEmit.length||this._deselectedToEmit.length)&&(this.changed.next({source:this,added:this._selectedToEmit,removed:this._deselectedToEmit}),this._deselectedToEmit=[],this._selectedToEmit=[])}_markSelected(n){n=this._getConcreteValue(n),this.isSelected(n)||(this._multiple||this._unmarkAll(),this.isSelected(n)||this._selection.add(n),this._emitChanges&&this._selectedToEmit.push(n))}_unmarkSelected(n){n=this._getConcreteValue(n),this.isSelected(n)&&(this._selection.delete(n),this._emitChanges&&this._deselectedToEmit.push(n))}_unmarkAll(){this.isEmpty()||this._selection.forEach(n=>this._unmarkSelected(n))}_verifyValueAssignment(n){n.length>1&&this._multiple}_hasQueuedChanges(){return!!(this._deselectedToEmit.length||this._selectedToEmit.length)}_getConcreteValue(n,e){if(this.compareWith){e=e??this._selection;for(let i of e)if(this.compareWith(n,i))return i;return n}else return n}};var IB=(()=>{class t{_renderer;_elementRef;onChange=e=>{};onTouched=()=>{};constructor(e,i){this._renderer=e,this._elementRef=i}setProperty(e,i){this._renderer.setProperty(this._elementRef.nativeElement,e,i)}registerOnTouched(e){this.onTouched=e}registerOnChange(e){this.onChange=e}setDisabledState(e){this.setProperty("disabled",e)}static \u0275fac=function(i){return new(i||t)(be(ze),be(Y))};static \u0275dir=P({type:t})}return t})(),AB=(()=>{class t extends IB{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,features:[le]})}return t})(),dr=new O("");var jJ={provide:dr,useExisting:li(()=>di),multi:!0};function HJ(){let t=Xo()?Xo().getUserAgent():"";return/android (\d+)/.test(t.toLowerCase())}var zJ=new O(""),di=(()=>{class t extends IB{_compositionMode;_composing=!1;constructor(e,i,r){super(e,i),this._compositionMode=r,this._compositionMode==null&&(this._compositionMode=!HJ())}writeValue(e){let i=e??"";this.setProperty("value",i)}_handleInput(e){(!this._compositionMode||this._compositionMode&&!this._composing)&&this.onChange(e)}_compositionStart(){this._composing=!0}_compositionEnd(e){this._composing=!1,this._compositionMode&&this.onChange(e)}static \u0275fac=function(i){return new(i||t)(be(ze),be(Y),be(zJ,8))};static \u0275dir=P({type:t,selectors:[["input","formControlName","",3,"type","checkbox"],["textarea","formControlName",""],["input","formControl","",3,"type","checkbox"],["textarea","formControl",""],["input","ngModel","",3,"type","checkbox"],["textarea","ngModel",""],["","ngDefaultControl",""]],hostBindings:function(i,r){i&1&&S("input",function(a){return r._handleInput(a.target.value)})("blur",function(){return r.onTouched()})("compositionstart",function(){return r._compositionStart()})("compositionend",function(a){return r._compositionEnd(a.target.value)})},standalone:!1,features:[we([jJ]),le]})}return t})();function ik(t){return t==null||nk(t)===0}function nk(t){return t==null?null:Array.isArray(t)||typeof t=="string"?t.length:t instanceof Set?t.size:null}var sa=new O(""),xg=new O(""),UJ=/^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,Ve=class{static min(n){return $J(n)}static max(n){return WJ(n)}static required(n){return OB(n)}static requiredTrue(n){return GJ(n)}static email(n){return qJ(n)}static minLength(n){return YJ(n)}static maxLength(n){return QJ(n)}static pattern(n){return KJ(n)}static nullValidator(n){return cx()}static compose(n){return VB(n)}static composeAsync(n){return BB(n)}};function $J(t){return n=>{if(n.value==null||t==null)return null;let e=parseFloat(n.value);return!isNaN(e)&&e{if(n.value==null||t==null)return null;let e=parseFloat(n.value);return!isNaN(e)&&e>t?{max:{max:t,actual:n.value}}:null}}function OB(t){return ik(t.value)?{required:!0}:null}function GJ(t){return t.value===!0?null:{required:!0}}function qJ(t){return ik(t.value)||UJ.test(t.value)?null:{email:!0}}function YJ(t){return n=>{let e=n.value?.length??nk(n.value);return e===null||e===0?null:e{let e=n.value?.length??nk(n.value);return e!==null&&e>t?{maxlength:{requiredLength:t,actualLength:e}}:null}}function KJ(t){if(!t)return cx;let n,e;return typeof t=="string"?(e="",t.charAt(0)!=="^"&&(e+="^"),e+=t,t.charAt(t.length-1)!=="$"&&(e+="$"),n=new RegExp(e)):(e=t.toString(),n=t),i=>{if(ik(i.value))return null;let r=i.value;return n.test(r)?null:{pattern:{requiredPattern:e,actualValue:r}}}}function cx(t){return null}function RB(t){return t!=null}function PB(t){return fl(t)?Ut(t):t}function FB(t){let n={};return t.forEach(e=>{n=e!=null?I(I({},n),e):n}),Object.keys(n).length===0?null:n}function NB(t,n){return n.map(e=>e(t))}function ZJ(t){return!t.validate}function LB(t){return t.map(n=>ZJ(n)?n:e=>n.validate(e))}function VB(t){if(!t)return null;let n=t.filter(RB);return n.length==0?null:function(e){return FB(NB(e,n))}}function rk(t){return t!=null?VB(LB(t)):null}function BB(t){if(!t)return null;let n=t.filter(RB);return n.length==0?null:function(e){let i=NB(e,n).map(PB);return cs(i).pipe(se(FB))}}function ok(t){return t!=null?BB(LB(t)):null}function wB(t,n){return t===null?[n]:Array.isArray(t)?[...t,n]:[t,n]}function jB(t){return t._rawValidators}function HB(t){return t._rawAsyncValidators}function ek(t){return t?Array.isArray(t)?t:[t]:[]}function dx(t,n){return Array.isArray(t)?t.includes(n):t===n}function DB(t,n){let e=ek(n);return ek(t).forEach(r=>{dx(e,r)||e.push(r)}),e}function MB(t,n){return ek(n).filter(e=>!dx(t,e))}var ux=class{get value(){return this.control?this.control.value:null}get valid(){return this.control?this.control.valid:null}get invalid(){return this.control?this.control.invalid:null}get pending(){return this.control?this.control.pending:null}get disabled(){return this.control?this.control.disabled:null}get enabled(){return this.control?this.control.enabled:null}get errors(){return this.control?this.control.errors:null}get pristine(){return this.control?this.control.pristine:null}get dirty(){return this.control?this.control.dirty:null}get touched(){return this.control?this.control.touched:null}get status(){return this.control?this.control.status:null}get untouched(){return this.control?this.control.untouched:null}get statusChanges(){return this.control?this.control.statusChanges:null}get valueChanges(){return this.control?this.control.valueChanges:null}get path(){return null}_composedValidatorFn;_composedAsyncValidatorFn;_rawValidators=[];_rawAsyncValidators=[];_setValidators(n){this._rawValidators=n||[],this._composedValidatorFn=rk(this._rawValidators)}_setAsyncValidators(n){this._rawAsyncValidators=n||[],this._composedAsyncValidatorFn=ok(this._rawAsyncValidators)}get validator(){return this._composedValidatorFn||null}get asyncValidator(){return this._composedAsyncValidatorFn||null}_onDestroyCallbacks=[];_registerOnDestroy(n){this._onDestroyCallbacks.push(n)}_invokeOnDestroyCallbacks(){this._onDestroyCallbacks.forEach(n=>n()),this._onDestroyCallbacks=[]}reset(n=void 0){this.control&&this.control.reset(n)}hasError(n,e){return this.control?this.control.hasError(n,e):!1}getError(n,e){return this.control?this.control.getError(n,e):null}},$s=class extends ux{name;get formDirective(){return null}get path(){return null}},Kn=class extends ux{_parent=null;name=null;valueAccessor=null},mx=class{_cd;constructor(n){this._cd=n}get isTouched(){return this._cd?.control?._touched?.(),!!this._cd?.control?.touched}get isUntouched(){return!!this._cd?.control?.untouched}get isPristine(){return this._cd?.control?._pristine?.(),!!this._cd?.control?.pristine}get isDirty(){return!!this._cd?.control?.dirty}get isValid(){return this._cd?.control?._status?.(),!!this._cd?.control?.valid}get isInvalid(){return!!this._cd?.control?.invalid}get isPending(){return!!this._cd?.control?.pending}get isSubmitted(){return this._cd?._submitted?.(),!!this._cd?.submitted}},XJ={"[class.ng-untouched]":"isUntouched","[class.ng-touched]":"isTouched","[class.ng-pristine]":"isPristine","[class.ng-dirty]":"isDirty","[class.ng-valid]":"isValid","[class.ng-invalid]":"isInvalid","[class.ng-pending]":"isPending"},J7e=Me(I({},XJ),{"[class.ng-submitted]":"isSubmitted"}),Pt=(()=>{class t extends mx{constructor(e){super(e)}static \u0275fac=function(i){return new(i||t)(be(Kn,2))};static \u0275dir=P({type:t,selectors:[["","formControlName",""],["","ngModel",""],["","formControl",""]],hostVars:14,hostBindings:function(i,r){i&2&&G("ng-untouched",r.isUntouched)("ng-touched",r.isTouched)("ng-pristine",r.isPristine)("ng-dirty",r.isDirty)("ng-valid",r.isValid)("ng-invalid",r.isInvalid)("ng-pending",r.isPending)},standalone:!1,features:[le]})}return t})(),so=(()=>{class t extends mx{constructor(e){super(e)}static \u0275fac=function(i){return new(i||t)(be($s,10))};static \u0275dir=P({type:t,selectors:[["","formGroupName",""],["","formArrayName",""],["","ngModelGroup",""],["","formGroup",""],["form",3,"ngNoForm",""],["","ngForm",""]],hostVars:16,hostBindings:function(i,r){i&2&&G("ng-untouched",r.isUntouched)("ng-touched",r.isTouched)("ng-pristine",r.isPristine)("ng-dirty",r.isDirty)("ng-valid",r.isValid)("ng-invalid",r.isInvalid)("ng-pending",r.isPending)("ng-submitted",r.isSubmitted)},standalone:!1,features:[le]})}return t})();var pg="VALID",lx="INVALID",Dh="PENDING",fg="DISABLED",Ic=class{},hx=class extends Ic{value;source;constructor(n,e){super(),this.value=n,this.source=e}},_g=class extends Ic{pristine;source;constructor(n,e){super(),this.pristine=n,this.source=e}},bg=class extends Ic{touched;source;constructor(n,e){super(),this.touched=n,this.source=e}},Mh=class extends Ic{status;source;constructor(n,e){super(),this.status=n,this.source=e}},px=class extends Ic{source;constructor(n){super(),this.source=n}},vg=class extends Ic{source;constructor(n){super(),this.source=n}};function ak(t){return(vx(t)?t.validators:t)||null}function JJ(t){return Array.isArray(t)?rk(t):t||null}function sk(t,n){return(vx(n)?n.asyncValidators:t)||null}function eee(t){return Array.isArray(t)?ok(t):t||null}function vx(t){return t!=null&&!Array.isArray(t)&&typeof t=="object"}function zB(t,n,e){let i=t.controls;if(!(n?Object.keys(i):i).length)throw new ue(1e3,"");if(!i[e])throw new ue(1001,"")}function UB(t,n,e){t._forEachChild((i,r)=>{if(e[r]===void 0)throw new ue(1002,"")})}var Ac=class{_pendingDirty=!1;_hasOwnPendingAsyncValidator=null;_pendingTouched=!1;_onCollectionChange=()=>{};_updateOn;_parent=null;_asyncValidationSubscription;_composedValidatorFn;_composedAsyncValidatorFn;_rawValidators;_rawAsyncValidators;value;constructor(n,e){this._assignValidators(n),this._assignAsyncValidators(e)}get validator(){return this._composedValidatorFn}set validator(n){this._rawValidators=this._composedValidatorFn=n}get asyncValidator(){return this._composedAsyncValidatorFn}set asyncValidator(n){this._rawAsyncValidators=this._composedAsyncValidatorFn=n}get parent(){return this._parent}get status(){return Ni(this.statusReactive)}set status(n){Ni(()=>this.statusReactive.set(n))}_status=ci(()=>this.statusReactive());statusReactive=he(void 0);get valid(){return this.status===pg}get invalid(){return this.status===lx}get pending(){return this.status==Dh}get disabled(){return this.status===fg}get enabled(){return this.status!==fg}errors;get pristine(){return Ni(this.pristineReactive)}set pristine(n){Ni(()=>this.pristineReactive.set(n))}_pristine=ci(()=>this.pristineReactive());pristineReactive=he(!0);get dirty(){return!this.pristine}get touched(){return Ni(this.touchedReactive)}set touched(n){Ni(()=>this.touchedReactive.set(n))}_touched=ci(()=>this.touchedReactive());touchedReactive=he(!1);get untouched(){return!this.touched}_events=new z;events=this._events.asObservable();valueChanges;statusChanges;get updateOn(){return this._updateOn?this._updateOn:this.parent?this.parent.updateOn:"change"}setValidators(n){this._assignValidators(n)}setAsyncValidators(n){this._assignAsyncValidators(n)}addValidators(n){this.setValidators(DB(n,this._rawValidators))}addAsyncValidators(n){this.setAsyncValidators(DB(n,this._rawAsyncValidators))}removeValidators(n){this.setValidators(MB(n,this._rawValidators))}removeAsyncValidators(n){this.setAsyncValidators(MB(n,this._rawAsyncValidators))}hasValidator(n){return dx(this._rawValidators,n)}hasAsyncValidator(n){return dx(this._rawAsyncValidators,n)}clearValidators(){this.validator=null}clearAsyncValidators(){this.asyncValidator=null}markAsTouched(n={}){let e=this.touched===!1;this.touched=!0;let i=n.sourceControl??this;this._parent&&!n.onlySelf&&this._parent.markAsTouched(Me(I({},n),{sourceControl:i})),e&&n.emitEvent!==!1&&this._events.next(new bg(!0,i))}markAllAsDirty(n={}){this.markAsDirty({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:this}),this._forEachChild(e=>e.markAllAsDirty(n))}markAllAsTouched(n={}){this.markAsTouched({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:this}),this._forEachChild(e=>e.markAllAsTouched(n))}markAsUntouched(n={}){let e=this.touched===!0;this.touched=!1,this._pendingTouched=!1;let i=n.sourceControl??this;this._forEachChild(r=>{r.markAsUntouched({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:i})}),this._parent&&!n.onlySelf&&this._parent._updateTouched(n,i),e&&n.emitEvent!==!1&&this._events.next(new bg(!1,i))}markAsDirty(n={}){let e=this.pristine===!0;this.pristine=!1;let i=n.sourceControl??this;this._parent&&!n.onlySelf&&this._parent.markAsDirty(Me(I({},n),{sourceControl:i})),e&&n.emitEvent!==!1&&this._events.next(new _g(!1,i))}markAsPristine(n={}){let e=this.pristine===!1;this.pristine=!0,this._pendingDirty=!1;let i=n.sourceControl??this;this._forEachChild(r=>{r.markAsPristine({onlySelf:!0,emitEvent:n.emitEvent})}),this._parent&&!n.onlySelf&&this._parent._updatePristine(n,i),e&&n.emitEvent!==!1&&this._events.next(new _g(!0,i))}markAsPending(n={}){this.status=Dh;let e=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new Mh(this.status,e)),this.statusChanges.emit(this.status)),this._parent&&!n.onlySelf&&this._parent.markAsPending(Me(I({},n),{sourceControl:e}))}disable(n={}){let e=this._parentMarkedDirty(n.onlySelf);this.status=fg,this.errors=null,this._forEachChild(r=>{r.disable(Me(I({},n),{onlySelf:!0}))}),this._updateValue();let i=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new hx(this.value,i)),this._events.next(new Mh(this.status,i)),this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._updateAncestors(Me(I({},n),{skipPristineCheck:e}),this),this._onDisabledChange.forEach(r=>r(!0))}enable(n={}){let e=this._parentMarkedDirty(n.onlySelf);this.status=pg,this._forEachChild(i=>{i.enable(Me(I({},n),{onlySelf:!0}))}),this.updateValueAndValidity({onlySelf:!0,emitEvent:n.emitEvent}),this._updateAncestors(Me(I({},n),{skipPristineCheck:e}),this),this._onDisabledChange.forEach(i=>i(!1))}_updateAncestors(n,e){this._parent&&!n.onlySelf&&(this._parent.updateValueAndValidity(n),n.skipPristineCheck||this._parent._updatePristine({},e),this._parent._updateTouched({},e))}setParent(n){this._parent=n}getRawValue(){return this.value}updateValueAndValidity(n={}){if(this._setInitialStatus(),this._updateValue(),this.enabled){let i=this._cancelExistingSubscription();this.errors=this._runValidator(),this.status=this._calculateStatus(),(this.status===pg||this.status===Dh)&&this._runAsyncValidator(i,n.emitEvent)}let e=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new hx(this.value,e)),this._events.next(new Mh(this.status,e)),this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._parent&&!n.onlySelf&&this._parent.updateValueAndValidity(Me(I({},n),{sourceControl:e}))}_updateTreeValidity(n={emitEvent:!0}){this._forEachChild(e=>e._updateTreeValidity(n)),this.updateValueAndValidity({onlySelf:!0,emitEvent:n.emitEvent})}_setInitialStatus(){this.status=this._allControlsDisabled()?fg:pg}_runValidator(){return this.validator?this.validator(this):null}_runAsyncValidator(n,e){if(this.asyncValidator){this.status=Dh,this._hasOwnPendingAsyncValidator={emitEvent:e!==!1,shouldHaveEmitted:n!==!1};let i=PB(this.asyncValidator(this));this._asyncValidationSubscription=i.subscribe(r=>{this._hasOwnPendingAsyncValidator=null,this.setErrors(r,{emitEvent:e,shouldHaveEmitted:n})})}}_cancelExistingSubscription(){if(this._asyncValidationSubscription){this._asyncValidationSubscription.unsubscribe();let n=(this._hasOwnPendingAsyncValidator?.emitEvent||this._hasOwnPendingAsyncValidator?.shouldHaveEmitted)??!1;return this._hasOwnPendingAsyncValidator=null,n}return!1}setErrors(n,e={}){this.errors=n,this._updateControlsErrors(e.emitEvent!==!1,this,e.shouldHaveEmitted)}get(n){let e=n;return e==null||(Array.isArray(e)||(e=e.split(".")),e.length===0)?null:e.reduce((i,r)=>i&&i._find(r),this)}getError(n,e){let i=e?this.get(e):this;return i&&i.errors?i.errors[n]:null}hasError(n,e){return!!this.getError(n,e)}get root(){let n=this;for(;n._parent;)n=n._parent;return n}_updateControlsErrors(n,e,i){this.status=this._calculateStatus(),n&&this.statusChanges.emit(this.status),(n||i)&&this._events.next(new Mh(this.status,e)),this._parent&&this._parent._updateControlsErrors(n,e,i)}_initObservables(){this.valueChanges=new U,this.statusChanges=new U}_calculateStatus(){return this._allControlsDisabled()?fg:this.errors?lx:this._hasOwnPendingAsyncValidator||this._anyControlsHaveStatus(Dh)?Dh:this._anyControlsHaveStatus(lx)?lx:pg}_anyControlsHaveStatus(n){return this._anyControls(e=>e.status===n)}_anyControlsDirty(){return this._anyControls(n=>n.dirty)}_anyControlsTouched(){return this._anyControls(n=>n.touched)}_updatePristine(n,e){let i=!this._anyControlsDirty(),r=this.pristine!==i;this.pristine=i,this._parent&&!n.onlySelf&&this._parent._updatePristine(n,e),r&&this._events.next(new _g(this.pristine,e))}_updateTouched(n={},e){this.touched=this._anyControlsTouched(),this._events.next(new bg(this.touched,e)),this._parent&&!n.onlySelf&&this._parent._updateTouched(n,e)}_onDisabledChange=[];_registerOnCollectionChange(n){this._onCollectionChange=n}_setUpdateStrategy(n){vx(n)&&n.updateOn!=null&&(this._updateOn=n.updateOn)}_parentMarkedDirty(n){let e=this._parent&&this._parent.dirty;return!n&&!!e&&!this._parent._anyControlsDirty()}_find(n){return null}_assignValidators(n){this._rawValidators=Array.isArray(n)?n.slice():n,this._composedValidatorFn=JJ(this._rawValidators)}_assignAsyncValidators(n){this._rawAsyncValidators=Array.isArray(n)?n.slice():n,this._composedAsyncValidatorFn=eee(this._rawAsyncValidators)}},fu=class extends Ac{constructor(n,e,i){super(ak(e),sk(i,e)),this.controls=n,this._initObservables(),this._setUpdateStrategy(e),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}controls;registerControl(n,e){return this.controls[n]?this.controls[n]:(this.controls[n]=e,e.setParent(this),e._registerOnCollectionChange(this._onCollectionChange),e)}addControl(n,e,i={}){this.registerControl(n,e),this.updateValueAndValidity({emitEvent:i.emitEvent}),this._onCollectionChange()}removeControl(n,e={}){this.controls[n]&&this.controls[n]._registerOnCollectionChange(()=>{}),delete this.controls[n],this.updateValueAndValidity({emitEvent:e.emitEvent}),this._onCollectionChange()}setControl(n,e,i={}){this.controls[n]&&this.controls[n]._registerOnCollectionChange(()=>{}),delete this.controls[n],e&&this.registerControl(n,e),this.updateValueAndValidity({emitEvent:i.emitEvent}),this._onCollectionChange()}contains(n){return this.controls.hasOwnProperty(n)&&this.controls[n].enabled}setValue(n,e={}){UB(this,!0,n),Object.keys(n).forEach(i=>{zB(this,!0,i),this.controls[i].setValue(n[i],{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e)}patchValue(n,e={}){n!=null&&(Object.keys(n).forEach(i=>{let r=this.controls[i];r&&r.patchValue(n[i],{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e))}reset(n={},e={}){this._forEachChild((i,r)=>{i.reset(n?n[r]:null,{onlySelf:!0,emitEvent:e.emitEvent})}),this._updatePristine(e,this),this._updateTouched(e,this),this.updateValueAndValidity(e),e?.emitEvent!==!1&&this._events.next(new vg(this))}getRawValue(){return this._reduceChildren({},(n,e,i)=>(n[i]=e.getRawValue(),n))}_syncPendingControls(){let n=this._reduceChildren(!1,(e,i)=>i._syncPendingControls()?!0:e);return n&&this.updateValueAndValidity({onlySelf:!0}),n}_forEachChild(n){Object.keys(this.controls).forEach(e=>{let i=this.controls[e];i&&n(i,e)})}_setUpControls(){this._forEachChild(n=>{n.setParent(this),n._registerOnCollectionChange(this._onCollectionChange)})}_updateValue(){this.value=this._reduceValue()}_anyControls(n){for(let[e,i]of Object.entries(this.controls))if(this.contains(e)&&n(i))return!0;return!1}_reduceValue(){let n={};return this._reduceChildren(n,(e,i,r)=>((i.enabled||this.disabled)&&(e[r]=i.value),e))}_reduceChildren(n,e){let i=n;return this._forEachChild((r,o)=>{i=e(i,r,o)}),i}_allControlsDisabled(){for(let n of Object.keys(this.controls))if(this.controls[n].enabled)return!1;return Object.keys(this.controls).length>0||this.disabled}_find(n){return this.controls.hasOwnProperty(n)?this.controls[n]:null}};var yx=fu;var tk=class extends fu{};var Eh=new O("",{providedIn:"root",factory:()=>xx}),xx="always";function $B(t,n){return[...n.path,t]}function yg(t,n,e=xx){lk(t,n),n.valueAccessor.writeValue(t.value),(t.disabled||e==="always")&&n.valueAccessor.setDisabledState?.(t.disabled),iee(t,n),ree(t,n),nee(t,n),tee(t,n)}function fx(t,n,e=!0){let i=()=>{};n.valueAccessor&&(n.valueAccessor.registerOnChange(i),n.valueAccessor.registerOnTouched(i)),_x(t,n),t&&(n._invokeOnDestroyCallbacks(),t._registerOnCollectionChange(()=>{}))}function gx(t,n){t.forEach(e=>{e.registerOnValidatorChange&&e.registerOnValidatorChange(n)})}function tee(t,n){if(n.valueAccessor.setDisabledState){let e=i=>{n.valueAccessor.setDisabledState(i)};t.registerOnDisabledChange(e),n._registerOnDestroy(()=>{t._unregisterOnDisabledChange(e)})}}function lk(t,n){let e=jB(t);n.validator!==null?t.setValidators(wB(e,n.validator)):typeof e=="function"&&t.setValidators([e]);let i=HB(t);n.asyncValidator!==null?t.setAsyncValidators(wB(i,n.asyncValidator)):typeof i=="function"&&t.setAsyncValidators([i]);let r=()=>t.updateValueAndValidity();gx(n._rawValidators,r),gx(n._rawAsyncValidators,r)}function _x(t,n){let e=!1;if(t!==null){if(n.validator!==null){let r=jB(t);if(Array.isArray(r)&&r.length>0){let o=r.filter(a=>a!==n.validator);o.length!==r.length&&(e=!0,t.setValidators(o))}}if(n.asyncValidator!==null){let r=HB(t);if(Array.isArray(r)&&r.length>0){let o=r.filter(a=>a!==n.asyncValidator);o.length!==r.length&&(e=!0,t.setAsyncValidators(o))}}}let i=()=>{};return gx(n._rawValidators,i),gx(n._rawAsyncValidators,i),e}function iee(t,n){n.valueAccessor.registerOnChange(e=>{t._pendingValue=e,t._pendingChange=!0,t._pendingDirty=!0,t.updateOn==="change"&&WB(t,n)})}function nee(t,n){n.valueAccessor.registerOnTouched(()=>{t._pendingTouched=!0,t.updateOn==="blur"&&t._pendingChange&&WB(t,n),t.updateOn!=="submit"&&t.markAsTouched()})}function WB(t,n){t._pendingDirty&&t.markAsDirty(),t.setValue(t._pendingValue,{emitModelToViewChange:!1}),n.viewToModelUpdate(t._pendingValue),t._pendingChange=!1}function ree(t,n){let e=(i,r)=>{n.valueAccessor.writeValue(i),r&&n.viewToModelUpdate(i)};t.registerOnChange(e),n._registerOnDestroy(()=>{t._unregisterOnChange(e)})}function GB(t,n){t==null,lk(t,n)}function oee(t,n){return _x(t,n)}function ck(t,n){if(!t.hasOwnProperty("model"))return!1;let e=t.model;return e.isFirstChange()?!0:!Object.is(n,e.currentValue)}function aee(t){return Object.getPrototypeOf(t.constructor)===AB}function qB(t,n){t._syncPendingControls(),n.forEach(e=>{let i=e.control;i.updateOn==="submit"&&i._pendingChange&&(e.viewToModelUpdate(i._pendingValue),i._pendingChange=!1)})}function dk(t,n){if(!n)return null;Array.isArray(n);let e,i,r;return n.forEach(o=>{o.constructor===di?e=o:aee(o)?i=o:r=o}),r||i||e||null}function see(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}var lee={provide:$s,useExisting:li(()=>Oc)},gg=Promise.resolve(),Oc=(()=>{class t extends $s{callSetDisabledState;get submitted(){return Ni(this.submittedReactive)}_submitted=ci(()=>this.submittedReactive());submittedReactive=he(!1);_directives=new Set;form;ngSubmit=new U;options;constructor(e,i,r){super(),this.callSetDisabledState=r,this.form=new fu({},rk(e),ok(i))}ngAfterViewInit(){this._setUpdateStrategy()}get formDirective(){return this}get control(){return this.form}get path(){return[]}get controls(){return this.form.controls}addControl(e){gg.then(()=>{let i=this._findContainer(e.path);e.control=i.registerControl(e.name,e.control),yg(e.control,e,this.callSetDisabledState),e.control.updateValueAndValidity({emitEvent:!1}),this._directives.add(e)})}getControl(e){return this.form.get(e.path)}removeControl(e){gg.then(()=>{let i=this._findContainer(e.path);i&&i.removeControl(e.name),this._directives.delete(e)})}addFormGroup(e){gg.then(()=>{let i=this._findContainer(e.path),r=new fu({});GB(r,e),i.registerControl(e.name,r),r.updateValueAndValidity({emitEvent:!1})})}removeFormGroup(e){gg.then(()=>{let i=this._findContainer(e.path);i&&i.removeControl(e.name)})}getFormGroup(e){return this.form.get(e.path)}updateModel(e,i){gg.then(()=>{this.form.get(e.path).setValue(i)})}setValue(e){this.control.setValue(e)}onSubmit(e){return this.submittedReactive.set(!0),qB(this.form,this._directives),this.ngSubmit.emit(e),this.form._events.next(new px(this.control)),e?.target?.method==="dialog"}onReset(){this.resetForm()}resetForm(e=void 0){this.form.reset(e),this.submittedReactive.set(!1)}_setUpdateStrategy(){this.options&&this.options.updateOn!=null&&(this.form._updateOn=this.options.updateOn)}_findContainer(e){return e.pop(),e.length?this.form.get(e):this.form}static \u0275fac=function(i){return new(i||t)(be(sa,10),be(xg,10),be(Eh,8))};static \u0275dir=P({type:t,selectors:[["form",3,"ngNoForm","",3,"formGroup",""],["ng-form"],["","ngForm",""]],hostBindings:function(i,r){i&1&&S("submit",function(a){return r.onSubmit(a)})("reset",function(){return r.onReset()})},inputs:{options:[0,"ngFormOptions","options"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],standalone:!1,features:[we([lee]),le]})}return t})();function EB(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}function SB(t){return typeof t=="object"&&t!==null&&Object.keys(t).length===2&&"value"in t&&"disabled"in t}var aa=class extends Ac{defaultValue=null;_onChange=[];_pendingValue;_pendingChange=!1;constructor(n=null,e,i){super(ak(e),sk(i,e)),this._applyFormState(n),this._setUpdateStrategy(e),this._initObservables(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator}),vx(e)&&(e.nonNullable||e.initialValueIsDefault)&&(SB(n)?this.defaultValue=n.value:this.defaultValue=n)}setValue(n,e={}){this.value=this._pendingValue=n,this._onChange.length&&e.emitModelToViewChange!==!1&&this._onChange.forEach(i=>i(this.value,e.emitViewToModelChange!==!1)),this.updateValueAndValidity(e)}patchValue(n,e={}){this.setValue(n,e)}reset(n=this.defaultValue,e={}){this._applyFormState(n),this.markAsPristine(e),this.markAsUntouched(e),this.setValue(this.value,e),this._pendingChange=!1,e?.emitEvent!==!1&&this._events.next(new vg(this))}_updateValue(){}_anyControls(n){return!1}_allControlsDisabled(){return this.disabled}registerOnChange(n){this._onChange.push(n)}_unregisterOnChange(n){EB(this._onChange,n)}registerOnDisabledChange(n){this._onDisabledChange.push(n)}_unregisterOnDisabledChange(n){EB(this._onDisabledChange,n)}_forEachChild(n){}_syncPendingControls(){return this.updateOn==="submit"&&(this._pendingDirty&&this.markAsDirty(),this._pendingTouched&&this.markAsTouched(),this._pendingChange)?(this.setValue(this._pendingValue,{onlySelf:!0,emitModelToViewChange:!1}),!0):!1}_applyFormState(n){SB(n)?(this.value=this._pendingValue=n.value,n.disabled?this.disable({onlySelf:!0,emitEvent:!1}):this.enable({onlySelf:!0,emitEvent:!1})):this.value=this._pendingValue=n}},YB=aa,cee=t=>t instanceof aa;var dee={provide:Kn,useExisting:li(()=>Ro)},kB=Promise.resolve(),Ro=(()=>{class t extends Kn{_changeDetectorRef;callSetDisabledState;control=new aa;static ngAcceptInputType_isDisabled;_registered=!1;viewModel;name="";isDisabled;model;options;update=new U;constructor(e,i,r,o,a,s){super(),this._changeDetectorRef=a,this.callSetDisabledState=s,this._parent=e,this._setValidators(i),this._setAsyncValidators(r),this.valueAccessor=dk(this,o)}ngOnChanges(e){if(this._checkForErrors(),!this._registered||"name"in e){if(this._registered&&(this._checkName(),this.formDirective)){let i=e.name.previousValue;this.formDirective.removeControl({name:i,path:this._getPath(i)})}this._setUpControl()}"isDisabled"in e&&this._updateDisabled(e),ck(e,this.viewModel)&&(this._updateValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.formDirective&&this.formDirective.removeControl(this)}get path(){return this._getPath(this.name)}get formDirective(){return this._parent?this._parent.formDirective:null}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}_setUpControl(){this._setUpdateStrategy(),this._isStandalone()?this._setUpStandalone():this.formDirective.addControl(this),this._registered=!0}_setUpdateStrategy(){this.options&&this.options.updateOn!=null&&(this.control._updateOn=this.options.updateOn)}_isStandalone(){return!this._parent||!!(this.options&&this.options.standalone)}_setUpStandalone(){yg(this.control,this,this.callSetDisabledState),this.control.updateValueAndValidity({emitEvent:!1})}_checkForErrors(){this._checkName()}_checkName(){this.options&&this.options.name&&(this.name=this.options.name),!this._isStandalone()&&this.name}_updateValue(e){kB.then(()=>{this.control.setValue(e,{emitViewToModelChange:!1}),this._changeDetectorRef?.markForCheck()})}_updateDisabled(e){let i=e.isDisabled.currentValue,r=i!==0&&B(i);kB.then(()=>{r&&!this.control.disabled?this.control.disable():!r&&this.control.disabled&&this.control.enable(),this._changeDetectorRef?.markForCheck()})}_getPath(e){return this._parent?$B(e,this._parent):[e]}static \u0275fac=function(i){return new(i||t)(be($s,9),be(sa,10),be(xg,10),be(dr,10),be(ye,8),be(Eh,8))};static \u0275dir=P({type:t,selectors:[["","ngModel","",3,"formControlName","",3,"formControl",""]],inputs:{name:"name",isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"],options:[0,"ngModelOptions","options"]},outputs:{update:"ngModelChange"},exportAs:["ngModel"],standalone:!1,features:[we([dee]),le,Oe]})}return t})();var lo=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["form",3,"ngNoForm","",3,"ngNativeValidate",""]],hostAttrs:["novalidate",""],standalone:!1})}return t})(),uee={provide:dr,useExisting:li(()=>gu),multi:!0},gu=(()=>{class t extends AB{writeValue(e){let i=e??"";this.setProperty("value",i)}registerOnChange(e){this.onChange=i=>{e(i==""?null:parseFloat(i))}}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["input","type","number","formControlName",""],["input","type","number","formControl",""],["input","type","number","ngModel",""]],hostBindings:function(i,r){i&1&&S("input",function(a){return r.onChange(a.target.value)})("blur",function(){return r.onTouched()})},standalone:!1,features:[we([uee]),le]})}return t})();var uk=new O(""),mee={provide:Kn,useExisting:li(()=>Po)},Po=(()=>{class t extends Kn{_ngModelWarningConfig;callSetDisabledState;viewModel;form;set isDisabled(e){}model;update=new U;static _ngModelWarningSentOnce=!1;_ngModelWarningSent=!1;constructor(e,i,r,o,a){super(),this._ngModelWarningConfig=o,this.callSetDisabledState=a,this._setValidators(e),this._setAsyncValidators(i),this.valueAccessor=dk(this,r)}ngOnChanges(e){if(this._isControlChanged(e)){let i=e.form.previousValue;i&&fx(i,this,!1),yg(this.form,this,this.callSetDisabledState),this.form.updateValueAndValidity({emitEvent:!1})}ck(e,this.viewModel)&&(this.form.setValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.form&&fx(this.form,this,!1)}get path(){return[]}get control(){return this.form}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}_isControlChanged(e){return e.hasOwnProperty("form")}static \u0275fac=function(i){return new(i||t)(be(sa,10),be(xg,10),be(dr,10),be(uk,8),be(Eh,8))};static \u0275dir=P({type:t,selectors:[["","formControl",""]],inputs:{form:[0,"formControl","form"],isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"]},outputs:{update:"ngModelChange"},exportAs:["ngForm"],standalone:!1,features:[we([mee]),le,Oe]})}return t})(),hee={provide:$s,useExisting:li(()=>nn)},nn=(()=>{class t extends $s{callSetDisabledState;get submitted(){return Ni(this._submittedReactive)}set submitted(e){this._submittedReactive.set(e)}_submitted=ci(()=>this._submittedReactive());_submittedReactive=he(!1);_oldForm;_onCollectionChange=()=>this._updateDomValue();directives=[];form=null;ngSubmit=new U;constructor(e,i,r){super(),this.callSetDisabledState=r,this._setValidators(e),this._setAsyncValidators(i)}ngOnChanges(e){e.hasOwnProperty("form")&&(this._updateValidators(),this._updateDomValue(),this._updateRegistrations(),this._oldForm=this.form)}ngOnDestroy(){this.form&&(_x(this.form,this),this.form._onCollectionChange===this._onCollectionChange&&this.form._registerOnCollectionChange(()=>{}))}get formDirective(){return this}get control(){return this.form}get path(){return[]}addControl(e){let i=this.form.get(e.path);return yg(i,e,this.callSetDisabledState),i.updateValueAndValidity({emitEvent:!1}),this.directives.push(e),i}getControl(e){return this.form.get(e.path)}removeControl(e){fx(e.control||null,e,!1),see(this.directives,e)}addFormGroup(e){this._setUpFormContainer(e)}removeFormGroup(e){this._cleanUpFormContainer(e)}getFormGroup(e){return this.form.get(e.path)}addFormArray(e){this._setUpFormContainer(e)}removeFormArray(e){this._cleanUpFormContainer(e)}getFormArray(e){return this.form.get(e.path)}updateModel(e,i){this.form.get(e.path).setValue(i)}onSubmit(e){return this._submittedReactive.set(!0),qB(this.form,this.directives),this.ngSubmit.emit(e),this.form._events.next(new px(this.control)),e?.target?.method==="dialog"}onReset(){this.resetForm()}resetForm(e=void 0,i={}){this.form.reset(e,i),this._submittedReactive.set(!1)}_updateDomValue(){this.directives.forEach(e=>{let i=e.control,r=this.form.get(e.path);i!==r&&(fx(i||null,e),cee(r)&&(yg(r,e,this.callSetDisabledState),e.control=r))}),this.form._updateTreeValidity({emitEvent:!1})}_setUpFormContainer(e){let i=this.form.get(e.path);GB(i,e),i.updateValueAndValidity({emitEvent:!1})}_cleanUpFormContainer(e){if(this.form){let i=this.form.get(e.path);i&&oee(i,e)&&i.updateValueAndValidity({emitEvent:!1})}}_updateRegistrations(){this.form._registerOnCollectionChange(this._onCollectionChange),this._oldForm&&this._oldForm._registerOnCollectionChange(()=>{})}_updateValidators(){lk(this.form,this),this._oldForm&&_x(this._oldForm,this)}static \u0275fac=function(i){return new(i||t)(be(sa,10),be(xg,10),be(Eh,8))};static \u0275dir=P({type:t,selectors:[["","formGroup",""]],hostBindings:function(i,r){i&1&&S("submit",function(a){return r.onSubmit(a)})("reset",function(){return r.onReset()})},inputs:{form:[0,"formGroup","form"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],standalone:!1,features:[we([hee]),le,Oe]})}return t})();var pee={provide:Kn,useExisting:li(()=>Yr)},Yr=(()=>{class t extends Kn{_ngModelWarningConfig;_added=!1;viewModel;control;name=null;set isDisabled(e){}model;update=new U;static _ngModelWarningSentOnce=!1;_ngModelWarningSent=!1;constructor(e,i,r,o,a){super(),this._ngModelWarningConfig=a,this._parent=e,this._setValidators(i),this._setAsyncValidators(r),this.valueAccessor=dk(this,o)}ngOnChanges(e){this._added||this._setUpControl(),ck(e,this.viewModel)&&(this.viewModel=this.model,this.formDirective.updateModel(this,this.model))}ngOnDestroy(){this.formDirective&&this.formDirective.removeControl(this)}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}get path(){return $B(this.name==null?this.name:this.name.toString(),this._parent)}get formDirective(){return this._parent?this._parent.formDirective:null}_setUpControl(){this.control=this.formDirective.addControl(this),this._added=!0}static \u0275fac=function(i){return new(i||t)(be($s,13),be(sa,10),be(xg,10),be(dr,10),be(uk,8))};static \u0275dir=P({type:t,selectors:[["","formControlName",""]],inputs:{name:[0,"formControlName","name"],isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"]},outputs:{update:"ngModelChange"},standalone:!1,features:[we([pee]),le,Oe]})}return t})();var fee=(()=>{class t{_validator=cx;_onChange;_enabled;ngOnChanges(e){if(this.inputName in e){let i=this.normalizeInput(e[this.inputName].currentValue);this._enabled=this.enabled(i),this._validator=this._enabled?this.createValidator(i):cx,this._onChange&&this._onChange()}}validate(e){return this._validator(e)}registerOnValidatorChange(e){this._onChange=e}enabled(e){return e!=null}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,features:[Oe]})}return t})();var gee={provide:sa,useExisting:li(()=>Fo),multi:!0};var Fo=(()=>{class t extends fee{required;inputName="required";normalizeInput=B;createValidator=e=>OB;enabled(e){return e}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","required","","formControlName","",3,"type","checkbox"],["","required","","formControl","",3,"type","checkbox"],["","required","","ngModel","",3,"type","checkbox"]],hostVars:1,hostBindings:function(i,r){i&2&&X("required",r._enabled?"":null)},inputs:{required:"required"},standalone:!1,features:[we([gee]),le]})}return t})();var QB=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})(),bx=class extends Ac{constructor(n,e,i){super(ak(e),sk(i,e)),this.controls=n,this._initObservables(),this._setUpdateStrategy(e),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}controls;at(n){return this.controls[this._adjustIndex(n)]}push(n,e={}){Array.isArray(n)?n.forEach(i=>{this.controls.push(i),this._registerControl(i)}):(this.controls.push(n),this._registerControl(n)),this.updateValueAndValidity({emitEvent:e.emitEvent}),this._onCollectionChange()}insert(n,e,i={}){this.controls.splice(n,0,e),this._registerControl(e),this.updateValueAndValidity({emitEvent:i.emitEvent})}removeAt(n,e={}){let i=this._adjustIndex(n);i<0&&(i=0),this.controls[i]&&this.controls[i]._registerOnCollectionChange(()=>{}),this.controls.splice(i,1),this.updateValueAndValidity({emitEvent:e.emitEvent})}setControl(n,e,i={}){let r=this._adjustIndex(n);r<0&&(r=0),this.controls[r]&&this.controls[r]._registerOnCollectionChange(()=>{}),this.controls.splice(r,1),e&&(this.controls.splice(r,0,e),this._registerControl(e)),this.updateValueAndValidity({emitEvent:i.emitEvent}),this._onCollectionChange()}get length(){return this.controls.length}setValue(n,e={}){UB(this,!1,n),n.forEach((i,r)=>{zB(this,!1,r),this.at(r).setValue(i,{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e)}patchValue(n,e={}){n!=null&&(n.forEach((i,r)=>{this.at(r)&&this.at(r).patchValue(i,{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e))}reset(n=[],e={}){this._forEachChild((i,r)=>{i.reset(n[r],{onlySelf:!0,emitEvent:e.emitEvent})}),this._updatePristine(e,this),this._updateTouched(e,this),this.updateValueAndValidity(e),e?.emitEvent!==!1&&this._events.next(new vg(this))}getRawValue(){return this.controls.map(n=>n.getRawValue())}clear(n={}){this.controls.length<1||(this._forEachChild(e=>e._registerOnCollectionChange(()=>{})),this.controls.splice(0),this.updateValueAndValidity({emitEvent:n.emitEvent}))}_adjustIndex(n){return n<0?n+this.length:n}_syncPendingControls(){let n=this.controls.reduce((e,i)=>i._syncPendingControls()?!0:e,!1);return n&&this.updateValueAndValidity({onlySelf:!0}),n}_forEachChild(n){this.controls.forEach((e,i)=>{n(e,i)})}_updateValue(){this.value=this.controls.filter(n=>n.enabled||this.disabled).map(n=>n.value)}_anyControls(n){return this.controls.some(e=>e.enabled&&n(e))}_setUpControls(){this._forEachChild(n=>this._registerControl(n))}_allControlsDisabled(){for(let n of this.controls)if(n.enabled)return!1;return this.controls.length>0||this.disabled}_registerControl(n){n.setParent(this),n._registerOnCollectionChange(this._onCollectionChange)}_find(n){return this.at(n)??null}},mk=bx;function TB(t){return!!t&&(t.asyncValidators!==void 0||t.validators!==void 0||t.updateOn!==void 0)}var co=(()=>{class t{useNonNullable=!1;get nonNullable(){let e=new t;return e.useNonNullable=!0,e}group(e,i=null){let r=this._reduceControls(e),o={};return TB(i)?o=i:i!==null&&(o.validators=i.validator,o.asyncValidators=i.asyncValidator),new fu(r,o)}record(e,i=null){let r=this._reduceControls(e);return new tk(r,i)}control(e,i,r){let o={};return this.useNonNullable?(TB(i)?o=i:(o.validators=i,o.asyncValidators=r),new aa(e,Me(I({},o),{nonNullable:!0}))):new aa(e,i,r)}array(e,i,r){let o=e.map(a=>this._createControl(a));return new bx(o,i,r)}_reduceControls(e){let i={};return Object.keys(e).forEach(r=>{i[r]=this._createControl(e[r])}),i}_createControl(e){if(e instanceof aa)return e;if(e instanceof Ac)return e;if(Array.isArray(e)){let i=e[0],r=e.length>1?e[1]:null,o=e.length>2?e[2]:null;return this.control(i,r,o)}else return this.control(e)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var Qr=(()=>{class t{static withConfig(e){return{ngModule:t,providers:[{provide:Eh,useValue:e.callSetDisabledState??xx}]}}static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[QB]})}return t})(),Zn=(()=>{class t{static withConfig(e){return{ngModule:t,providers:[{provide:uk,useValue:e.warnOnNgModelWithFormControl??"always"},{provide:Eh,useValue:e.callSetDisabledState??xx}]}}static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[QB]})}return t})();var ai=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,Jm,Xt,De]})}return t})();var _ee=["trigger"],bee=["panel"],vee=[[["mat-select-trigger"]],"*"],yee=["mat-select-trigger","*"];function xee(t,n){if(t&1&&(m(0,"span",4),f(1),h()),t&2){let e=x();g(),N(e.placeholder)}}function Cee(t,n){t&1&&ne(0)}function wee(t,n){if(t&1&&(m(0,"span",11),f(1),h()),t&2){let e=x(2);g(),N(e.triggerValue)}}function Dee(t,n){if(t&1&&(m(0,"span",5),L(1,Cee,1,0)(2,wee,2,1,"span",11),h()),t&2){let e=x();g(),V(e.customTrigger?1:2)}}function Mee(t,n){if(t&1){let e=q();m(0,"div",12,1),S("keydown",function(r){k(e);let o=x();return T(o._handleKeydown(r))}),ne(2,1),h()}if(t&2){let e=x();at(Zo("mat-mdc-select-panel mdc-menu-surface mdc-menu-surface--open ",e._getPanelTheme())),G("mat-select-panel-animations-enabled",!e._animationsDisabled),v("ngClass",e.panelClass),X("id",e.id+"-panel")("aria-multiselectable",e.multiple)("aria-label",e.ariaLabel||null)("aria-labelledby",e._getPanelAriaLabelledby())}}var hk=new O("mat-select-scroll-strategy",{providedIn:"root",factory:()=>{let t=u(de);return()=>Tn(t)}});function KB(t){let n=u(de);return()=>Tn(n)}var ZB=new O("MAT_SELECT_CONFIG"),XB={provide:hk,deps:[],useFactory:KB},JB=new O("MatSelectTrigger"),Cx=class{source;value;constructor(n,e){this.source=n,this.value=e}},es=(()=>{class t{_viewportRuler=u(sr);_changeDetectorRef=u(ye);_elementRef=u(Y);_dir=u(Yt,{optional:!0});_idGenerator=u(et);_renderer=u(ze);_parentFormField=u(oa,{optional:!0});ngControl=u(Kn,{self:!0,optional:!0});_liveAnnouncer=u(Xf);_defaultOptions=u(ZB,{optional:!0});_animationsDisabled=Qe();_initialized=new z;_cleanupDetach;options;optionGroups;customTrigger;_positions=[{originX:"start",originY:"bottom",overlayX:"start",overlayY:"top"},{originX:"end",originY:"bottom",overlayX:"end",overlayY:"top"},{originX:"start",originY:"top",overlayX:"start",overlayY:"bottom",panelClass:"mat-mdc-select-panel-above"},{originX:"end",originY:"top",overlayX:"end",overlayY:"bottom",panelClass:"mat-mdc-select-panel-above"}];_scrollOptionIntoView(e){let i=this.options.toArray()[e];if(i){let r=this.panel.nativeElement,o=tg(e,this.options,this.optionGroups),a=i._getHostElement();e===0&&o===1?r.scrollTop=0:r.scrollTop=ig(a.offsetTop,a.offsetHeight,r.scrollTop,r.offsetHeight)}}_positioningSettled(){this._scrollOptionIntoView(this._keyManager.activeItemIndex||0)}_getChangeEvent(e){return new Cx(this,e)}_scrollStrategyFactory=u(hk);_panelOpen=!1;_compareWith=(e,i)=>e===i;_uid=this._idGenerator.getId("mat-select-");_triggerAriaLabelledBy=null;_previousControl;_destroy=new z;_errorStateTracker;stateChanges=new z;disableAutomaticLabeling=!0;userAriaDescribedBy;_selectionModel;_keyManager;_preferredOverlayOrigin;_overlayWidth;_onChange=()=>{};_onTouched=()=>{};_valueId=this._idGenerator.getId("mat-select-value-");_scrollStrategy;_overlayPanelClass=this._defaultOptions?.overlayPanelClass||"";get focused(){return this._focused||this._panelOpen}_focused=!1;controlType="mat-select";trigger;panel;_overlayDir;panelClass;disabled=!1;get disableRipple(){return this._disableRipple()}set disableRipple(e){this._disableRipple.set(e)}_disableRipple=he(!1);tabIndex=0;get hideSingleSelectionIndicator(){return this._hideSingleSelectionIndicator}set hideSingleSelectionIndicator(e){this._hideSingleSelectionIndicator=e,this._syncParentProperties()}_hideSingleSelectionIndicator=this._defaultOptions?.hideSingleSelectionIndicator??!1;get placeholder(){return this._placeholder}set placeholder(e){this._placeholder=e,this.stateChanges.next()}_placeholder;get required(){return this._required??this.ngControl?.control?.hasValidator(Ve.required)??!1}set required(e){this._required=e,this.stateChanges.next()}_required;get multiple(){return this._multiple}set multiple(e){this._selectionModel,this._multiple=e}_multiple=!1;disableOptionCentering=this._defaultOptions?.disableOptionCentering??!1;get compareWith(){return this._compareWith}set compareWith(e){this._compareWith=e,this._selectionModel&&this._initializeSelection()}get value(){return this._value}set value(e){this._assignValue(e)&&this._onChange(e)}_value;ariaLabel="";ariaLabelledby;get errorStateMatcher(){return this._errorStateTracker.matcher}set errorStateMatcher(e){this._errorStateTracker.matcher=e}typeaheadDebounceInterval;sortComparator;get id(){return this._id}set id(e){this._id=e||this._uid,this.stateChanges.next()}_id;get errorState(){return this._errorStateTracker.errorState}set errorState(e){this._errorStateTracker.errorState=e}panelWidth=this._defaultOptions&&typeof this._defaultOptions.panelWidth<"u"?this._defaultOptions.panelWidth:"auto";canSelectNullableOptions=this._defaultOptions?.canSelectNullableOptions??!1;optionSelectionChanges=Fn(()=>{let e=this.options;return e?e.changes.pipe(Ue(e),je(()=>it(...e.map(i=>i.onSelectionChange)))):this._initialized.pipe(je(()=>this.optionSelectionChanges))});openedChange=new U;_openedStream=this.openedChange.pipe(ce(e=>e),se(()=>{}));_closedStream=this.openedChange.pipe(ce(e=>!e),se(()=>{}));selectionChange=new U;valueChange=new U;constructor(){let e=u(kl),i=u(Oc,{optional:!0}),r=u(nn,{optional:!0}),o=u(new Li("tabindex"),{optional:!0});this.ngControl&&(this.ngControl.valueAccessor=this),this._defaultOptions?.typeaheadDebounceInterval!=null&&(this.typeaheadDebounceInterval=this._defaultOptions.typeaheadDebounceInterval),this._errorStateTracker=new Sl(e,this.ngControl,r,i,this.stateChanges),this._scrollStrategy=this._scrollStrategyFactory(),this.tabIndex=o==null?0:parseInt(o)||0,this.id=this.id}ngOnInit(){this._selectionModel=new hg(this.multiple),this.stateChanges.next(),this._viewportRuler.change().pipe(xe(this._destroy)).subscribe(()=>{this.panelOpen&&(this._overlayWidth=this._getOverlayWidth(this._preferredOverlayOrigin),this._changeDetectorRef.detectChanges())})}ngAfterContentInit(){this._initialized.next(),this._initialized.complete(),this._initKeyManager(),this._selectionModel.changed.pipe(xe(this._destroy)).subscribe(e=>{e.added.forEach(i=>i.select()),e.removed.forEach(i=>i.deselect())}),this.options.changes.pipe(Ue(null),xe(this._destroy)).subscribe(()=>{this._resetOptions(),this._initializeSelection()})}ngDoCheck(){let e=this._getTriggerAriaLabelledby(),i=this.ngControl;if(e!==this._triggerAriaLabelledBy){let r=this._elementRef.nativeElement;this._triggerAriaLabelledBy=e,e?r.setAttribute("aria-labelledby",e):r.removeAttribute("aria-labelledby")}i&&(this._previousControl!==i.control&&(this._previousControl!==void 0&&i.disabled!==null&&i.disabled!==this.disabled&&(this.disabled=i.disabled),this._previousControl=i.control),this.updateErrorState())}ngOnChanges(e){(e.disabled||e.userAriaDescribedBy)&&this.stateChanges.next(),e.typeaheadDebounceInterval&&this._keyManager&&this._keyManager.withTypeAhead(this.typeaheadDebounceInterval)}ngOnDestroy(){this._cleanupDetach?.(),this._keyManager?.destroy(),this._destroy.next(),this._destroy.complete(),this.stateChanges.complete(),this._clearFromModal()}toggle(){this.panelOpen?this.close():this.open()}open(){this._canOpen()&&(this._parentFormField&&(this._preferredOverlayOrigin=this._parentFormField.getConnectedOverlayOrigin()),this._cleanupDetach?.(),this._overlayWidth=this._getOverlayWidth(this._preferredOverlayOrigin),this._applyModalPanelOwnership(),this._panelOpen=!0,this._overlayDir.positionChange.pipe(mt(1)).subscribe(()=>{this._changeDetectorRef.detectChanges(),this._positioningSettled()}),this._overlayDir.attachOverlay(),this._keyManager.withHorizontalOrientation(null),this._highlightCorrectOption(),this._changeDetectorRef.markForCheck(),this.stateChanges.next(),Promise.resolve().then(()=>this.openedChange.emit(!0)))}_trackedModal=null;_applyModalPanelOwnership(){let e=this._elementRef.nativeElement.closest('body > .cdk-overlay-container [aria-modal="true"]');if(!e)return;let i=`${this.id}-panel`;this._trackedModal&&Mc(this._trackedModal,"aria-owns",i),ih(e,"aria-owns",i),this._trackedModal=e}_clearFromModal(){if(!this._trackedModal)return;let e=`${this.id}-panel`;Mc(this._trackedModal,"aria-owns",e),this._trackedModal=null}close(){this._panelOpen&&(this._panelOpen=!1,this._exitAndDetach(),this._keyManager.withHorizontalOrientation(this._isRtl()?"rtl":"ltr"),this._changeDetectorRef.markForCheck(),this._onTouched(),this.stateChanges.next(),Promise.resolve().then(()=>this.openedChange.emit(!1)))}_exitAndDetach(){if(this._animationsDisabled||!this.panel){this._detachOverlay();return}this._cleanupDetach?.(),this._cleanupDetach=()=>{i(),clearTimeout(r),this._cleanupDetach=void 0};let e=this.panel.nativeElement,i=this._renderer.listen(e,"animationend",o=>{o.animationName==="_mat-select-exit"&&(this._cleanupDetach?.(),this._detachOverlay())}),r=setTimeout(()=>{this._cleanupDetach?.(),this._detachOverlay()},200);e.classList.add("mat-select-panel-exit")}_detachOverlay(){this._overlayDir.detachOverlay(),this._changeDetectorRef.markForCheck()}writeValue(e){this._assignValue(e)}registerOnChange(e){this._onChange=e}registerOnTouched(e){this._onTouched=e}setDisabledState(e){this.disabled=e,this._changeDetectorRef.markForCheck(),this.stateChanges.next()}get panelOpen(){return this._panelOpen}get selected(){return this.multiple?this._selectionModel?.selected||[]:this._selectionModel?.selected[0]}get triggerValue(){if(this.empty)return"";if(this._multiple){let e=this._selectionModel.selected.map(i=>i.viewValue);return this._isRtl()&&e.reverse(),e.join(", ")}return this._selectionModel.selected[0].viewValue}updateErrorState(){this._errorStateTracker.updateErrorState()}_isRtl(){return this._dir?this._dir.value==="rtl":!1}_handleKeydown(e){this.disabled||(this.panelOpen?this._handleOpenKeydown(e):this._handleClosedKeydown(e))}_handleClosedKeydown(e){let i=e.keyCode,r=i===40||i===38||i===37||i===39,o=i===13||i===32,a=this._keyManager;if(!a.isTyping()&&o&&!Gt(e)||(this.multiple||e.altKey)&&r)e.preventDefault(),this.open();else if(!this.multiple){let s=this.selected;a.onKeydown(e);let l=this.selected;l&&s!==l&&this._liveAnnouncer.announce(l.viewValue,1e4)}}_handleOpenKeydown(e){let i=this._keyManager,r=e.keyCode,o=r===40||r===38,a=i.isTyping();if(o&&e.altKey)e.preventDefault(),this.close();else if(!a&&(r===13||r===32)&&i.activeItem&&!Gt(e))e.preventDefault(),i.activeItem._selectViaInteraction();else if(!a&&this._multiple&&r===65&&e.ctrlKey){e.preventDefault();let s=this.options.some(l=>!l.disabled&&!l.selected);this.options.forEach(l=>{l.disabled||(s?l.select():l.deselect())})}else{let s=i.activeItemIndex;i.onKeydown(e),this._multiple&&o&&e.shiftKey&&i.activeItem&&i.activeItemIndex!==s&&i.activeItem._selectViaInteraction()}}_handleOverlayKeydown(e){e.keyCode===27&&!Gt(e)&&(e.preventDefault(),this.close())}_onFocus(){this.disabled||(this._focused=!0,this.stateChanges.next())}_onBlur(){this._focused=!1,this._keyManager?.cancelTypeahead(),!this.disabled&&!this.panelOpen&&(this._onTouched(),this._changeDetectorRef.markForCheck(),this.stateChanges.next())}_getPanelTheme(){return this._parentFormField?`mat-${this._parentFormField.color}`:""}get empty(){return!this._selectionModel||this._selectionModel.isEmpty()}_initializeSelection(){Promise.resolve().then(()=>{this.ngControl&&(this._value=this.ngControl.value),this._setSelectionByValue(this._value),this.stateChanges.next()})}_setSelectionByValue(e){if(this.options.forEach(i=>i.setInactiveStyles()),this._selectionModel.clear(),this.multiple&&e)Array.isArray(e),e.forEach(i=>this._selectOptionByValue(i)),this._sortValues();else{let i=this._selectOptionByValue(e);i?this._keyManager.updateActiveItem(i):this.panelOpen||this._keyManager.updateActiveItem(-1)}this._changeDetectorRef.markForCheck()}_selectOptionByValue(e){let i=this.options.find(r=>{if(this._selectionModel.isSelected(r))return!1;try{return(r.value!=null||this.canSelectNullableOptions)&&this._compareWith(r.value,e)}catch{return!1}});return i&&this._selectionModel.select(i),i}_assignValue(e){return e!==this._value||this._multiple&&Array.isArray(e)?(this.options&&this._setSelectionByValue(e),this._value=e,!0):!1}_skipPredicate=e=>this.panelOpen?!1:e.disabled;_getOverlayWidth(e){return this.panelWidth==="auto"?(e instanceof wh?e.elementRef:e||this._elementRef).nativeElement.getBoundingClientRect().width:this.panelWidth===null?"":this.panelWidth}_syncParentProperties(){if(this.options)for(let e of this.options)e._changeDetectorRef.markForCheck()}_initKeyManager(){this._keyManager=new tu(this.options).withTypeAhead(this.typeaheadDebounceInterval).withVerticalOrientation().withHorizontalOrientation(this._isRtl()?"rtl":"ltr").withHomeAndEnd().withPageUpDown().withAllowedModifierKeys(["shiftKey"]).skipPredicate(this._skipPredicate),this._keyManager.tabOut.subscribe(()=>{this.panelOpen&&(!this.multiple&&this._keyManager.activeItem&&this._keyManager.activeItem._selectViaInteraction(),this.focus(),this.close())}),this._keyManager.change.subscribe(()=>{this._panelOpen&&this.panel?this._scrollOptionIntoView(this._keyManager.activeItemIndex||0):!this._panelOpen&&!this.multiple&&this._keyManager.activeItem&&this._keyManager.activeItem._selectViaInteraction()})}_resetOptions(){let e=it(this.options.changes,this._destroy);this.optionSelectionChanges.pipe(xe(e)).subscribe(i=>{this._onSelect(i.source,i.isUserInput),i.isUserInput&&!this.multiple&&this._panelOpen&&(this.close(),this.focus())}),it(...this.options.map(i=>i._stateChanges)).pipe(xe(e)).subscribe(()=>{this._changeDetectorRef.detectChanges(),this.stateChanges.next()})}_onSelect(e,i){let r=this._selectionModel.isSelected(e);!this.canSelectNullableOptions&&e.value==null&&!this._multiple?(e.deselect(),this._selectionModel.clear(),this.value!=null&&this._propagateChanges(e.value)):(r!==e.selected&&(e.selected?this._selectionModel.select(e):this._selectionModel.deselect(e)),i&&this._keyManager.setActiveItem(e),this.multiple&&(this._sortValues(),i&&this.focus())),r!==this._selectionModel.isSelected(e)&&this._propagateChanges(),this.stateChanges.next()}_sortValues(){if(this.multiple){let e=this.options.toArray();this._selectionModel.sort((i,r)=>this.sortComparator?this.sortComparator(i,r,e):e.indexOf(i)-e.indexOf(r)),this.stateChanges.next()}}_propagateChanges(e){let i;this.multiple?i=this.selected.map(r=>r.value):i=this.selected?this.selected.value:e,this._value=i,this.valueChange.emit(i),this._onChange(i),this.selectionChange.emit(this._getChangeEvent(i)),this._changeDetectorRef.markForCheck()}_highlightCorrectOption(){if(this._keyManager)if(this.empty){let e=-1;for(let i=0;i0&&!!this._overlayDir}focus(e){this._elementRef.nativeElement.focus(e)}_getPanelAriaLabelledby(){if(this.ariaLabel)return null;let e=this._parentFormField?.getLabelId()||null,i=e?e+" ":"";return this.ariaLabelledby?i+this.ariaLabelledby:e}_getAriaActiveDescendant(){return this.panelOpen&&this._keyManager&&this._keyManager.activeItem?this._keyManager.activeItem.id:null}_getTriggerAriaLabelledby(){if(this.ariaLabel)return null;let e=this._parentFormField?.getLabelId()||"";return this.ariaLabelledby&&(e+=" "+this.ariaLabelledby),e||(e=this._valueId),e}get describedByIds(){return this._elementRef.nativeElement.getAttribute("aria-describedby")?.split(" ")||[]}setDescribedByIds(e){e.length?this._elementRef.nativeElement.setAttribute("aria-describedby",e.join(" ")):this._elementRef.nativeElement.removeAttribute("aria-describedby")}onContainerClick(){this.focus(),this.open()}get shouldLabelFloat(){return this.panelOpen||!this.empty||this.focused&&!!this.placeholder}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-select"]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,JB,5),Ce(o,Sn,5),Ce(o,ou,5)),i&2){let a;j(a=H())&&(r.customTrigger=a.first),j(a=H())&&(r.options=a),j(a=H())&&(r.optionGroups=a)}},viewQuery:function(i,r){if(i&1&&(ie(_ee,5),ie(bee,5),ie(sx,5)),i&2){let o;j(o=H())&&(r.trigger=o.first),j(o=H())&&(r.panel=o.first),j(o=H())&&(r._overlayDir=o.first)}},hostAttrs:["role","combobox","aria-haspopup","listbox",1,"mat-mdc-select"],hostVars:21,hostBindings:function(i,r){i&1&&S("keydown",function(a){return r._handleKeydown(a)})("focus",function(){return r._onFocus()})("blur",function(){return r._onBlur()}),i&2&&(X("id",r.id)("tabindex",r.disabled?-1:r.tabIndex)("aria-controls",r.panelOpen?r.id+"-panel":null)("aria-expanded",r.panelOpen)("aria-label",r.ariaLabel||null)("aria-required",r.required.toString())("aria-disabled",r.disabled.toString())("aria-invalid",r.errorState)("aria-activedescendant",r._getAriaActiveDescendant()),G("mat-mdc-select-disabled",r.disabled)("mat-mdc-select-invalid",r.errorState)("mat-mdc-select-required",r.required)("mat-mdc-select-empty",r.empty)("mat-mdc-select-multiple",r.multiple)("mat-select-open",r.panelOpen))},inputs:{userAriaDescribedBy:[0,"aria-describedby","userAriaDescribedBy"],panelClass:"panelClass",disabled:[2,"disabled","disabled",B],disableRipple:[2,"disableRipple","disableRipple",B],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:ht(e)],hideSingleSelectionIndicator:[2,"hideSingleSelectionIndicator","hideSingleSelectionIndicator",B],placeholder:"placeholder",required:[2,"required","required",B],multiple:[2,"multiple","multiple",B],disableOptionCentering:[2,"disableOptionCentering","disableOptionCentering",B],compareWith:"compareWith",value:"value",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],errorStateMatcher:"errorStateMatcher",typeaheadDebounceInterval:[2,"typeaheadDebounceInterval","typeaheadDebounceInterval",ht],sortComparator:"sortComparator",id:"id",panelWidth:"panelWidth",canSelectNullableOptions:[2,"canSelectNullableOptions","canSelectNullableOptions",B]},outputs:{openedChange:"openedChange",_openedStream:"opened",_closedStream:"closed",selectionChange:"selectionChange",valueChange:"valueChange"},exportAs:["matSelect"],features:[we([{provide:Za,useExisting:t},{provide:ru,useExisting:t}]),Oe],ngContentSelectors:yee,decls:11,vars:9,consts:[["fallbackOverlayOrigin","cdkOverlayOrigin","trigger",""],["panel",""],["cdk-overlay-origin","",1,"mat-mdc-select-trigger",3,"click"],[1,"mat-mdc-select-value"],[1,"mat-mdc-select-placeholder","mat-mdc-select-min-line"],[1,"mat-mdc-select-value-text"],[1,"mat-mdc-select-arrow-wrapper"],[1,"mat-mdc-select-arrow"],["viewBox","0 0 24 24","width","24px","height","24px","focusable","false","aria-hidden","true"],["d","M7 10l5 5 5-5z"],["cdk-connected-overlay","","cdkConnectedOverlayLockPosition","","cdkConnectedOverlayHasBackdrop","","cdkConnectedOverlayBackdropClass","cdk-overlay-transparent-backdrop",3,"detach","backdropClick","overlayKeydown","cdkConnectedOverlayDisableClose","cdkConnectedOverlayPanelClass","cdkConnectedOverlayScrollStrategy","cdkConnectedOverlayOrigin","cdkConnectedOverlayPositions","cdkConnectedOverlayWidth","cdkConnectedOverlayFlexibleDimensions"],[1,"mat-mdc-select-min-line"],["role","listbox","tabindex","-1",3,"keydown","ngClass"]],template:function(i,r){if(i&1){let o=q();Ee(vee),m(0,"div",2,0),S("click",function(){return k(o),T(r.open())}),m(3,"div",3),L(4,xee,2,1,"span",4)(5,Dee,3,1,"span",5),h(),m(6,"div",6)(7,"div",7),ii(),m(8,"svg",8),M(9,"path",9),h()()()(),A(10,Mee,3,10,"ng-template",10),S("detach",function(){return k(o),T(r.close())})("backdropClick",function(){return k(o),T(r.close())})("overlayKeydown",function(s){return k(o),T(r._handleOverlayKeydown(s))})}if(i&2){let o=Te(1);g(3),X("id",r._valueId),g(),V(r.empty?4:5),g(6),v("cdkConnectedOverlayDisableClose",!0)("cdkConnectedOverlayPanelClass",r._overlayPanelClass)("cdkConnectedOverlayScrollStrategy",r._scrollStrategy)("cdkConnectedOverlayOrigin",r._preferredOverlayOrigin||o)("cdkConnectedOverlayPositions",r._positions)("cdkConnectedOverlayWidth",r._overlayWidth)("cdkConnectedOverlayFlexibleDimensions",!0)}},dependencies:[wh,sx,zd],styles:[`@keyframes _mat-select-enter{from{opacity:0;transform:scaleY(0.8)}to{opacity:1;transform:none}}@keyframes _mat-select-exit{from{opacity:1}to{opacity:0}}.mat-mdc-select{display:inline-block;width:100%;outline:none;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:var(--mat-select-enabled-trigger-text-color, var(--mat-sys-on-surface));font-family:var(--mat-select-trigger-text-font, var(--mat-sys-body-large-font));line-height:var(--mat-select-trigger-text-line-height, var(--mat-sys-body-large-line-height));font-size:var(--mat-select-trigger-text-size, var(--mat-sys-body-large-size));font-weight:var(--mat-select-trigger-text-weight, var(--mat-sys-body-large-weight));letter-spacing:var(--mat-select-trigger-text-tracking, var(--mat-sys-body-large-tracking))}div.mat-mdc-select-panel{box-shadow:var(--mat-select-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12))}.mat-mdc-select-disabled{color:var(--mat-select-disabled-trigger-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-mdc-select-disabled .mat-mdc-select-placeholder{color:var(--mat-select-disabled-trigger-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-mdc-select-trigger{display:inline-flex;align-items:center;cursor:pointer;position:relative;box-sizing:border-box;width:100%}.mat-mdc-select-disabled .mat-mdc-select-trigger{-webkit-user-select:none;user-select:none;cursor:default}.mat-mdc-select-value{width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mat-mdc-select-value-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mat-mdc-select-arrow-wrapper{height:24px;flex-shrink:0;display:inline-flex;align-items:center}.mat-form-field-appearance-fill .mdc-text-field--no-label .mat-mdc-select-arrow-wrapper{transform:none}.mat-mdc-form-field .mat-mdc-select.mat-mdc-select-invalid .mat-mdc-select-arrow,.mat-form-field-invalid:not(.mat-form-field-disabled) .mat-mdc-form-field-infix::after{color:var(--mat-select-invalid-arrow-color, var(--mat-sys-error))}.mat-mdc-select-arrow{width:10px;height:5px;position:relative;color:var(--mat-select-enabled-arrow-color, var(--mat-sys-on-surface-variant))}.mat-mdc-form-field.mat-focused .mat-mdc-select-arrow{color:var(--mat-select-focused-arrow-color, var(--mat-sys-primary))}.mat-mdc-form-field .mat-mdc-select.mat-mdc-select-disabled .mat-mdc-select-arrow{color:var(--mat-select-disabled-arrow-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-select-open .mat-mdc-select-arrow{transform:rotate(180deg)}.mat-form-field-animations-enabled .mat-mdc-select-arrow{transition:transform 80ms linear}.mat-mdc-select-arrow svg{fill:currentColor;position:absolute;top:50%;left:50%;transform:translate(-50%, -50%)}@media(forced-colors: active){.mat-mdc-select-arrow svg{fill:CanvasText}.mat-mdc-select-disabled .mat-mdc-select-arrow svg{fill:GrayText}}div.mat-mdc-select-panel{width:100%;max-height:275px;outline:0;overflow:auto;padding:8px 0;border-radius:4px;box-sizing:border-box;position:relative;background-color:var(--mat-select-panel-background-color, var(--mat-sys-surface-container))}@media(forced-colors: active){div.mat-mdc-select-panel{outline:solid 1px}}.cdk-overlay-pane:not(.mat-mdc-select-panel-above) div.mat-mdc-select-panel{border-top-left-radius:0;border-top-right-radius:0;transform-origin:top center}.mat-mdc-select-panel-above div.mat-mdc-select-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;transform-origin:bottom center}.mat-select-panel-animations-enabled{animation:_mat-select-enter 120ms cubic-bezier(0, 0, 0.2, 1)}.mat-select-panel-animations-enabled.mat-select-panel-exit{animation:_mat-select-exit 100ms linear}.mat-mdc-select-placeholder{transition:color 400ms 133.3333333333ms cubic-bezier(0.25, 0.8, 0.25, 1);color:var(--mat-select-placeholder-text-color, var(--mat-sys-on-surface-variant))}.mat-mdc-form-field:not(.mat-form-field-animations-enabled) .mat-mdc-select-placeholder,._mat-animation-noopable .mat-mdc-select-placeholder{transition:none}.mat-form-field-hide-placeholder .mat-mdc-select-placeholder{color:rgba(0,0,0,0);-webkit-text-fill-color:rgba(0,0,0,0);transition:none;display:block}.mat-mdc-form-field-type-mat-select:not(.mat-form-field-disabled) .mat-mdc-text-field-wrapper{cursor:pointer}.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-fill .mat-mdc-floating-label{max-width:calc(100% - 18px)}.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-fill .mdc-floating-label--float-above{max-width:calc(100%/0.75 - 24px)}.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-outline .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-outline .mdc-text-field--label-floating .mdc-notched-outline__notch{max-width:calc(100% - 24px)}.mat-mdc-select-min-line:empty::before{content:" ";white-space:pre;width:1px;display:inline-block;visibility:hidden}.mat-form-field-appearance-fill .mat-mdc-select-arrow-wrapper{transform:var(--mat-select-arrow-transform, translateY(-8px))} +`],encapsulation:2,changeDetection:0})}return t})();var Rc=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[XB],imports:[cr,ch,De,Tr,ai,ch,De]})}return t})();var Eee=["mat-icon-button",""],See=["*"],kee=new O("MAT_BUTTON_CONFIG");function e3(t){return t==null?void 0:ht(t)}var wx=(()=>{class t{_elementRef=u(Y);_ngZone=u(ae);_animationsDisabled=Qe();_config=u(kee,{optional:!0});_focusMonitor=u(oi);_cleanupClick;_renderer=u(ze);_rippleLoader=u(iy);_isAnchor;_isFab=!1;color;get disableRipple(){return this._disableRipple}set disableRipple(e){this._disableRipple=e,this._updateRippleDisabled()}_disableRipple=!1;get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._updateRippleDisabled()}_disabled=!1;ariaDisabled;disabledInteractive;tabIndex;set _tabindex(e){this.tabIndex=e}constructor(){u(ft).load(Oi);let e=this._elementRef.nativeElement;this._isAnchor=e.tagName==="A",this.disabledInteractive=this._config?.disabledInteractive??!1,this.color=this._config?.color??null,this._rippleLoader?.configureRipple(e,{className:"mat-mdc-button-ripple"})}ngAfterViewInit(){this._focusMonitor.monitor(this._elementRef,!0),this._isAnchor&&this._setupAsAnchor()}ngOnDestroy(){this._cleanupClick?.(),this._focusMonitor.stopMonitoring(this._elementRef),this._rippleLoader?.destroyRipple(this._elementRef.nativeElement)}focus(e="program",i){e?this._focusMonitor.focusVia(this._elementRef.nativeElement,e,i):this._elementRef.nativeElement.focus(i)}_getAriaDisabled(){return this.ariaDisabled!=null?this.ariaDisabled:this._isAnchor?this.disabled||null:this.disabled&&this.disabledInteractive?!0:null}_getDisabledAttribute(){return this.disabledInteractive||!this.disabled?null:!0}_updateRippleDisabled(){this._rippleLoader?.setDisabled(this._elementRef.nativeElement,this.disableRipple||this.disabled)}_getTabIndex(){return this._isAnchor?this.disabled&&!this.disabledInteractive?-1:this.tabIndex:this.tabIndex}_setupAsAnchor(){this._cleanupClick=this._ngZone.runOutsideAngular(()=>this._renderer.listen(this._elementRef.nativeElement,"click",e=>{this.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}))}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,hostAttrs:[1,"mat-mdc-button-base"],hostVars:13,hostBindings:function(i,r){i&2&&(X("disabled",r._getDisabledAttribute())("aria-disabled",r._getAriaDisabled())("tabindex",r._getTabIndex()),at(r.color?"mat-"+r.color:""),G("mat-mdc-button-disabled",r.disabled)("mat-mdc-button-disabled-interactive",r.disabledInteractive)("mat-unthemed",!r.color)("_mat-animation-noopable",r._animationsDisabled))},inputs:{color:"color",disableRipple:[2,"disableRipple","disableRipple",B],disabled:[2,"disabled","disabled",B],ariaDisabled:[2,"aria-disabled","ariaDisabled",B],disabledInteractive:[2,"disabledInteractive","disabledInteractive",B],tabIndex:[2,"tabIndex","tabIndex",e3],_tabindex:[2,"tabindex","_tabindex",e3]}})}return t})(),Ft=(()=>{class t extends wx{constructor(){super(),this._rippleLoader.configureRipple(this._elementRef.nativeElement,{centered:!0})}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["button","mat-icon-button",""],["a","mat-icon-button",""],["button","matIconButton",""],["a","matIconButton",""]],hostAttrs:[1,"mdc-icon-button","mat-mdc-icon-button"],exportAs:["matButton","matAnchor"],features:[le],attrs:Eee,ngContentSelectors:See,decls:4,vars:0,consts:[[1,"mat-mdc-button-persistent-ripple","mdc-icon-button__ripple"],[1,"mat-focus-indicator"],[1,"mat-mdc-button-touch-target"]],template:function(i,r){i&1&&(Ee(),ni(0,"span",0),ne(1),ni(2,"span",1)(3,"span",2))},styles:[`.mat-mdc-icon-button{-webkit-user-select:none;user-select:none;display:inline-block;position:relative;box-sizing:border-box;border:none;outline:none;background-color:rgba(0,0,0,0);fill:currentColor;text-decoration:none;cursor:pointer;z-index:0;overflow:visible;border-radius:var(--mat-icon-button-container-shape, var(--mat-sys-corner-full, 50%));flex-shrink:0;text-align:center;width:var(--mat-icon-button-state-layer-size, 40px);height:var(--mat-icon-button-state-layer-size, 40px);padding:calc(calc(var(--mat-icon-button-state-layer-size, 40px) - var(--mat-icon-button-icon-size, 24px)) / 2);font-size:var(--mat-icon-button-icon-size, 24px);color:var(--mat-icon-button-icon-color, var(--mat-sys-on-surface-variant));-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-icon-button .mat-mdc-button-ripple,.mat-mdc-icon-button .mat-mdc-button-persistent-ripple,.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none;border-radius:inherit}.mat-mdc-icon-button .mat-mdc-button-ripple{overflow:hidden}.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before{content:"";opacity:0}.mat-mdc-icon-button .mdc-button__label,.mat-mdc-icon-button .mat-icon{z-index:1;position:relative}.mat-mdc-icon-button .mat-focus-indicator{top:0;left:0;right:0;bottom:0;position:absolute;border-radius:inherit}.mat-mdc-icon-button:focus>.mat-focus-indicator::before{content:"";border-radius:inherit}.mat-mdc-icon-button .mat-ripple-element{background-color:var(--mat-icon-button-ripple-color, color-mix(in srgb, var(--mat-sys-on-surface-variant) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent))}.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-icon-button-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-mdc-icon-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-icon-button-disabled-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-mdc-icon-button:hover>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-icon-button-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-icon-button.cdk-program-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-icon-button.cdk-keyboard-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-icon-button.mat-mdc-button-disabled-interactive:focus>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-icon-button-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-icon-button:active>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-icon-button-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity))}.mat-mdc-icon-button .mat-mdc-button-touch-target{position:absolute;top:50%;height:var(--mat-icon-button-touch-target-size, 48px);display:var(--mat-icon-button-touch-target-display, block);left:50%;width:var(--mat-icon-button-touch-target-size, 48px);transform:translate(-50%, -50%)}.mat-mdc-icon-button._mat-animation-noopable{transition:none !important;animation:none !important}.mat-mdc-icon-button[disabled],.mat-mdc-icon-button.mat-mdc-button-disabled{cursor:default;pointer-events:none;color:var(--mat-icon-button-disabled-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-mdc-icon-button.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-mdc-icon-button img,.mat-mdc-icon-button svg{width:var(--mat-icon-button-icon-size, 24px);height:var(--mat-icon-button-icon-size, 24px);vertical-align:baseline}.mat-mdc-icon-button .mat-mdc-button-persistent-ripple{border-radius:var(--mat-icon-button-container-shape, var(--mat-sys-corner-full, 50%))}.mat-mdc-icon-button[hidden]{display:none}.mat-mdc-icon-button.mat-unthemed:not(.mdc-ripple-upgraded):focus::before,.mat-mdc-icon-button.mat-primary:not(.mdc-ripple-upgraded):focus::before,.mat-mdc-icon-button.mat-accent:not(.mdc-ripple-upgraded):focus::before,.mat-mdc-icon-button.mat-warn:not(.mdc-ripple-upgraded):focus::before{background:rgba(0,0,0,0);opacity:1} +`,`@media(forced-colors: active){.mat-mdc-button:not(.mdc-button--outlined),.mat-mdc-unelevated-button:not(.mdc-button--outlined),.mat-mdc-raised-button:not(.mdc-button--outlined),.mat-mdc-outlined-button:not(.mdc-button--outlined),.mat-mdc-button-base.mat-tonal-button,.mat-mdc-icon-button.mat-mdc-icon-button,.mat-mdc-outlined-button .mdc-button__ripple{outline:solid 1px}} +`],encapsulation:2,changeDetection:0})}return t})();var Tee=["tooltip"],pk=20;var fk=new O("mat-tooltip-scroll-strategy",{providedIn:"root",factory:()=>{let t=u(de);return()=>Tn(t,{scrollThrottle:pk})}});function n3(t){let n=u(de);return()=>Tn(n,{scrollThrottle:pk})}var gk={provide:fk,deps:[],useFactory:n3};function r3(){return{showDelay:0,hideDelay:0,touchendHideDelay:1500}}var o3=new O("mat-tooltip-default-options",{providedIn:"root",factory:r3});var t3="tooltip-panel",i3=Cc({passive:!0}),Iee=8,Aee=8,Oee=24,Ree=200,ur=(()=>{class t{_elementRef=u(Y);_ngZone=u(ae);_platform=u(Ye);_ariaDescriber=u(nh);_focusMonitor=u(oi);_dir=u(Yt);_injector=u(de);_viewContainerRef=u(st);_animationsDisabled=Qe();_defaultOptions=u(o3,{optional:!0});_overlayRef;_tooltipInstance;_overlayPanelClass;_portal;_position="below";_positionAtOrigin=!1;_disabled=!1;_tooltipClass;_viewInitialized=!1;_pointerExitEventsInitialized=!1;_tooltipComponent=a3;_viewportMargin=8;_currentPosition;_cssClassPrefix="mat-mdc";_ariaDescriptionPending;_dirSubscribed=!1;get position(){return this._position}set position(e){e!==this._position&&(this._position=e,this._overlayRef&&(this._updatePosition(this._overlayRef),this._tooltipInstance?.show(0),this._overlayRef.updatePosition()))}get positionAtOrigin(){return this._positionAtOrigin}set positionAtOrigin(e){this._positionAtOrigin=Vi(e),this._detach(),this._overlayRef=null}get disabled(){return this._disabled}set disabled(e){let i=Vi(e);this._disabled!==i&&(this._disabled=i,i?this.hide(0):this._setupPointerEnterEventsIfNeeded(),this._syncAriaDescription(this.message))}get showDelay(){return this._showDelay}set showDelay(e){this._showDelay=Gn(e)}_showDelay;get hideDelay(){return this._hideDelay}set hideDelay(e){this._hideDelay=Gn(e),this._tooltipInstance&&(this._tooltipInstance._mouseLeaveHideDelay=this._hideDelay)}_hideDelay;touchGestures="auto";get message(){return this._message}set message(e){let i=this._message;this._message=e!=null?String(e).trim():"",!this._message&&this._isTooltipVisible()?this.hide(0):(this._setupPointerEnterEventsIfNeeded(),this._updateTooltipMessage()),this._syncAriaDescription(i)}_message="";get tooltipClass(){return this._tooltipClass}set tooltipClass(e){this._tooltipClass=e,this._tooltipInstance&&this._setTooltipClass(this._tooltipClass)}_passiveListeners=[];_touchstartTimeout=null;_destroyed=new z;_isDestroyed=!1;constructor(){let e=this._defaultOptions;e&&(this._showDelay=e.showDelay,this._hideDelay=e.hideDelay,e.position&&(this.position=e.position),e.positionAtOrigin&&(this.positionAtOrigin=e.positionAtOrigin),e.touchGestures&&(this.touchGestures=e.touchGestures),e.tooltipClass&&(this.tooltipClass=e.tooltipClass)),this._viewportMargin=Iee}ngAfterViewInit(){this._viewInitialized=!0,this._setupPointerEnterEventsIfNeeded(),this._focusMonitor.monitor(this._elementRef).pipe(xe(this._destroyed)).subscribe(e=>{e?e==="keyboard"&&this._ngZone.run(()=>this.show()):this._ngZone.run(()=>this.hide(0))})}ngOnDestroy(){let e=this._elementRef.nativeElement;this._touchstartTimeout&&clearTimeout(this._touchstartTimeout),this._overlayRef&&(this._overlayRef.dispose(),this._tooltipInstance=null),this._passiveListeners.forEach(([i,r])=>{e.removeEventListener(i,r,i3)}),this._passiveListeners.length=0,this._destroyed.next(),this._destroyed.complete(),this._isDestroyed=!0,this._ariaDescriber.removeDescription(e,this.message,"tooltip"),this._focusMonitor.stopMonitoring(e)}show(e=this.showDelay,i){if(this.disabled||!this.message||this._isTooltipVisible()){this._tooltipInstance?._cancelPendingAnimations();return}let r=this._createOverlay(i);this._detach(),this._portal=this._portal||new ao(this._tooltipComponent,this._viewContainerRef);let o=this._tooltipInstance=r.attach(this._portal).instance;o._triggerElement=this._elementRef.nativeElement,o._mouseLeaveHideDelay=this._hideDelay,o.afterHidden().pipe(xe(this._destroyed)).subscribe(()=>this._detach()),this._setTooltipClass(this._tooltipClass),this._updateTooltipMessage(),o.show(e)}hide(e=this.hideDelay){let i=this._tooltipInstance;i&&(i.isVisible()?i.hide(e):(i._cancelPendingAnimations(),this._detach()))}toggle(e){this._isTooltipVisible()?this.hide():this.show(void 0,e)}_isTooltipVisible(){return!!this._tooltipInstance&&this._tooltipInstance.isVisible()}_createOverlay(e){if(this._overlayRef){let a=this._overlayRef.getConfig().positionStrategy;if((!this.positionAtOrigin||!e)&&a._origin instanceof Y)return this._overlayRef;this._detach()}let i=this._injector.get(zs).getAncestorScrollContainers(this._elementRef),r=`${this._cssClassPrefix}-${t3}`,o=Ja(this._injector,this.positionAtOrigin?e||this._elementRef:this._elementRef).withTransformOriginOn(`.${this._cssClassPrefix}-tooltip`).withFlexibleDimensions(!1).withViewportMargin(this._viewportMargin).withScrollableContainers(i);return o.positionChanges.pipe(xe(this._destroyed)).subscribe(a=>{this._updateCurrentPositionClass(a.connectionPair),this._tooltipInstance&&a.scrollableViewProperties.isOverlayClipped&&this._tooltipInstance.isVisible()&&this._ngZone.run(()=>this.hide(0))}),this._overlayRef=qr(this._injector,{direction:this._dir,positionStrategy:o,panelClass:this._overlayPanelClass?[...this._overlayPanelClass,r]:r,scrollStrategy:this._injector.get(fk)(),disableAnimations:this._animationsDisabled}),this._updatePosition(this._overlayRef),this._overlayRef.detachments().pipe(xe(this._destroyed)).subscribe(()=>this._detach()),this._overlayRef.outsidePointerEvents().pipe(xe(this._destroyed)).subscribe(()=>this._tooltipInstance?._handleBodyInteraction()),this._overlayRef.keydownEvents().pipe(xe(this._destroyed)).subscribe(a=>{this._isTooltipVisible()&&a.keyCode===27&&!Gt(a)&&(a.preventDefault(),a.stopPropagation(),this._ngZone.run(()=>this.hide(0)))}),this._defaultOptions?.disableTooltipInteractivity&&this._overlayRef.addPanelClass(`${this._cssClassPrefix}-tooltip-panel-non-interactive`),this._dirSubscribed||(this._dirSubscribed=!0,this._dir.change.pipe(xe(this._destroyed)).subscribe(()=>{this._overlayRef&&this._updatePosition(this._overlayRef)})),this._overlayRef}_detach(){this._overlayRef&&this._overlayRef.hasAttached()&&this._overlayRef.detach(),this._tooltipInstance=null}_updatePosition(e){let i=e.getConfig().positionStrategy,r=this._getOrigin(),o=this._getOverlayPosition();i.withPositions([this._addOffset(I(I({},r.main),o.main)),this._addOffset(I(I({},r.fallback),o.fallback))])}_addOffset(e){let i=Aee,r=!this._dir||this._dir.value=="ltr";return e.originY==="top"?e.offsetY=-i:e.originY==="bottom"?e.offsetY=i:e.originX==="start"?e.offsetX=r?-i:i:e.originX==="end"&&(e.offsetX=r?i:-i),e}_getOrigin(){let e=!this._dir||this._dir.value=="ltr",i=this.position,r;i=="above"||i=="below"?r={originX:"center",originY:i=="above"?"top":"bottom"}:i=="before"||i=="left"&&e||i=="right"&&!e?r={originX:"start",originY:"center"}:(i=="after"||i=="right"&&e||i=="left"&&!e)&&(r={originX:"end",originY:"center"});let{x:o,y:a}=this._invertPosition(r.originX,r.originY);return{main:r,fallback:{originX:o,originY:a}}}_getOverlayPosition(){let e=!this._dir||this._dir.value=="ltr",i=this.position,r;i=="above"?r={overlayX:"center",overlayY:"bottom"}:i=="below"?r={overlayX:"center",overlayY:"top"}:i=="before"||i=="left"&&e||i=="right"&&!e?r={overlayX:"end",overlayY:"center"}:(i=="after"||i=="right"&&e||i=="left"&&!e)&&(r={overlayX:"start",overlayY:"center"});let{x:o,y:a}=this._invertPosition(r.overlayX,r.overlayY);return{main:r,fallback:{overlayX:o,overlayY:a}}}_updateTooltipMessage(){this._tooltipInstance&&(this._tooltipInstance.message=this.message,this._tooltipInstance._markForCheck(),vt(()=>{this._tooltipInstance&&this._overlayRef.updatePosition()},{injector:this._injector}))}_setTooltipClass(e){this._tooltipInstance&&(this._tooltipInstance.tooltipClass=e,this._tooltipInstance._markForCheck())}_invertPosition(e,i){return this.position==="above"||this.position==="below"?i==="top"?i="bottom":i==="bottom"&&(i="top"):e==="end"?e="start":e==="start"&&(e="end"),{x:e,y:i}}_updateCurrentPositionClass(e){let{overlayY:i,originX:r,originY:o}=e,a;if(i==="center"?this._dir&&this._dir.value==="rtl"?a=r==="end"?"left":"right":a=r==="start"?"left":"right":a=i==="bottom"&&o==="top"?"above":"below",a!==this._currentPosition){let s=this._overlayRef;if(s){let l=`${this._cssClassPrefix}-${t3}-`;s.removePanelClass(l+this._currentPosition),s.addPanelClass(l+a)}this._currentPosition=a}}_setupPointerEnterEventsIfNeeded(){this._disabled||!this.message||!this._viewInitialized||this._passiveListeners.length||(this._platformSupportsMouseEvents()?this._passiveListeners.push(["mouseenter",e=>{this._setupPointerExitEventsIfNeeded();let i;e.x!==void 0&&e.y!==void 0&&(i=e),this.show(void 0,i)}]):this.touchGestures!=="off"&&(this._disableNativeGesturesIfNecessary(),this._passiveListeners.push(["touchstart",e=>{let i=e.targetTouches?.[0],r=i?{x:i.clientX,y:i.clientY}:void 0;this._setupPointerExitEventsIfNeeded(),this._touchstartTimeout&&clearTimeout(this._touchstartTimeout);let o=500;this._touchstartTimeout=setTimeout(()=>{this._touchstartTimeout=null,this.show(void 0,r)},this._defaultOptions?.touchLongPressShowDelay??o)}])),this._addListeners(this._passiveListeners))}_setupPointerExitEventsIfNeeded(){if(this._pointerExitEventsInitialized)return;this._pointerExitEventsInitialized=!0;let e=[];if(this._platformSupportsMouseEvents())e.push(["mouseleave",i=>{let r=i.relatedTarget;(!r||!this._overlayRef?.overlayElement.contains(r))&&this.hide()}],["wheel",i=>this._wheelListener(i)]);else if(this.touchGestures!=="off"){this._disableNativeGesturesIfNecessary();let i=()=>{this._touchstartTimeout&&clearTimeout(this._touchstartTimeout),this.hide(this._defaultOptions?.touchendHideDelay)};e.push(["touchend",i],["touchcancel",i])}this._addListeners(e),this._passiveListeners.push(...e)}_addListeners(e){e.forEach(([i,r])=>{this._elementRef.nativeElement.addEventListener(i,r,i3)})}_platformSupportsMouseEvents(){return!this._platform.IOS&&!this._platform.ANDROID}_wheelListener(e){if(this._isTooltipVisible()){let i=this._injector.get(_e).elementFromPoint(e.clientX,e.clientY),r=this._elementRef.nativeElement;i!==r&&!r.contains(i)&&this.hide()}}_disableNativeGesturesIfNecessary(){let e=this.touchGestures;if(e!=="off"){let i=this._elementRef.nativeElement,r=i.style;(e==="on"||i.nodeName!=="INPUT"&&i.nodeName!=="TEXTAREA")&&(r.userSelect=r.msUserSelect=r.webkitUserSelect=r.MozUserSelect="none"),(e==="on"||!i.draggable)&&(r.webkitUserDrag="none"),r.touchAction="none",r.webkitTapHighlightColor="transparent"}}_syncAriaDescription(e){this._ariaDescriptionPending||(this._ariaDescriptionPending=!0,this._ariaDescriber.removeDescription(this._elementRef.nativeElement,e,"tooltip"),this._isDestroyed||vt({write:()=>{this._ariaDescriptionPending=!1,this.message&&!this.disabled&&this._ariaDescriber.describe(this._elementRef.nativeElement,this.message,"tooltip")}},{injector:this._injector}))}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matTooltip",""]],hostAttrs:[1,"mat-mdc-tooltip-trigger"],hostVars:2,hostBindings:function(i,r){i&2&&G("mat-mdc-tooltip-disabled",r.disabled)},inputs:{position:[0,"matTooltipPosition","position"],positionAtOrigin:[0,"matTooltipPositionAtOrigin","positionAtOrigin"],disabled:[0,"matTooltipDisabled","disabled"],showDelay:[0,"matTooltipShowDelay","showDelay"],hideDelay:[0,"matTooltipHideDelay","hideDelay"],touchGestures:[0,"matTooltipTouchGestures","touchGestures"],message:[0,"matTooltip","message"],tooltipClass:[0,"matTooltipClass","tooltipClass"]},exportAs:["matTooltip"]})}return t})(),a3=(()=>{class t{_changeDetectorRef=u(ye);_elementRef=u(Y);_isMultiline=!1;message;tooltipClass;_showTimeoutId;_hideTimeoutId;_triggerElement;_mouseLeaveHideDelay;_animationsDisabled=Qe();_tooltip;_closeOnInteraction=!1;_isVisible=!1;_onHide=new z;_showAnimation="mat-mdc-tooltip-show";_hideAnimation="mat-mdc-tooltip-hide";constructor(){}show(e){this._hideTimeoutId!=null&&clearTimeout(this._hideTimeoutId),this._showTimeoutId=setTimeout(()=>{this._toggleVisibility(!0),this._showTimeoutId=void 0},e)}hide(e){this._showTimeoutId!=null&&clearTimeout(this._showTimeoutId),this._hideTimeoutId=setTimeout(()=>{this._toggleVisibility(!1),this._hideTimeoutId=void 0},e)}afterHidden(){return this._onHide}isVisible(){return this._isVisible}ngOnDestroy(){this._cancelPendingAnimations(),this._onHide.complete(),this._triggerElement=null}_handleBodyInteraction(){this._closeOnInteraction&&this.hide(0)}_markForCheck(){this._changeDetectorRef.markForCheck()}_handleMouseLeave({relatedTarget:e}){(!e||!this._triggerElement.contains(e))&&(this.isVisible()?this.hide(this._mouseLeaveHideDelay):this._finalizeAnimation(!1))}_onShow(){this._isMultiline=this._isTooltipMultiline(),this._markForCheck()}_isTooltipMultiline(){let e=this._elementRef.nativeElement.getBoundingClientRect();return e.height>Oee&&e.width>=Ree}_handleAnimationEnd({animationName:e}){(e===this._showAnimation||e===this._hideAnimation)&&this._finalizeAnimation(e===this._showAnimation)}_cancelPendingAnimations(){this._showTimeoutId!=null&&clearTimeout(this._showTimeoutId),this._hideTimeoutId!=null&&clearTimeout(this._hideTimeoutId),this._showTimeoutId=this._hideTimeoutId=void 0}_finalizeAnimation(e){e?this._closeOnInteraction=!0:this.isVisible()||this._onHide.next()}_toggleVisibility(e){let i=this._tooltip.nativeElement,r=this._showAnimation,o=this._hideAnimation;if(i.classList.remove(e?o:r),i.classList.add(e?r:o),this._isVisible!==e&&(this._isVisible=e,this._changeDetectorRef.markForCheck()),e&&!this._animationsDisabled&&typeof getComputedStyle=="function"){let a=getComputedStyle(i);(a.getPropertyValue("animation-duration")==="0s"||a.getPropertyValue("animation-name")==="none")&&(this._animationsDisabled=!0)}e&&this._onShow(),this._animationsDisabled&&(i.classList.add("_mat-animation-noopable"),this._finalizeAnimation(e))}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-tooltip-component"]],viewQuery:function(i,r){if(i&1&&ie(Tee,7),i&2){let o;j(o=H())&&(r._tooltip=o.first)}},hostAttrs:["aria-hidden","true"],hostBindings:function(i,r){i&1&&S("mouseleave",function(a){return r._handleMouseLeave(a)})},decls:4,vars:4,consts:[["tooltip",""],[1,"mdc-tooltip","mat-mdc-tooltip",3,"animationend","ngClass"],[1,"mat-mdc-tooltip-surface","mdc-tooltip__surface"]],template:function(i,r){if(i&1){let o=q();m(0,"div",1,0),S("animationend",function(s){return k(o),T(r._handleAnimationEnd(s))}),m(2,"div",2),f(3),h()()}i&2&&(G("mdc-tooltip--multiline",r._isMultiline),v("ngClass",r.tooltipClass),g(3),N(r.message))},dependencies:[zd],styles:[`.mat-mdc-tooltip{position:relative;transform:scale(0);display:inline-flex}.mat-mdc-tooltip::before{content:"";top:0;right:0;bottom:0;left:0;z-index:-1;position:absolute}.mat-mdc-tooltip-panel-below .mat-mdc-tooltip::before{top:-8px}.mat-mdc-tooltip-panel-above .mat-mdc-tooltip::before{bottom:-8px}.mat-mdc-tooltip-panel-right .mat-mdc-tooltip::before{left:-8px}.mat-mdc-tooltip-panel-left .mat-mdc-tooltip::before{right:-8px}.mat-mdc-tooltip._mat-animation-noopable{animation:none;transform:scale(1)}.mat-mdc-tooltip-surface{word-break:normal;overflow-wrap:anywhere;padding:4px 8px;min-width:40px;max-width:200px;min-height:24px;max-height:40vh;box-sizing:border-box;overflow:hidden;text-align:center;will-change:transform,opacity;background-color:var(--mat-tooltip-container-color, var(--mat-sys-inverse-surface));color:var(--mat-tooltip-supporting-text-color, var(--mat-sys-inverse-on-surface));border-radius:var(--mat-tooltip-container-shape, var(--mat-sys-corner-extra-small));font-family:var(--mat-tooltip-supporting-text-font, var(--mat-sys-body-small-font));font-size:var(--mat-tooltip-supporting-text-size, var(--mat-sys-body-small-size));font-weight:var(--mat-tooltip-supporting-text-weight, var(--mat-sys-body-small-weight));line-height:var(--mat-tooltip-supporting-text-line-height, var(--mat-sys-body-small-line-height));letter-spacing:var(--mat-tooltip-supporting-text-tracking, var(--mat-sys-body-small-tracking))}.mat-mdc-tooltip-surface::before{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;border:1px solid rgba(0,0,0,0);border-radius:inherit;content:"";pointer-events:none}.mdc-tooltip--multiline .mat-mdc-tooltip-surface{text-align:left}[dir=rtl] .mdc-tooltip--multiline .mat-mdc-tooltip-surface{text-align:right}.mat-mdc-tooltip-panel{line-height:normal}.mat-mdc-tooltip-panel.mat-mdc-tooltip-panel-non-interactive{pointer-events:none}@keyframes mat-mdc-tooltip-show{0%{opacity:0;transform:scale(0.8)}100%{opacity:1;transform:scale(1)}}@keyframes mat-mdc-tooltip-hide{0%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(0.8)}}.mat-mdc-tooltip-show{animation:mat-mdc-tooltip-show 150ms cubic-bezier(0, 0, 0.2, 1) forwards}.mat-mdc-tooltip-hide{animation:mat-mdc-tooltip-hide 75ms cubic-bezier(0.4, 0, 1, 1) forwards} +`],encapsulation:2,changeDetection:0})}return t})();var An=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[gk],imports:[El,cr,De,De,Tr]})}return t})();var Pee=["matButton",""],c3=[[["",8,"material-icons",3,"iconPositionEnd",""],["mat-icon",3,"iconPositionEnd",""],["","matButtonIcon","",3,"iconPositionEnd",""]],"*",[["","iconPositionEnd","",8,"material-icons"],["mat-icon","iconPositionEnd",""],["","matButtonIcon","","iconPositionEnd",""]]],d3=[".material-icons:not([iconPositionEnd]), mat-icon:not([iconPositionEnd]), [matButtonIcon]:not([iconPositionEnd])","*",".material-icons[iconPositionEnd], mat-icon[iconPositionEnd], [matButtonIcon][iconPositionEnd]"],Fee=["mat-fab",""];var s3=new Map([["text",["mat-mdc-button"]],["filled",["mdc-button--unelevated","mat-mdc-unelevated-button"]],["elevated",["mdc-button--raised","mat-mdc-raised-button"]],["outlined",["mdc-button--outlined","mat-mdc-outlined-button"]],["tonal",["mat-tonal-button"]]]),_t=(()=>{class t extends wx{get appearance(){return this._appearance}set appearance(e){this.setAppearance(e||this._config?.defaultAppearance||"text")}_appearance=null;constructor(){super();let e=Nee(this._elementRef.nativeElement);e&&this.setAppearance(e)}setAppearance(e){if(e===this._appearance)return;let i=this._elementRef.nativeElement.classList,r=this._appearance?s3.get(this._appearance):null,o=s3.get(e);r&&i.remove(...r),i.add(...o),this._appearance=e}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["button","matButton",""],["a","matButton",""],["button","mat-button",""],["button","mat-raised-button",""],["button","mat-flat-button",""],["button","mat-stroked-button",""],["a","mat-button",""],["a","mat-raised-button",""],["a","mat-flat-button",""],["a","mat-stroked-button",""]],hostAttrs:[1,"mdc-button"],inputs:{appearance:[0,"matButton","appearance"]},exportAs:["matButton","matAnchor"],features:[le],attrs:Pee,ngContentSelectors:d3,decls:7,vars:4,consts:[[1,"mat-mdc-button-persistent-ripple"],[1,"mdc-button__label"],[1,"mat-focus-indicator"],[1,"mat-mdc-button-touch-target"]],template:function(i,r){i&1&&(Ee(c3),ni(0,"span",0),ne(1),gt(2,"span",1),ne(3,1),yt(),ne(4,2),ni(5,"span",2)(6,"span",3)),i&2&&G("mdc-button__ripple",!r._isFab)("mdc-fab__ripple",r._isFab)},styles:[`.mat-mdc-button-base{text-decoration:none}.mat-mdc-button-base .mat-icon{min-height:fit-content;flex-shrink:0}.mdc-button{-webkit-user-select:none;user-select:none;position:relative;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;min-width:64px;border:none;outline:none;line-height:inherit;-webkit-appearance:none;overflow:visible;vertical-align:middle;background:rgba(0,0,0,0);padding:0 8px}.mdc-button::-moz-focus-inner{padding:0;border:0}.mdc-button:active{outline:none}.mdc-button:hover{cursor:pointer}.mdc-button:disabled{cursor:default;pointer-events:none}.mdc-button[hidden]{display:none}.mdc-button .mdc-button__label{position:relative}.mat-mdc-button{padding:0 var(--mat-button-text-horizontal-padding, 12px);height:var(--mat-button-text-container-height, 40px);font-family:var(--mat-button-text-label-text-font, var(--mat-sys-label-large-font));font-size:var(--mat-button-text-label-text-size, var(--mat-sys-label-large-size));letter-spacing:var(--mat-button-text-label-text-tracking, var(--mat-sys-label-large-tracking));text-transform:var(--mat-button-text-label-text-transform);font-weight:var(--mat-button-text-label-text-weight, var(--mat-sys-label-large-weight))}.mat-mdc-button,.mat-mdc-button .mdc-button__ripple{border-radius:var(--mat-button-text-container-shape, var(--mat-sys-corner-full))}.mat-mdc-button:not(:disabled){color:var(--mat-button-text-label-text-color, var(--mat-sys-primary))}.mat-mdc-button[disabled],.mat-mdc-button.mat-mdc-button-disabled{cursor:default;pointer-events:none;color:var(--mat-button-text-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-mdc-button.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-mdc-button:has(.material-icons,mat-icon,[matButtonIcon]){padding:0 var(--mat-button-text-with-icon-horizontal-padding, 16px)}.mat-mdc-button>.mat-icon{margin-right:var(--mat-button-text-icon-spacing, 8px);margin-left:var(--mat-button-text-icon-offset, -4px)}[dir=rtl] .mat-mdc-button>.mat-icon{margin-right:var(--mat-button-text-icon-offset, -4px);margin-left:var(--mat-button-text-icon-spacing, 8px)}.mat-mdc-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-text-icon-offset, -4px);margin-left:var(--mat-button-text-icon-spacing, 8px)}[dir=rtl] .mat-mdc-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-text-icon-spacing, 8px);margin-left:var(--mat-button-text-icon-offset, -4px)}.mat-mdc-button .mat-ripple-element{background-color:var(--mat-button-text-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent))}.mat-mdc-button .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-text-state-layer-color, var(--mat-sys-primary))}.mat-mdc-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-text-disabled-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-mdc-button:hover>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-text-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-button.cdk-program-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-button.cdk-keyboard-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-button.mat-mdc-button-disabled-interactive:focus>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-text-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-button:active>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-text-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity))}.mat-mdc-button .mat-mdc-button-touch-target{position:absolute;top:50%;height:var(--mat-button-text-touch-target-size, 48px);display:var(--mat-button-text-touch-target-display, block);left:0;right:0;transform:translateY(-50%)}.mat-mdc-unelevated-button{transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);height:var(--mat-button-filled-container-height, 40px);font-family:var(--mat-button-filled-label-text-font, var(--mat-sys-label-large-font));font-size:var(--mat-button-filled-label-text-size, var(--mat-sys-label-large-size));letter-spacing:var(--mat-button-filled-label-text-tracking, var(--mat-sys-label-large-tracking));text-transform:var(--mat-button-filled-label-text-transform);font-weight:var(--mat-button-filled-label-text-weight, var(--mat-sys-label-large-weight));padding:0 var(--mat-button-filled-horizontal-padding, 24px)}.mat-mdc-unelevated-button>.mat-icon{margin-right:var(--mat-button-filled-icon-spacing, 8px);margin-left:var(--mat-button-filled-icon-offset, -8px)}[dir=rtl] .mat-mdc-unelevated-button>.mat-icon{margin-right:var(--mat-button-filled-icon-offset, -8px);margin-left:var(--mat-button-filled-icon-spacing, 8px)}.mat-mdc-unelevated-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-filled-icon-offset, -8px);margin-left:var(--mat-button-filled-icon-spacing, 8px)}[dir=rtl] .mat-mdc-unelevated-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-filled-icon-spacing, 8px);margin-left:var(--mat-button-filled-icon-offset, -8px)}.mat-mdc-unelevated-button .mat-ripple-element{background-color:var(--mat-button-filled-ripple-color, color-mix(in srgb, var(--mat-sys-on-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent))}.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-filled-state-layer-color, var(--mat-sys-on-primary))}.mat-mdc-unelevated-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-filled-disabled-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-mdc-unelevated-button:hover>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-filled-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-unelevated-button.cdk-program-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-unelevated-button.cdk-keyboard-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-unelevated-button.mat-mdc-button-disabled-interactive:focus>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-filled-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-unelevated-button:active>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-filled-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity))}.mat-mdc-unelevated-button .mat-mdc-button-touch-target{position:absolute;top:50%;height:var(--mat-button-filled-touch-target-size, 48px);display:var(--mat-button-filled-touch-target-display, block);left:0;right:0;transform:translateY(-50%)}.mat-mdc-unelevated-button:not(:disabled){color:var(--mat-button-filled-label-text-color, var(--mat-sys-on-primary));background-color:var(--mat-button-filled-container-color, var(--mat-sys-primary))}.mat-mdc-unelevated-button,.mat-mdc-unelevated-button .mdc-button__ripple{border-radius:var(--mat-button-filled-container-shape, var(--mat-sys-corner-full))}.mat-mdc-unelevated-button[disabled],.mat-mdc-unelevated-button.mat-mdc-button-disabled{cursor:default;pointer-events:none;color:var(--mat-button-filled-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));background-color:var(--mat-button-filled-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}.mat-mdc-unelevated-button.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-mdc-raised-button{transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);box-shadow:var(--mat-button-protected-container-elevation-shadow, var(--mat-sys-level1));height:var(--mat-button-protected-container-height, 40px);font-family:var(--mat-button-protected-label-text-font, var(--mat-sys-label-large-font));font-size:var(--mat-button-protected-label-text-size, var(--mat-sys-label-large-size));letter-spacing:var(--mat-button-protected-label-text-tracking, var(--mat-sys-label-large-tracking));text-transform:var(--mat-button-protected-label-text-transform);font-weight:var(--mat-button-protected-label-text-weight, var(--mat-sys-label-large-weight));padding:0 var(--mat-button-protected-horizontal-padding, 24px)}.mat-mdc-raised-button>.mat-icon{margin-right:var(--mat-button-protected-icon-spacing, 8px);margin-left:var(--mat-button-protected-icon-offset, -8px)}[dir=rtl] .mat-mdc-raised-button>.mat-icon{margin-right:var(--mat-button-protected-icon-offset, -8px);margin-left:var(--mat-button-protected-icon-spacing, 8px)}.mat-mdc-raised-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-protected-icon-offset, -8px);margin-left:var(--mat-button-protected-icon-spacing, 8px)}[dir=rtl] .mat-mdc-raised-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-protected-icon-spacing, 8px);margin-left:var(--mat-button-protected-icon-offset, -8px)}.mat-mdc-raised-button .mat-ripple-element{background-color:var(--mat-button-protected-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent))}.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-protected-state-layer-color, var(--mat-sys-primary))}.mat-mdc-raised-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-protected-disabled-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-mdc-raised-button:hover>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-protected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-raised-button.cdk-program-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-raised-button.cdk-keyboard-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-raised-button.mat-mdc-button-disabled-interactive:focus>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-protected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-raised-button:active>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-protected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity))}.mat-mdc-raised-button .mat-mdc-button-touch-target{position:absolute;top:50%;height:var(--mat-button-protected-touch-target-size, 48px);display:var(--mat-button-protected-touch-target-display, block);left:0;right:0;transform:translateY(-50%)}.mat-mdc-raised-button:not(:disabled){color:var(--mat-button-protected-label-text-color, var(--mat-sys-primary));background-color:var(--mat-button-protected-container-color, var(--mat-sys-surface))}.mat-mdc-raised-button,.mat-mdc-raised-button .mdc-button__ripple{border-radius:var(--mat-button-protected-container-shape, var(--mat-sys-corner-full))}.mat-mdc-raised-button:hover{box-shadow:var(--mat-button-protected-hover-container-elevation-shadow, var(--mat-sys-level2))}.mat-mdc-raised-button:focus{box-shadow:var(--mat-button-protected-focus-container-elevation-shadow, var(--mat-sys-level1))}.mat-mdc-raised-button:active,.mat-mdc-raised-button:focus:active{box-shadow:var(--mat-button-protected-pressed-container-elevation-shadow, var(--mat-sys-level1))}.mat-mdc-raised-button[disabled],.mat-mdc-raised-button.mat-mdc-button-disabled{cursor:default;pointer-events:none;color:var(--mat-button-protected-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));background-color:var(--mat-button-protected-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}.mat-mdc-raised-button[disabled].mat-mdc-button-disabled,.mat-mdc-raised-button.mat-mdc-button-disabled.mat-mdc-button-disabled{box-shadow:var(--mat-button-protected-disabled-container-elevation-shadow, var(--mat-sys-level0))}.mat-mdc-raised-button.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-mdc-outlined-button{border-style:solid;transition:border 280ms cubic-bezier(0.4, 0, 0.2, 1);height:var(--mat-button-outlined-container-height, 40px);font-family:var(--mat-button-outlined-label-text-font, var(--mat-sys-label-large-font));font-size:var(--mat-button-outlined-label-text-size, var(--mat-sys-label-large-size));letter-spacing:var(--mat-button-outlined-label-text-tracking, var(--mat-sys-label-large-tracking));text-transform:var(--mat-button-outlined-label-text-transform);font-weight:var(--mat-button-outlined-label-text-weight, var(--mat-sys-label-large-weight));border-radius:var(--mat-button-outlined-container-shape, var(--mat-sys-corner-full));border-width:var(--mat-button-outlined-outline-width, 1px);padding:0 var(--mat-button-outlined-horizontal-padding, 24px)}.mat-mdc-outlined-button>.mat-icon{margin-right:var(--mat-button-outlined-icon-spacing, 8px);margin-left:var(--mat-button-outlined-icon-offset, -8px)}[dir=rtl] .mat-mdc-outlined-button>.mat-icon{margin-right:var(--mat-button-outlined-icon-offset, -8px);margin-left:var(--mat-button-outlined-icon-spacing, 8px)}.mat-mdc-outlined-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-outlined-icon-offset, -8px);margin-left:var(--mat-button-outlined-icon-spacing, 8px)}[dir=rtl] .mat-mdc-outlined-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-outlined-icon-spacing, 8px);margin-left:var(--mat-button-outlined-icon-offset, -8px)}.mat-mdc-outlined-button .mat-ripple-element{background-color:var(--mat-button-outlined-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent))}.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-outlined-state-layer-color, var(--mat-sys-primary))}.mat-mdc-outlined-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-outlined-disabled-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-mdc-outlined-button:hover>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-outlined-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-outlined-button.cdk-program-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-outlined-button.cdk-keyboard-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-outlined-button.mat-mdc-button-disabled-interactive:focus>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-outlined-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-outlined-button:active>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-outlined-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity))}.mat-mdc-outlined-button .mat-mdc-button-touch-target{position:absolute;top:50%;height:var(--mat-button-outlined-touch-target-size, 48px);display:var(--mat-button-outlined-touch-target-display, block);left:0;right:0;transform:translateY(-50%)}.mat-mdc-outlined-button:not(:disabled){color:var(--mat-button-outlined-label-text-color, var(--mat-sys-primary));border-color:var(--mat-button-outlined-outline-color, var(--mat-sys-outline))}.mat-mdc-outlined-button[disabled],.mat-mdc-outlined-button.mat-mdc-button-disabled{cursor:default;pointer-events:none;color:var(--mat-button-outlined-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));border-color:var(--mat-button-outlined-disabled-outline-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}.mat-mdc-outlined-button.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-tonal-button{transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);height:var(--mat-button-tonal-container-height, 40px);font-family:var(--mat-button-tonal-label-text-font, var(--mat-sys-label-large-font));font-size:var(--mat-button-tonal-label-text-size, var(--mat-sys-label-large-size));letter-spacing:var(--mat-button-tonal-label-text-tracking, var(--mat-sys-label-large-tracking));text-transform:var(--mat-button-tonal-label-text-transform);font-weight:var(--mat-button-tonal-label-text-weight, var(--mat-sys-label-large-weight));padding:0 var(--mat-button-tonal-horizontal-padding, 24px)}.mat-tonal-button:not(:disabled){color:var(--mat-button-tonal-label-text-color, var(--mat-sys-on-secondary-container));background-color:var(--mat-button-tonal-container-color, var(--mat-sys-secondary-container))}.mat-tonal-button,.mat-tonal-button .mdc-button__ripple{border-radius:var(--mat-button-tonal-container-shape, var(--mat-sys-corner-full))}.mat-tonal-button[disabled],.mat-tonal-button.mat-mdc-button-disabled{cursor:default;pointer-events:none;color:var(--mat-button-tonal-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));background-color:var(--mat-button-tonal-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}.mat-tonal-button.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-tonal-button>.mat-icon{margin-right:var(--mat-button-tonal-icon-spacing, 8px);margin-left:var(--mat-button-tonal-icon-offset, -8px)}[dir=rtl] .mat-tonal-button>.mat-icon{margin-right:var(--mat-button-tonal-icon-offset, -8px);margin-left:var(--mat-button-tonal-icon-spacing, 8px)}.mat-tonal-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-tonal-icon-offset, -8px);margin-left:var(--mat-button-tonal-icon-spacing, 8px)}[dir=rtl] .mat-tonal-button .mdc-button__label+.mat-icon{margin-right:var(--mat-button-tonal-icon-spacing, 8px);margin-left:var(--mat-button-tonal-icon-offset, -8px)}.mat-tonal-button .mat-ripple-element{background-color:var(--mat-button-tonal-ripple-color, color-mix(in srgb, var(--mat-sys-on-secondary-container) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent))}.mat-tonal-button .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-tonal-state-layer-color, var(--mat-sys-on-secondary-container))}.mat-tonal-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-button-tonal-disabled-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-tonal-button:hover>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-tonal-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-tonal-button.cdk-program-focused>.mat-mdc-button-persistent-ripple::before,.mat-tonal-button.cdk-keyboard-focused>.mat-mdc-button-persistent-ripple::before,.mat-tonal-button.mat-mdc-button-disabled-interactive:focus>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-tonal-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-tonal-button:active>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-button-tonal-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity))}.mat-tonal-button .mat-mdc-button-touch-target{position:absolute;top:50%;height:var(--mat-button-tonal-touch-target-size, 48px);display:var(--mat-button-tonal-touch-target-display, block);left:0;right:0;transform:translateY(-50%)}.mat-mdc-button,.mat-mdc-unelevated-button,.mat-mdc-raised-button,.mat-mdc-outlined-button,.mat-tonal-button{-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-button .mat-mdc-button-ripple,.mat-mdc-button .mat-mdc-button-persistent-ripple,.mat-mdc-button .mat-mdc-button-persistent-ripple::before,.mat-mdc-unelevated-button .mat-mdc-button-ripple,.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple,.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before,.mat-mdc-raised-button .mat-mdc-button-ripple,.mat-mdc-raised-button .mat-mdc-button-persistent-ripple,.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before,.mat-mdc-outlined-button .mat-mdc-button-ripple,.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple,.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before,.mat-tonal-button .mat-mdc-button-ripple,.mat-tonal-button .mat-mdc-button-persistent-ripple,.mat-tonal-button .mat-mdc-button-persistent-ripple::before{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none;border-radius:inherit}.mat-mdc-button .mat-mdc-button-ripple,.mat-mdc-unelevated-button .mat-mdc-button-ripple,.mat-mdc-raised-button .mat-mdc-button-ripple,.mat-mdc-outlined-button .mat-mdc-button-ripple,.mat-tonal-button .mat-mdc-button-ripple{overflow:hidden}.mat-mdc-button .mat-mdc-button-persistent-ripple::before,.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before,.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before,.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before,.mat-tonal-button .mat-mdc-button-persistent-ripple::before{content:"";opacity:0}.mat-mdc-button .mdc-button__label,.mat-mdc-button .mat-icon,.mat-mdc-unelevated-button .mdc-button__label,.mat-mdc-unelevated-button .mat-icon,.mat-mdc-raised-button .mdc-button__label,.mat-mdc-raised-button .mat-icon,.mat-mdc-outlined-button .mdc-button__label,.mat-mdc-outlined-button .mat-icon,.mat-tonal-button .mdc-button__label,.mat-tonal-button .mat-icon{z-index:1;position:relative}.mat-mdc-button .mat-focus-indicator,.mat-mdc-unelevated-button .mat-focus-indicator,.mat-mdc-raised-button .mat-focus-indicator,.mat-mdc-outlined-button .mat-focus-indicator,.mat-tonal-button .mat-focus-indicator{top:0;left:0;right:0;bottom:0;position:absolute;border-radius:inherit}.mat-mdc-button:focus>.mat-focus-indicator::before,.mat-mdc-unelevated-button:focus>.mat-focus-indicator::before,.mat-mdc-raised-button:focus>.mat-focus-indicator::before,.mat-mdc-outlined-button:focus>.mat-focus-indicator::before,.mat-tonal-button:focus>.mat-focus-indicator::before{content:"";border-radius:inherit}.mat-mdc-button._mat-animation-noopable,.mat-mdc-unelevated-button._mat-animation-noopable,.mat-mdc-raised-button._mat-animation-noopable,.mat-mdc-outlined-button._mat-animation-noopable,.mat-tonal-button._mat-animation-noopable{transition:none !important;animation:none !important}.mat-mdc-button>.mat-icon,.mat-mdc-unelevated-button>.mat-icon,.mat-mdc-raised-button>.mat-icon,.mat-mdc-outlined-button>.mat-icon,.mat-tonal-button>.mat-icon{display:inline-block;position:relative;vertical-align:top;font-size:1.125rem;height:1.125rem;width:1.125rem}.mat-mdc-outlined-button .mat-mdc-button-ripple,.mat-mdc-outlined-button .mdc-button__ripple{top:-1px;left:-1px;bottom:-1px;right:-1px}.mat-mdc-unelevated-button .mat-focus-indicator::before,.mat-tonal-button .mat-focus-indicator::before,.mat-mdc-raised-button .mat-focus-indicator::before{margin:calc(calc(var(--mat-focus-indicator-border-width, 3px) + 2px)*-1)}.mat-mdc-outlined-button .mat-focus-indicator::before{margin:calc(calc(var(--mat-focus-indicator-border-width, 3px) + 3px)*-1)} +`,`@media(forced-colors: active){.mat-mdc-button:not(.mdc-button--outlined),.mat-mdc-unelevated-button:not(.mdc-button--outlined),.mat-mdc-raised-button:not(.mdc-button--outlined),.mat-mdc-outlined-button:not(.mdc-button--outlined),.mat-mdc-button-base.mat-tonal-button,.mat-mdc-icon-button.mat-mdc-icon-button,.mat-mdc-outlined-button .mdc-button__ripple{outline:solid 1px}} +`],encapsulation:2,changeDetection:0})}return t})();function Nee(t){return t.hasAttribute("mat-raised-button")?"elevated":t.hasAttribute("mat-stroked-button")?"outlined":t.hasAttribute("mat-flat-button")?"filled":t.hasAttribute("mat-button")?"text":null}var Lee=new O("mat-mdc-fab-default-options",{providedIn:"root",factory:u3});function u3(){return{color:"accent"}}var l3=u3(),Dx=(()=>{class t extends wx{_options=u(Lee,{optional:!0});_isFab=!0;extended;constructor(){super(),this._options=this._options||l3,this.color=this._options.color||l3.color}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["button","mat-fab",""],["a","mat-fab",""],["button","matFab",""],["a","matFab",""]],hostAttrs:[1,"mdc-fab","mat-mdc-fab-base","mat-mdc-fab"],hostVars:4,hostBindings:function(i,r){i&2&&G("mdc-fab--extended",r.extended)("mat-mdc-extended-fab",r.extended)},inputs:{extended:[2,"extended","extended",B]},exportAs:["matButton","matAnchor"],features:[le],attrs:Fee,ngContentSelectors:d3,decls:7,vars:4,consts:[[1,"mat-mdc-button-persistent-ripple"],[1,"mdc-button__label"],[1,"mat-focus-indicator"],[1,"mat-mdc-button-touch-target"]],template:function(i,r){i&1&&(Ee(c3),ni(0,"span",0),ne(1),gt(2,"span",1),ne(3,1),yt(),ne(4,2),ni(5,"span",2)(6,"span",3)),i&2&&G("mdc-button__ripple",!r._isFab)("mdc-fab__ripple",r._isFab)},styles:[`.mat-mdc-fab-base{-webkit-user-select:none;user-select:none;position:relative;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:56px;height:56px;padding:0;border:none;fill:currentColor;text-decoration:none;cursor:pointer;-moz-appearance:none;-webkit-appearance:none;overflow:visible;transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1),opacity 15ms linear 30ms,transform 270ms 0ms cubic-bezier(0, 0, 0.2, 1);flex-shrink:0;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-fab-base .mat-mdc-button-ripple,.mat-mdc-fab-base .mat-mdc-button-persistent-ripple,.mat-mdc-fab-base .mat-mdc-button-persistent-ripple::before{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none;border-radius:inherit}.mat-mdc-fab-base .mat-mdc-button-ripple{overflow:hidden}.mat-mdc-fab-base .mat-mdc-button-persistent-ripple::before{content:"";opacity:0}.mat-mdc-fab-base .mdc-button__label,.mat-mdc-fab-base .mat-icon{z-index:1;position:relative}.mat-mdc-fab-base .mat-focus-indicator{top:0;left:0;right:0;bottom:0;position:absolute}.mat-mdc-fab-base:focus>.mat-focus-indicator::before{content:""}.mat-mdc-fab-base._mat-animation-noopable{transition:none !important;animation:none !important}.mat-mdc-fab-base::before{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;border:1px solid rgba(0,0,0,0);border-radius:inherit;content:"";pointer-events:none}.mat-mdc-fab-base[hidden]{display:none}.mat-mdc-fab-base::-moz-focus-inner{padding:0;border:0}.mat-mdc-fab-base:active,.mat-mdc-fab-base:focus{outline:none}.mat-mdc-fab-base:hover{cursor:pointer}.mat-mdc-fab-base>svg{width:100%}.mat-mdc-fab-base .mat-icon,.mat-mdc-fab-base .material-icons{transition:transform 180ms 90ms cubic-bezier(0, 0, 0.2, 1);fill:currentColor;will-change:transform}.mat-mdc-fab-base .mat-focus-indicator::before{margin:calc(calc(var(--mat-focus-indicator-border-width, 3px) + 2px)*-1)}.mat-mdc-fab-base[disabled],.mat-mdc-fab-base.mat-mdc-button-disabled{cursor:default;pointer-events:none}.mat-mdc-fab-base[disabled],.mat-mdc-fab-base[disabled]:focus,.mat-mdc-fab-base.mat-mdc-button-disabled,.mat-mdc-fab-base.mat-mdc-button-disabled:focus{box-shadow:none}.mat-mdc-fab-base.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-mdc-fab{background-color:var(--mat-fab-container-color, var(--mat-sys-primary-container));border-radius:var(--mat-fab-container-shape, var(--mat-sys-corner-large));color:var(--mat-fab-foreground-color, var(--mat-sys-on-primary-container, inherit));box-shadow:var(--mat-fab-container-elevation-shadow, var(--mat-sys-level3))}.mat-mdc-fab:hover{box-shadow:var(--mat-fab-hover-container-elevation-shadow, var(--mat-sys-level4))}.mat-mdc-fab:focus{box-shadow:var(--mat-fab-focus-container-elevation-shadow, var(--mat-sys-level3))}.mat-mdc-fab:active,.mat-mdc-fab:focus:active{box-shadow:var(--mat-fab-pressed-container-elevation-shadow, var(--mat-sys-level3))}.mat-mdc-fab[disabled],.mat-mdc-fab.mat-mdc-button-disabled{cursor:default;pointer-events:none;color:var(--mat-fab-disabled-state-foreground-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));background-color:var(--mat-fab-disabled-state-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}.mat-mdc-fab.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-mdc-fab .mat-mdc-button-touch-target{position:absolute;top:50%;height:var(--mat-fab-touch-target-size, 48px);display:var(--mat-fab-touch-target-display, block);left:50%;width:var(--mat-fab-touch-target-size, 48px);transform:translate(-50%, -50%)}.mat-mdc-fab .mat-ripple-element{background-color:var(--mat-fab-ripple-color, color-mix(in srgb, var(--mat-sys-on-primary-container) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent))}.mat-mdc-fab .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-fab-state-layer-color, var(--mat-sys-on-primary-container))}.mat-mdc-fab.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-fab-disabled-state-layer-color)}.mat-mdc-fab:hover>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-fab-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-fab.cdk-program-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-fab.cdk-keyboard-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-fab.mat-mdc-button-disabled-interactive:focus>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-fab-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-fab:active>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-fab-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity))}.mat-mdc-mini-fab{width:40px;height:40px;background-color:var(--mat-fab-small-container-color, var(--mat-sys-primary-container));border-radius:var(--mat-fab-small-container-shape, var(--mat-sys-corner-medium));color:var(--mat-fab-small-foreground-color, var(--mat-sys-on-primary-container, inherit));box-shadow:var(--mat-fab-small-container-elevation-shadow, var(--mat-sys-level3))}.mat-mdc-mini-fab:hover{box-shadow:var(--mat-fab-small-hover-container-elevation-shadow, var(--mat-sys-level4))}.mat-mdc-mini-fab:focus{box-shadow:var(--mat-fab-small-focus-container-elevation-shadow, var(--mat-sys-level3))}.mat-mdc-mini-fab:active,.mat-mdc-mini-fab:focus:active{box-shadow:var(--mat-fab-small-pressed-container-elevation-shadow, var(--mat-sys-level3))}.mat-mdc-mini-fab[disabled],.mat-mdc-mini-fab.mat-mdc-button-disabled{cursor:default;pointer-events:none;color:var(--mat-fab-small-disabled-state-foreground-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));background-color:var(--mat-fab-small-disabled-state-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}.mat-mdc-mini-fab.mat-mdc-button-disabled-interactive{pointer-events:auto}.mat-mdc-mini-fab .mat-mdc-button-touch-target{position:absolute;top:50%;height:var(--mat-fab-small-touch-target-size, 48px);display:var(--mat-fab-small-touch-target-display);left:50%;width:var(--mat-fab-small-touch-target-size, 48px);transform:translate(-50%, -50%)}.mat-mdc-mini-fab .mat-ripple-element{background-color:var(--mat-fab-small-ripple-color, color-mix(in srgb, var(--mat-sys-on-primary-container) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent))}.mat-mdc-mini-fab .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-fab-small-state-layer-color, var(--mat-sys-on-primary-container))}.mat-mdc-mini-fab.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before{background-color:var(--mat-fab-small-disabled-state-layer-color)}.mat-mdc-mini-fab:hover>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-fab-small-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-mini-fab.cdk-program-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-mini-fab.cdk-keyboard-focused>.mat-mdc-button-persistent-ripple::before,.mat-mdc-mini-fab.mat-mdc-button-disabled-interactive:focus>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-fab-small-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-mini-fab:active>.mat-mdc-button-persistent-ripple::before{opacity:var(--mat-fab-small-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity))}.mat-mdc-extended-fab{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;padding-left:20px;padding-right:20px;width:auto;max-width:100%;line-height:normal;box-shadow:var(--mat-fab-extended-container-elevation-shadow, var(--mat-sys-level3));height:var(--mat-fab-extended-container-height, 56px);border-radius:var(--mat-fab-extended-container-shape, var(--mat-sys-corner-large));font-family:var(--mat-fab-extended-label-text-font, var(--mat-sys-label-large-font));font-size:var(--mat-fab-extended-label-text-size, var(--mat-sys-label-large-size));font-weight:var(--mat-fab-extended-label-text-weight, var(--mat-sys-label-large-weight));letter-spacing:var(--mat-fab-extended-label-text-tracking, var(--mat-sys-label-large-tracking))}.mat-mdc-extended-fab:hover{box-shadow:var(--mat-fab-extended-hover-container-elevation-shadow, var(--mat-sys-level4))}.mat-mdc-extended-fab:focus{box-shadow:var(--mat-fab-extended-focus-container-elevation-shadow, var(--mat-sys-level3))}.mat-mdc-extended-fab:active,.mat-mdc-extended-fab:focus:active{box-shadow:var(--mat-fab-extended-pressed-container-elevation-shadow, var(--mat-sys-level3))}.mat-mdc-extended-fab[disabled],.mat-mdc-extended-fab.mat-mdc-button-disabled{cursor:default;pointer-events:none}.mat-mdc-extended-fab[disabled],.mat-mdc-extended-fab[disabled]:focus,.mat-mdc-extended-fab.mat-mdc-button-disabled,.mat-mdc-extended-fab.mat-mdc-button-disabled:focus{box-shadow:none}.mat-mdc-extended-fab.mat-mdc-button-disabled-interactive{pointer-events:auto}[dir=rtl] .mat-mdc-extended-fab .mdc-button__label+.mat-icon,[dir=rtl] .mat-mdc-extended-fab .mdc-button__label+.material-icons,.mat-mdc-extended-fab>.mat-icon,.mat-mdc-extended-fab>.material-icons{margin-left:-8px;margin-right:12px}.mat-mdc-extended-fab .mdc-button__label+.mat-icon,.mat-mdc-extended-fab .mdc-button__label+.material-icons,[dir=rtl] .mat-mdc-extended-fab>.mat-icon,[dir=rtl] .mat-mdc-extended-fab>.material-icons{margin-left:12px;margin-right:-8px}.mat-mdc-extended-fab .mat-mdc-button-touch-target{width:100%} +`],encapsulation:2,changeDetection:0})}return t})();var Fe=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,Io,De]})}return t})();function Vee(t,n){if(t&1&&(m(0,"mat-option",17),f(1),h()),t&2){let e=n.$implicit;v("value",e),g(),fe(" ",e," ")}}function Bee(t,n){if(t&1){let e=q();m(0,"mat-form-field",14)(1,"mat-select",16,0),S("selectionChange",function(r){k(e);let o=x(2);return T(o._changePageSize(r.value))}),Mt(3,Vee,2,2,"mat-option",17,Em),h(),m(5,"div",18),S("click",function(){k(e);let r=Te(2);return T(r.open())}),h()()}if(t&2){let e=x(2);v("appearance",e._formFieldAppearance)("color",e.color),g(),v("value",e.pageSize)("disabled",e.disabled),pc("aria-labelledby",e._pageSizeLabelId),v("panelClass",e.selectConfig.panelClass||"")("disableOptionCentering",e.selectConfig.disableOptionCentering),g(2),Et(e._displayedPageSizeOptions)}}function jee(t,n){if(t&1&&(m(0,"div",15),f(1),h()),t&2){let e=x(2);g(),N(e.pageSize)}}function Hee(t,n){if(t&1&&(m(0,"div",3)(1,"div",13),f(2),h(),L(3,Bee,6,7,"mat-form-field",14),L(4,jee,2,1,"div",15),h()),t&2){let e=x();g(),X("id",e._pageSizeLabelId),g(),fe(" ",e._intl.itemsPerPageLabel," "),g(),V(e._displayedPageSizeOptions.length>1?3:-1),g(),V(e._displayedPageSizeOptions.length<=1?4:-1)}}function zee(t,n){if(t&1){let e=q();m(0,"button",19),S("click",function(){k(e);let r=x();return T(r._buttonClicked(0,r._previousButtonsDisabled()))}),ii(),m(1,"svg",8),M(2,"path",20),h()()}if(t&2){let e=x();v("matTooltip",e._intl.firstPageLabel)("matTooltipDisabled",e._previousButtonsDisabled())("disabled",e._previousButtonsDisabled())("tabindex",e._previousButtonsDisabled()?-1:null),X("aria-label",e._intl.firstPageLabel)}}function Uee(t,n){if(t&1){let e=q();m(0,"button",21),S("click",function(){k(e);let r=x();return T(r._buttonClicked(r.getNumberOfPages()-1,r._nextButtonsDisabled()))}),ii(),m(1,"svg",8),M(2,"path",22),h()()}if(t&2){let e=x();v("matTooltip",e._intl.lastPageLabel)("matTooltipDisabled",e._nextButtonsDisabled())("disabled",e._nextButtonsDisabled())("tabindex",e._nextButtonsDisabled()?-1:null),X("aria-label",e._intl.lastPageLabel)}}var Pc=(()=>{class t{changes=new z;itemsPerPageLabel="Items per page:";nextPageLabel="Next page";previousPageLabel="Previous page";firstPageLabel="First page";lastPageLabel="Last page";getRangeLabel=(e,i,r)=>{if(r==0||i==0)return`0 of ${r}`;r=Math.max(r,0);let o=e*i,a=o{class t{_intl=u(Pc);_changeDetectorRef=u(ye);_formFieldAppearance;_pageSizeLabelId=u(et).getId("mat-paginator-page-size-label-");_intlChanges;_isInitialized=!1;_initializedStream=new ss(1);color;get pageIndex(){return this._pageIndex}set pageIndex(e){this._pageIndex=Math.max(e||0,0),this._changeDetectorRef.markForCheck()}_pageIndex=0;get length(){return this._length}set length(e){this._length=e||0,this._changeDetectorRef.markForCheck()}_length=0;get pageSize(){return this._pageSize}set pageSize(e){this._pageSize=Math.max(e||0,0),this._updateDisplayedPageSizeOptions()}_pageSize;get pageSizeOptions(){return this._pageSizeOptions}set pageSizeOptions(e){this._pageSizeOptions=(e||[]).map(i=>ht(i,0)),this._updateDisplayedPageSizeOptions()}_pageSizeOptions=[];hidePageSize=!1;showFirstLastButtons=!1;selectConfig={};disabled=!1;page=new U;_displayedPageSizeOptions;initialized=this._initializedStream;constructor(){let e=this._intl,i=u(qee,{optional:!0});if(this._intlChanges=e.changes.subscribe(()=>this._changeDetectorRef.markForCheck()),i){let{pageSize:r,pageSizeOptions:o,hidePageSize:a,showFirstLastButtons:s}=i;r!=null&&(this._pageSize=r),o!=null&&(this._pageSizeOptions=o),a!=null&&(this.hidePageSize=a),s!=null&&(this.showFirstLastButtons=s)}this._formFieldAppearance=i?.formFieldAppearance||"outline"}ngOnInit(){this._isInitialized=!0,this._updateDisplayedPageSizeOptions(),this._initializedStream.next()}ngOnDestroy(){this._initializedStream.complete(),this._intlChanges.unsubscribe()}nextPage(){this.hasNextPage()&&this._navigate(this.pageIndex+1)}previousPage(){this.hasPreviousPage()&&this._navigate(this.pageIndex-1)}firstPage(){this.hasPreviousPage()&&this._navigate(0)}lastPage(){this.hasNextPage()&&this._navigate(this.getNumberOfPages()-1)}hasPreviousPage(){return this.pageIndex>=1&&this.pageSize!=0}hasNextPage(){let e=this.getNumberOfPages()-1;return this.pageIndexe-i),this._changeDetectorRef.markForCheck())}_emitPageEvent(e){this.page.emit({previousPageIndex:e,pageIndex:this.pageIndex,pageSize:this.pageSize,length:this.length})}_navigate(e){let i=this.pageIndex;e!==i&&(this.pageIndex=e,this._emitPageEvent(i))}_buttonClicked(e,i){i||this._navigate(e)}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-paginator"]],hostAttrs:["role","group",1,"mat-mdc-paginator"],inputs:{color:"color",pageIndex:[2,"pageIndex","pageIndex",ht],length:[2,"length","length",ht],pageSize:[2,"pageSize","pageSize",ht],pageSizeOptions:"pageSizeOptions",hidePageSize:[2,"hidePageSize","hidePageSize",B],showFirstLastButtons:[2,"showFirstLastButtons","showFirstLastButtons",B],selectConfig:"selectConfig",disabled:[2,"disabled","disabled",B]},outputs:{page:"page"},exportAs:["matPaginator"],decls:14,vars:14,consts:[["selectRef",""],[1,"mat-mdc-paginator-outer-container"],[1,"mat-mdc-paginator-container"],[1,"mat-mdc-paginator-page-size"],[1,"mat-mdc-paginator-range-actions"],["aria-live","polite",1,"mat-mdc-paginator-range-label"],["matIconButton","","type","button","matTooltipPosition","above","disabledInteractive","",1,"mat-mdc-paginator-navigation-first",3,"matTooltip","matTooltipDisabled","disabled","tabindex"],["matIconButton","","type","button","matTooltipPosition","above","disabledInteractive","",1,"mat-mdc-paginator-navigation-previous",3,"click","matTooltip","matTooltipDisabled","disabled","tabindex"],["viewBox","0 0 24 24","focusable","false","aria-hidden","true",1,"mat-mdc-paginator-icon"],["d","M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"],["matIconButton","","type","button","matTooltipPosition","above","disabledInteractive","",1,"mat-mdc-paginator-navigation-next",3,"click","matTooltip","matTooltipDisabled","disabled","tabindex"],["d","M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"],["matIconButton","","type","button","matTooltipPosition","above","disabledInteractive","",1,"mat-mdc-paginator-navigation-last",3,"matTooltip","matTooltipDisabled","disabled","tabindex"],[1,"mat-mdc-paginator-page-size-label"],[1,"mat-mdc-paginator-page-size-select",3,"appearance","color"],[1,"mat-mdc-paginator-page-size-value"],["hideSingleSelectionIndicator","",3,"selectionChange","value","disabled","aria-labelledby","panelClass","disableOptionCentering"],[3,"value"],[1,"mat-mdc-paginator-touch-target",3,"click"],["matIconButton","","type","button","matTooltipPosition","above","disabledInteractive","",1,"mat-mdc-paginator-navigation-first",3,"click","matTooltip","matTooltipDisabled","disabled","tabindex"],["d","M18.41 16.59L13.82 12l4.59-4.59L17 6l-6 6 6 6zM6 6h2v12H6z"],["matIconButton","","type","button","matTooltipPosition","above","disabledInteractive","",1,"mat-mdc-paginator-navigation-last",3,"click","matTooltip","matTooltipDisabled","disabled","tabindex"],["d","M5.59 7.41L10.18 12l-4.59 4.59L7 18l6-6-6-6zM16 6h2v12h-2z"]],template:function(i,r){i&1&&(m(0,"div",1)(1,"div",2),L(2,Hee,5,4,"div",3),m(3,"div",4)(4,"div",5),f(5),h(),L(6,zee,3,5,"button",6),m(7,"button",7),S("click",function(){return r._buttonClicked(r.pageIndex-1,r._previousButtonsDisabled())}),ii(),m(8,"svg",8),M(9,"path",9),h()(),Qo(),m(10,"button",10),S("click",function(){return r._buttonClicked(r.pageIndex+1,r._nextButtonsDisabled())}),ii(),m(11,"svg",8),M(12,"path",11),h()(),L(13,Uee,3,5,"button",12),h()()()),i&2&&(g(2),V(r.hidePageSize?-1:2),g(3),fe(" ",r._intl.getRangeLabel(r.pageIndex,r.pageSize,r.length)," "),g(),V(r.showFirstLastButtons?6:-1),g(),v("matTooltip",r._intl.previousPageLabel)("matTooltipDisabled",r._previousButtonsDisabled())("disabled",r._previousButtonsDisabled())("tabindex",r._previousButtonsDisabled()?-1:null),X("aria-label",r._intl.previousPageLabel),g(3),v("matTooltip",r._intl.nextPageLabel)("matTooltipDisabled",r._nextButtonsDisabled())("disabled",r._nextButtonsDisabled())("tabindex",r._nextButtonsDisabled()?-1:null),X("aria-label",r._intl.nextPageLabel),g(3),V(r.showFirstLastButtons?13:-1))},dependencies:[Xt,es,Sn,Ft,ur],styles:[`.mat-mdc-paginator{display:block;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:var(--mat-paginator-container-text-color, var(--mat-sys-on-surface));background-color:var(--mat-paginator-container-background-color, var(--mat-sys-surface));font-family:var(--mat-paginator-container-text-font, var(--mat-sys-body-small-font));line-height:var(--mat-paginator-container-text-line-height, var(--mat-sys-body-small-line-height));font-size:var(--mat-paginator-container-text-size, var(--mat-sys-body-small-size));font-weight:var(--mat-paginator-container-text-weight, var(--mat-sys-body-small-weight));letter-spacing:var(--mat-paginator-container-text-tracking, var(--mat-sys-body-small-tracking));--mat-form-field-container-height: var(--mat-paginator-form-field-container-height, 40px);--mat-form-field-container-vertical-padding: var(--mat-paginator-form-field-container-vertical-padding, 8px)}.mat-mdc-paginator .mat-mdc-select-value{font-size:var(--mat-paginator-select-trigger-text-size, var(--mat-sys-body-small-size))}.mat-mdc-paginator .mat-mdc-form-field-subscript-wrapper{display:none}.mat-mdc-paginator .mat-mdc-select{line-height:1.5}.mat-mdc-paginator-outer-container{display:flex}.mat-mdc-paginator-container{display:flex;align-items:center;justify-content:flex-end;padding:0 8px;flex-wrap:wrap;width:100%;min-height:var(--mat-paginator-container-size, 56px)}.mat-mdc-paginator-page-size{display:flex;align-items:baseline;margin-right:8px}[dir=rtl] .mat-mdc-paginator-page-size{margin-right:0;margin-left:8px}.mat-mdc-paginator-page-size-label{margin:0 4px}.mat-mdc-paginator-page-size-select{margin:0 4px;width:var(--mat-paginator-page-size-select-width, 84px)}.mat-mdc-paginator-range-label{margin:0 32px 0 24px}.mat-mdc-paginator-range-actions{display:flex;align-items:center}.mat-mdc-paginator-icon{display:inline-block;width:28px;fill:var(--mat-paginator-enabled-icon-color, var(--mat-sys-on-surface-variant))}.mat-mdc-icon-button[aria-disabled] .mat-mdc-paginator-icon{fill:var(--mat-paginator-disabled-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}[dir=rtl] .mat-mdc-paginator-icon{transform:rotate(180deg)}@media(forced-colors: active){.mat-mdc-icon-button[aria-disabled] .mat-mdc-paginator-icon,.mat-mdc-paginator-icon{fill:currentColor}.mat-mdc-paginator-range-actions .mat-mdc-icon-button{outline:solid 1px}.mat-mdc-paginator-range-actions .mat-mdc-icon-button[aria-disabled]{color:GrayText}}.mat-mdc-paginator-touch-target{display:var(--mat-paginator-touch-target-display, block);position:absolute;top:50%;left:50%;width:var(--mat-paginator-page-size-select-width, 84px);height:var(--mat-paginator-page-size-select-touch-target-height, 48px);background-color:rgba(0,0,0,0);transform:translate(-50%, -50%);cursor:pointer} +`],encapsulation:2,changeDetection:0})}return t})(),Fc=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[Wee],imports:[Fe,Rc,An,mr]})}return t})();var Cg=class extends $i{constructor(){super(...arguments),this._delegate=u($i)}getValidDateOrNull(n){return this.isDateInstance(n)&&this.isValid(n)?n:null}compareDatetime(n,e,i=!0){return this.compareDate(n,e)||this.getHour(n)-this.getHour(e)||i&&this.getMinute(n)-this.getMinute(e)}sameDatetime(n,e){if(n&&e){let i=this.isValid(n),r=this.isValid(e);return i&&r?!this.compareDatetime(n,e):i===r}return n===e}sameYear(n,e){return n&&e&&this.getYear(n)===this.getYear(e)}sameDay(n,e){return n&&e&&this.getDate(n)===this.getDate(e)&&this.sameMonthAndYear(n,e)}sameHour(n,e){return n&&e&&this.getHour(n)===this.getHour(e)&&this.sameDay(n,e)}sameMinute(n,e){return n&&e&&this.getMinute(n)===this.getMinute(e)&&this.sameHour(n,e)}sameMonthAndYear(n,e){if(n&&e){let i=this.isValid(n),r=this.isValid(e);return i&&r?!(this.getYear(n)-this.getYear(e)||this.getMonth(n)-this.getMonth(e)):i===r}return n===e}clone(n){return this._delegate.clone(n)}addCalendarYears(n,e){return this._delegate.addCalendarYears(n,e)}addCalendarMonths(n,e){return this._delegate.addCalendarMonths(n,e)}addCalendarDays(n,e){return this._delegate.addCalendarDays(n,e)}getYear(n){return this._delegate.getYear(n)}getMonth(n){return this._delegate.getMonth(n)}getDate(n){return this._delegate.getDate(n)}getDayOfWeek(n){return this._delegate.getDayOfWeek(n)}getMonthNames(n){return this._delegate.getMonthNames(n)}getDateNames(){return this._delegate.getDateNames()}getDayOfWeekNames(n){return this._delegate.getDayOfWeekNames(n)}getYearName(n){return this._delegate.getYearName(n)}getFirstDayOfWeek(){return this._delegate.getFirstDayOfWeek()}getNumDaysInMonth(n){return this._delegate.getNumDaysInMonth(n)}createDate(n,e,i){return this._delegate.createDate(n,e,i)}today(){return this._delegate.today()}parse(n,e){return this._delegate.parse(n,e)}format(n,e){return this._delegate.format(n,e)}toIso8601(n){return this._delegate.toIso8601(n)}isDateInstance(n){return this._delegate.isDateInstance(n)}isValid(n){return this._delegate.isValid(n)}invalid(){return this._delegate.invalid()}clampDate(n,e,i){return e&&this.compareDatetime(n,e)<0?e:i&&this.compareDatetime(n,i)>0?i:n}},m3=new O("mtx-datetime-formats"),sqe=h3(24,t=>String(t)),lqe=h3(60,t=>String(t));function h3(t,n){let e=Array(t);for(let i=0;i{let n=class n{transform(i){return Gi(i)?i:Q(i)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275pipe=io({name:"toObservable",type:n,pure:!0});let t=n;return t})();var f3=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275mod=ee({type:n}),n.\u0275inj=J({imports:[Je]});let t=n;return t})();function g3(t,n){let e=Array(t);for(let i=0;i{let n=class n extends Cg{constructor(){super();let i=u(Ls,{optional:!0});this.setLocale(i)}setLocale(i){super.setLocale(i)}getHour(i){return my(i)}getMinute(i){return hy(i)}isInNextMonth(i,r){let o=this.getDateInNextMonth(i);return super.sameMonthAndYear(o,r)}getWeek(i,r){return fh(i,{weekStartsOn:r})}createDatetime(i,r,o,a,s){if(r<0||r>11)throw Error(`Invalid month index "${r}". Month index has to be between 0 and 11.`);if(o<1)throw Error(`Invalid date "${o}". Date has to be greater than 0.`);if(a<0||a>23)throw Error(`Invalid hour "${a}". Hour has to be between 0 and 23.`);if(s<0||s>59)throw Error(`Invalid minute "${s}". Minute has to be between 0 and 59.`);let l=new Date(i,r,o,a,s);if(!mh(l))throw Error(`Invalid date "${o}" for month with index "${r}".`);return l}getFirstDateOfMonth(i){return pV(i)}getHourNames(){return g3(24,i=>i.toLocaleString(this.locale))}getMinuteNames(){return g3(60,i=>i.toLocaleString(this.locale))}addCalendarHours(i,r){return lV(i,r)}addCalendarMinutes(i,r){return uV(i,r)}deserialize(i){return this._delegate.deserialize(i)}getDateInNextMonth(i){return uh(i,1)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),Kee={parse:{dateInput:"P",monthInput:"LLLL",yearInput:"yyyy",datetimeInput:"P p",timeInput:"p"},display:{dateInput:"P",monthInput:"LLLL",yearInput:"yyyy",datetimeInput:"P p",timeInput:"p",monthYearLabel:"yyyy",dateA11yLabel:"LLLL dd, yyyy",monthYearA11yLabel:"MMMM yyyy",popupHeaderDateLabel:"ccc, dd LLL"}};function _3(t=Kee){return[{provide:$i,useClass:WS,deps:[Ls]},{provide:Cg,useClass:Qee},{provide:m3,useValue:t}]}var Zee=["container"];function Xee(t,n){}function Jee(t,n){}var ete=["*"];function tte(t,n){if(t&1&&M(0,"formly-field",0),t&2){let e=n.$implicit;v("field",e)}}var b3=["fieldComponent"];function ite(t,n,e){if(n.id)return n.id;let i=n.type;return!i&&n.template&&(i="template"),i instanceof Vd&&(i=i.prototype.constructor.name),[t,i,n.key,e].join("_")}function bn(t){return!Nc(t.key)&&t.key!==""&&(!Array.isArray(t.key)||t.key.length>0)}function _u(t){if(!bn(t))return[];if(t._keyPath?.key!==t.key){let n=[];if(typeof t.key=="string"){let e=t.key.indexOf("[")===-1?t.key:t.key.replace(/\[(\w+)\]/g,".$1");n=e.indexOf(".")!==-1?e.split("."):[e]}else Array.isArray(t.key)?n=t.key.slice(0):n=[`${t.key}`];hr(t,"_keyPath",{key:t.key,path:n})}return t._keyPath.path.slice(0)}var wk=["required","pattern","minLength","maxLength","min","max"];function wg(t,n){let e=_u(t);if(e.length===0)return;let i=t;for(;i.parent;)i=i.parent,e=[..._u(i),...e];if(n===void 0&&t.resetOnHide){let r=e.pop(),o=e.reduce((a,s)=>a[s]||{},i.model);delete o[r];return}nte(i.model,e,n)}function nte(t,n,e){for(let i=0;i{for(let i in e)Nc(t[i])||rte(t[i])?t[i]=Lc(e[i]):ote(t[i],e[i])&&Sh(t[i],e[i])}),t}function Nc(t){return t==null}function Ex(t){return t===void 0}function rte(t){return t===""}function _k(t){return typeof t=="function"}function ote(t,n){return ts(t)&&ts(n)&&Object.getPrototypeOf(t)===Object.getPrototypeOf(n)&&!(Array.isArray(t)||Array.isArray(n))}function ts(t){return t!=null&&typeof t=="object"}function ate(t){return!!t&&typeof t.then=="function"}function Lc(t){if(!ts(t)||Gi(t)||t instanceof te||t.changingThisBreaksApplicationSecurity||["RegExp","FileList","File","Blob"].indexOf(t.constructor?.name)!==-1)return t;if(t instanceof Set)return new Set(t);if(t instanceof Map)return new Map(t);if(t instanceof Uint8Array)return new Uint8Array(t);if(t instanceof Uint16Array)return new Uint16Array(t);if(t instanceof Uint32Array)return new Uint32Array(t);if(t._isAMomentObject&&_k(t.clone))return t.clone();if(t instanceof Ac)return null;if(t instanceof Date)return new Date(t.getTime());if(Array.isArray(t))return t.slice(0).map(i=>Lc(i));let n=Object.getPrototypeOf(t),e=Object.create(n);return e=Object.setPrototypeOf(e,n),Object.keys(t).reduce((i,r)=>{let o=Object.getOwnPropertyDescriptor(t,r);return o.get?Object.defineProperty(i,r,o):i[r]=Lc(t[r]),i},e)}function hr(t,n,e){Object.defineProperty(t,n,{enumerable:!1,writable:!0,configurable:!0}),t[n]=e}function v3(t,n,e){let i=[],r=()=>{i.forEach(a=>a()),i=[]},o=Zi(t,n,({firstChange:a,currentValue:s})=>{!a&&e(),r(),ts(s)&&s.constructor.name==="Object"&&Object.keys(s).forEach(l=>{i.push(v3(t,[...n,l],e))})});return()=>{o.unsubscribe(),r()}}function Zi(t,n,e){t._observers||hr(t,"_observers",{});let i=t;for(let s=0;s=1&&ts(i))){let{enumerable:s}=Object.getOwnPropertyDescriptor(i,r)||{enumerable:!0};Object.defineProperty(i,r,{enumerable:s,configurable:!0,get:()=>a.value,set:l=>{if(l!==a.value){let c=a.value;a.value=l,a.onChange.forEach(d=>d({previousValue:c,currentValue:l,firstChange:!1}))}}})}return{setValue(s,l=!0){if(s===a.value)return;let c=a.value;a.value=s,a.onChange.forEach(d=>{d!==e&&l&&d({previousValue:c,currentValue:s,firstChange:!1})})},unsubscribe(){a.onChange=a.onChange.filter(s=>s!==e),a.onChange.length===0&&delete t._observers[o]}}}function y3(t,n){if(n=Array.isArray(n)?n.join("."):n,!!t.fieldGroup)for(let e=0,i=t.fieldGroup.length;e{n instanceof Bd?n.injector.get(ye).markForCheck():n.markForCheck()})}function ste(t){let n=i=>i.hide||i.expressions?.hide||i.hideExpression,e=!t.resetOnHide||!n(t);if(!n(t)&&t.resetOnHide){let i=t.parent;for(;i&&!n(i);)i=i.parent;e=!i||!n(i)}return!e}function x3(){return+Tp.major>18||+Tp.major>=18&&+Tp.minor>=1}function lte(t,n){try{return Function(...n,`return ${t};`)}catch(e){console.error(e)}}function cte(t,n,e){return typeof t=="function"?t.apply(n,e):!!t}function dte(t,n=!1){let e=t.formControl,i=e._fields?e._fields.indexOf(t):-1;i!==-1&&e._fields.splice(i,1);let r=e.parent;if(!r)return;let o={emitEvent:n};if(r instanceof mk){let a=r.controls.findIndex(s=>s===e);a!==-1&&r.removeAt(a,o)}else if(r instanceof yx){let a=_u(t),s=a[a.length-1];r.get([s])===e&&r.removeControl(s,o)}e.setParent(null)}function ute(t){return t.formControl?t.formControl:t.shareFormControl===!1?null:t.form?.get(_u(t))}function C3(t,n,e=!1){if(n=n||t.formControl,n._fields||hr(n,"_fields",[]),n._fields.indexOf(t)===-1&&n._fields.push(t),!t.formControl&&n){hr(t,"formControl",n),n.setValidators(null),n.setAsyncValidators(null),t.props.disabled=!!t.props.disabled;let s=Zi(t,["props","disabled"],({firstChange:l,currentValue:c})=>{l||(c?t.formControl.disable():t.formControl.enable())});n instanceof aa&&n.registerOnDisabledChange(s.setValue)}if(!t.form||!bn(t))return;let i=t.form,r=_u(t),o=bu(t);!(Nc(n.value)&&Nc(o))&&n.value!==o&&n instanceof aa&&n.patchValue(o);for(let s=0;s{hr(n,"_hide",!!i),(!r||r&&i===!0)&&(n.props.hidden=i,n.options._hiddenFieldsForCheck.push({field:n}))}),n.hideExpression&&Zi(n,["hideExpression"],({currentValue:i})=>{n._expressions.hide=this.parseExpressions(n,"hide",typeof i=="boolean"?()=>i:i)});let e=(i,r)=>{typeof r=="string"||_k(r)?n._expressions[i]=this.parseExpressions(n,i,r):r instanceof Ne&&(n._expressions[i]={value$:r.pipe(He(o=>{this.evalExpr(n,i,o),n.options._detectChanges(n)}))})};n.expressions=n.expressions||{};for(let i of Object.keys(n.expressions))Zi(n,["expressions",i],({currentValue:r})=>{e(i,_k(r)?(...o)=>r(n,o[3]):r)});n.expressionProperties=n.expressionProperties||{};for(let i of Object.keys(n.expressionProperties))Zi(n,["expressionProperties",i],({currentValue:r})=>e(i,r))}postPopulate(n){if(!n.parent&&!n.options.checkExpressions){let e=!1;n.options.checkExpressions=(i,r)=>{if(e)return;e=!0;let o=this.checkExpressions(i,r),a=n.options;a._hiddenFieldsForCheck.sort(s=>s.field.hide?-1:1).forEach(s=>this.changeHideState(s.field,s.field.hide??s.default,!r)),a._hiddenFieldsForCheck=[],o&&this.checkExpressions(n),e=!1}}}parseExpressions(n,e,i){let r;if(n.parent&&["hide","props.disabled"].includes(e)){let a=s=>e==="hide"?s.hide:s.props.disabled;r=()=>{let s=n.parent;for(;s.parent&&!a(s);)s=s.parent;return a(s)}}i=i||(()=>!1),typeof i=="string"&&(i=lte(i,["model","formState","field"]));let o;return{callback:a=>{try{let s=cte(r?(...l)=>r(n)||i(...l):i,{field:n},[n.model,n.options.formState,n,a]);return a||o!==s&&(!ts(s)||Gi(s)||JSON.stringify(s)!==JSON.stringify(o))?(o=s,this.evalExpr(n,e,s),!0):!1}catch(s){throw s.message=`[Formly Error] [Expression "${e}"] ${s.message}`,s}}}}checkExpressions(n,e=!1){if(!n)return!1;let i=!1;if(n._expressions)for(let r of Object.keys(n._expressions))n._expressions[r].callback?.(e)&&(i=!0);return n.fieldGroup?.forEach(r=>this.checkExpressions(r,e)&&(i=!0)),i}changeDisabledState(n,e){n.fieldGroup&&n.fieldGroup.filter(i=>!i._expressions.hasOwnProperty("props.disabled")).forEach(i=>this.changeDisabledState(i,e)),bn(n)&&n.props.disabled!==e&&(n.props.disabled=e)}changeHideState(n,e,i){if(n.fieldGroup&&n.fieldGroup.filter(r=>r&&!r._expressions.hide).forEach(r=>this.changeHideState(r,e,i)),n.formControl&&bn(n)){hr(n,"_hide",!!(e||n.hide));let r=n.formControl;r._fields?.length>1&&Dg(r),e===!0&&(!r._fields||r._fields.every(o=>!!o._hide))?(dte(n,!0),i&&n.resetOnHide&&(wg(n,void 0),n.formControl.reset({value:void 0,disabled:n.formControl.disabled}),n.options.fieldChanges.next({value:void 0,field:n,type:"valueChanges"}),n.fieldGroup&&n.formControl instanceof mk&&(n.fieldGroup.length=0))):e===!1&&(n.resetOnHide&&!Ex(n.defaultValue)&&Ex(bu(n))&&wg(n,n.defaultValue),C3(n,void 0,!0),n.resetOnHide&&n.fieldArray&&n.fieldGroup?.length!==n.model?.length&&n.options.build(n))}n.options.fieldChanges&&n.options.fieldChanges.next({field:n,type:"hidden",value:e})}evalExpr(n,e,i){if(e.indexOf("model.")===0){let r=e.replace(/^model\./,""),o=n.fieldGroup?n:n.parent,a=n?.key===r?n.formControl:n.form.get(r);!a&&n.get(r)&&(a=n.get(r).formControl),wg({key:r,parent:o,model:n.model},i),a&&!(Nc(a.value)&&Nc(i))&&a.value!==i&&a.patchValue(i)}else{try{let r=n,o=this._evalExpressionPath(n,e),a=o.length-1;for(let s=0;sr).forEach(r=>{let o=r.match(/['|"](.*?)['|"]/);o?i.push(o[1]):i.push(...r.split(".").filter(a=>a))}),n._expressions[e]&&(n._expressions[e].paths=i),i}},yk=class{constructor(n){this.config=n,this.formId=0}prePopulate(n){let e=n.parent;this.initRootOptions(n),this.initFieldProps(n),e&&(Object.defineProperty(n,"options",{get:()=>e.options,configurable:!0}),Object.defineProperty(n,"model",{get:()=>bn(n)&&n.fieldGroup?bu(n):e.model,configurable:!0})),Object.defineProperty(n,"get",{value:i=>y3(n,i),configurable:!0}),this.getFieldComponentInstance(n).prePopulate?.(n)}onPopulate(n){this.initFieldOptions(n),this.getFieldComponentInstance(n).onPopulate?.(n),n.fieldGroup&&n.fieldGroup.forEach((e,i)=>{e&&(Object.defineProperty(e,"parent",{get:()=>n,configurable:!0}),Object.defineProperty(e,"index",{get:()=>i,configurable:!0})),this.formId++})}postPopulate(n){this.getFieldComponentInstance(n).postPopulate?.(n)}initFieldProps(n){n.props??=n.templateOptions,Object.defineProperty(n,"templateOptions",{get:()=>n.props,set:e=>n.props=e,configurable:!0})}initRootOptions(n){if(n.parent)return;let e=n.options;n.options.formState=n.options.formState||{},e.showError||(e.showError=this.config.extras.showError),e.fieldChanges||hr(e,"fieldChanges",new z),e._hiddenFieldsForCheck||(e._hiddenFieldsForCheck=[]),e._detectChanges=i=>{i._componentRefs&&bk(i),i.fieldGroup?.forEach(r=>r&&e._detectChanges(r))},e.detectChanges=i=>{i.options.checkExpressions?.(i),e._detectChanges(i)},e.resetModel=i=>{i=Lc(i??e._initialModel),n.model&&(Object.keys(n.model).forEach(r=>delete n.model[r]),Object.assign(n.model,i||{})),x3()||Zi(e,["parentForm","submitted"]).setValue(!1,!1),e.build(n),n.form.reset(n.model)},e.updateInitialValue=i=>e._initialModel=Lc(i??n.model),n.options.updateInitialValue()}initFieldOptions(n){Sh(n,{id:ite(`formly_${this.formId}`,n,n.index),hooks:{},modelOptions:{},validation:{messages:{}},props:!n.type||!bn(n)?{}:{label:"",placeholder:"",disabled:!1}}),this.config.extras.resetFieldOnHide&&n.resetOnHide!==!1&&(n.resetOnHide=!0),n.type!=="formly-template"&&(n.template||n.expressions?.template||n.expressionProperties?.template)&&(n.type="formly-template"),!n.type&&n.fieldGroup&&(n.type="formly-group"),n.type&&this.config.getMergedField(n),bn(n)&&!Ex(n.defaultValue)&&Ex(bu(n))&&!ste(n)&&wg(n,n.defaultValue),n.wrappers=n.wrappers||[]}getFieldComponentInstance(n){let e=()=>{let i=this.config.resolveFieldTypeRef(n),r=n._componentRefs?.slice(-1)[0];return r instanceof Bd&&r?.componentType===i?.componentType&&(i=r),i?.instance};return n._proxyInstance||hr(n,"_proxyInstance",new Proxy({},{get:(i,r)=>e()?.[r],set:(i,r,o)=>e()[r]=o})),n._proxyInstance}},xk=class{prePopulate(n){this.root||(this.root=n),n.parent&&Object.defineProperty(n,"form",{get:()=>n.parent.formControl,configurable:!0})}onPopulate(n){n.hasOwnProperty("fieldGroup")&&!bn(n)?hr(n,"formControl",n.form):this.addFormControl(n)}postPopulate(n){if(this.root!==n)return;if(this.root=null,this.setValidators(n)&&n.parent){let i=n.parent;for(;i;)(bn(i)||!i.parent)&&Dg(i.formControl,!0),i=i.parent}}addFormControl(n){let e=ute(n);if(!n.fieldArray){if(e){if(e instanceof aa){let i=bn(n)?bu(n):n.defaultValue;e.defaultValue=i}}else{let i={updateOn:n.modelOptions.updateOn};if(n.fieldGroup)e=new yx({},i);else{let r=bn(n)?bu(n):n.defaultValue;e=new YB({value:r,disabled:!!n.props.disabled},Me(I({},i),{initialValueIsDefault:!0}))}}C3(n,e)}}setValidators(n,e=!1){e===!1&&bn(n)&&n.props?.disabled&&(e=!0);let i=!1;if(n.fieldGroup?.forEach(r=>r&&this.setValidators(r,e)&&(i=!0)),bn(n)||!n.parent||!bn(n)&&!n.fieldGroup){let{formControl:r}=n;if(r&&(bn(n)&&r instanceof aa&&(e&&r.enabled&&(r.disable({emitEvent:!1,onlySelf:!0}),i=!0),!e&&r.disabled&&(r.enable({emitEvent:!1,onlySelf:!0}),i=!0)),r.validator===null&&this.hasValidators(n,"_validators")&&(r.setValidators(()=>{let o=Ve.compose(this.mergeValidators(n,"_validators"));return o?o(r):null}),i=!0),r.asyncValidator===null&&this.hasValidators(n,"_asyncValidators")&&(r.setAsyncValidators(()=>{let o=Ve.composeAsync(this.mergeValidators(n,"_asyncValidators"));return o?o(r):Q(null)}),i=!0),i)){Dg(r,!0);let o=r.parent;for(let a=1;a<_u(n).length;a++)o&&(Dg(o,!0),o=o.parent)}}return i}hasValidators(n,e){let i=n.formControl;return i?._fields?.length>1&&i._fields.some(r=>r[e].length>0)||n[e].length>0?!0:n.fieldGroup?.some(r=>r?.fieldGroup&&!bn(r)&&this.hasValidators(r,e))}mergeValidators(n,e){let i=[],r=n.formControl;return r?._fields?.length>1?r._fields.filter(o=>!o._hide).forEach(o=>i.push(...o[e])):n[e]&&i.push(...n[e]),n.fieldGroup&&n.fieldGroup.filter(o=>o?.fieldGroup&&!bn(o)).forEach(o=>i.push(...this.mergeValidators(o,e))),i}},Ck=class{constructor(n){this.config=n}onPopulate(n){this.initFieldValidation(n,"validators"),this.initFieldValidation(n,"asyncValidators")}initFieldValidation(n,e){let i=[];if(e==="validators"&&!(n.hasOwnProperty("fieldGroup")&&!bn(n))&&i.push(this.getPredefinedFieldValidation(n)),n[e])for(let r of Object.keys(n[e]))r==="validation"?i.push(...n[e].validation.map(o=>this.wrapNgValidatorFn(n,o))):i.push(this.wrapNgValidatorFn(n,n[e][r],r));hr(n,"_"+e,i)}getPredefinedFieldValidation(n){let e=[];return wk.forEach(i=>Zi(n,["props",i],({currentValue:r,firstChange:o})=>{e=e.filter(a=>a!==i),i==="required"&&r!=null&&typeof r!="boolean"&&console.warn(`Formly: Invalid prop 'required' of type '${typeof r}', expected 'boolean' (Field:${n.key}).`),r!=null&&r!==!1&&e.push(i),!o&&n.formControl&&Dg(n.formControl)})),i=>e.length===0?null:Ve.compose(e.map(r=>()=>{let o=n.props[r];switch(r){case"required":return Ve.required(i);case"pattern":return Ve.pattern(o)(i);case"minLength":let a=Ve.minLength(o)(i),s=this.config.getValidatorMessage("minlength")||n.validation?.messages?.minlength?"minlength":"minLength";return a?{[s]:a.minlength}:null;case"maxLength":let l=Ve.maxLength(o)(i),c=this.config.getValidatorMessage("maxlength")||n.validation?.messages?.maxlength?"maxlength":"maxLength";return l?{[c]:l.maxlength}:null;case"min":return Ve.min(o)(i);case"max":return Ve.max(o)(i);default:return null}}))(i)}wrapNgValidatorFn(n,e,i){let r;if(typeof e=="string"&&(r=Lc(this.config.getValidator(e))),typeof e=="object"&&e.name&&(r=Lc(this.config.getValidator(e.name)),e.options&&(r.options=e.options)),typeof e=="object"&&e.expression){let o=e,{expression:a}=o,s=cd(o,["expression"]);r={name:i,validation:a,options:Object.keys(s).length>0?s:null}}return typeof e=="function"&&(r={name:i,validation:e}),a=>{let s=r.validation(a,n,r.options);return ate(s)?s.then(l=>this.handleResult(n,i?!!l:l,r)):Gi(s)?s.pipe(se(l=>this.handleResult(n,i?!!l:l,r))):this.handleResult(n,i?!!s:s,r)}}handleResult(n,e,{name:i,options:r}){typeof e=="boolean"&&(e=e?null:{[i]:r||!0});let o=n.formControl;return o?._childrenErrors?.[i]?.(),ts(e)&&Object.keys(e).forEach(a=>{let s=e[a].errorPath?e[a].errorPath:r?.errorPath,l=s?n.formControl.get(s):null;if(l){let c=e[a],{errorPath:d}=c,p=cd(c,["errorPath"]);l.setErrors(Me(I({},l.errors||{}),{[a]:p})),!o._childrenErrors&&hr(o,"_childrenErrors",{}),o._childrenErrors[a]=()=>{let y=l.errors||{},{[a]:_}=y,b=cd(y,[xA(a)]);l.setErrors(Object.keys(b).length===0?null:b)}}}),e}},Eg=(()=>{let n=class n{constructor(){this.field={}}set _formlyControls(i){let r=this.field;r._localFields=i.map(o=>o.control._fields||[]).flat().filter(o=>o.formControl!==this.field.formControl)}get model(){return this.field.model}get form(){return this.field.form}get options(){return this.field.options}get key(){return this.field.key}get formControl(){return this.field.formControl}get props(){return this.field.props||{}}get to(){return this.props}get showError(){return this.options.showError(this)}get id(){return this.field.id}get formState(){return this.options?.formState||{}}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,viewQuery:function(r,o){if(r&1&&ie(Kn,5),r&2){let a;j(a=H())&&(o._formlyControls=a)}},inputs:{field:"field"},standalone:!1});let t=n;return t})(),mte=(()=>{let n=class n extends Eg{get template(){return this.field&&this.field.template!==this.innerHtml.template&&(this.innerHtml={template:this.field.template,content:this.props.safeHtml?this.sanitizer.bypassSecurityTrustHtml(this.field.template):this.field.template}),this.innerHtml.content}constructor(i){super(),this.sanitizer=i,this.innerHtml={}}};n.\u0275fac=function(r){return new(r||n)(be(kf))},n.\u0275cmp=E({type:n,selectors:[["formly-template"]],standalone:!1,features:[le],decls:1,vars:1,consts:[[3,"innerHtml"]],template:function(r,o){r&1&&M(0,"div",0),r&2&&v("innerHtml",o.template,rf)},encapsulation:2,changeDetection:0});let t=n;return t})(),Dk=(()=>{let n=class n{constructor(){this.types={},this.validators={},this.wrappers={},this.messages={},this.extras={checkExpressionOn:"modelChange",lazyRender:!0,resetFieldOnHide:!0,renderFormlyFieldElement:!0,showError(i){return i.formControl?.invalid&&(i.formControl?.touched||i.options.parentForm?.submitted||!!i.field.validation?.show)}},this.extensions={},this.presets={},this.extensionsByPriority={},this.componentRefs={}}addConfig(i){if(Array.isArray(i)){i.forEach(r=>this.addConfig(r));return}i.types&&i.types.forEach(r=>this.setType(r)),i.validators&&i.validators.forEach(r=>this.setValidator(r)),i.wrappers&&i.wrappers.forEach(r=>this.setWrapper(r)),i.validationMessages&&i.validationMessages.forEach(r=>this.addValidatorMessage(r.name,r.message)),i.extensions&&this.setSortedExtensions(i.extensions),i.extras&&(this.extras=I(I({},this.extras),i.extras)),i.presets&&(this.presets=I(I({},this.presets),i.presets.reduce((r,o)=>Me(I({},r),{[o.name]:o.config}),{})))}setType(i){Array.isArray(i)?i.forEach(r=>this.setType(r)):(this.types[i.name]||(this.types[i.name]={name:i.name}),["component","extends","defaultOptions","wrappers"].forEach(r=>{i.hasOwnProperty(r)&&(this.types[i.name][r]=i[r])}))}getType(i,r=!1){if(i instanceof Vd)return{component:i,name:i.prototype.constructor.name};if(!this.types[i]){if(r)throw new Error(`[Formly Error] The type "${i}" could not be found. Please make sure that is registered through the FormlyModule declaration.`);return null}return this.mergeExtendedType(i),this.types[i]}getMergedField(i={}){let r=this.getType(i.type);if(!r)return;r.defaultOptions&&Sh(i,r.defaultOptions);let o=r.extends&&this.getType(r.extends).defaultOptions;o&&Sh(i,o),i?.optionsTypes&&i.optionsTypes.forEach(s=>{let l=this.getType(s).defaultOptions;l&&Sh(i,l)});let a=this.resolveFieldTypeRef(i);a?.instance?.defaultOptions&&Sh(i,a.instance.defaultOptions),!i.wrappers&&r.wrappers&&(i.wrappers=[...r.wrappers])}resolveFieldTypeRef(i={}){let r=this.getType(i.type);if(!r||!r.component)return null;if(!this.componentRefs[r.name]){let{_viewContainerRef:o,_injector:a}=i.options;if(!o||!a)return null;let s=o.createComponent(r.component,{injector:a});this.componentRefs[r.name]=s;try{s.destroy()}catch(l){console.error(`An error occurred while destroying the Formly component type "${i.type}"`,l)}}return this.componentRefs[r.name]}clearRefs(){this.componentRefs={}}setWrapper(i){this.wrappers[i.name]=i,i.types&&i.types.forEach(r=>{this.setTypeWrapper(r,i.name)})}getWrapper(i){if(i instanceof Vd)return{component:i,name:i.prototype.constructor.name};if(!this.wrappers[i])throw new Error(`[Formly Error] The wrapper "${i}" could not be found. Please make sure that is registered through the FormlyModule declaration.`);return this.wrappers[i]}setTypeWrapper(i,r){this.types[i]||(this.types[i]={}),this.types[i].wrappers||(this.types[i].wrappers=[]),this.types[i].wrappers.indexOf(r)===-1&&this.types[i].wrappers.push(r)}setValidator(i){this.validators[i.name]=i}getValidator(i){if(!this.validators[i])throw new Error(`[Formly Error] The validator "${i}" could not be found. Please make sure that is registered through the FormlyModule declaration.`);return this.validators[i]}addValidatorMessage(i,r){this.messages[i]=r}getValidatorMessage(i){return this.messages[i]}setSortedExtensions(i){i.forEach(r=>{let o=r.priority??1;this.extensionsByPriority[o]=Me(I({},this.extensionsByPriority[o]),{[r.name]:r.extension})}),this.extensions=Object.keys(this.extensionsByPriority).map(Number).sort((r,o)=>r-o).reduce((r,o)=>I(I({},r),this.extensionsByPriority[o]),{})}mergeExtendedType(i){if(!this.types[i].extends)return;let r=this.getType(this.types[i].extends);this.types[i].component||(this.types[i].component=r.component),this.types[i].wrappers||(this.types[i].wrappers=r.wrappers)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var hte=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),pte=(()=>{let n=class n{get containerRef(){return this.config.extras.renderFormlyFieldElement?this.viewContainerRef:this.hostContainerRef}get elementRef(){return this.config.extras.renderFormlyFieldElement?this._elementRef:this.componentRefs?.[0]instanceof Bd?this.componentRefs[0].location:null}constructor(i,r,o,a,s){this.config=i,this.renderer=r,this._elementRef=o,this.hostContainerRef=a,this.form=s,this.hostObservers=[],this.componentRefs=[],this.hooksObservers=[],this.detectFieldBuild=!1,this.valueChangesUnsubscribe=()=>{}}ngAfterContentInit(){this.triggerHook("afterContentInit")}ngAfterViewInit(){this.triggerHook("afterViewInit")}ngDoCheck(){this.detectFieldBuild&&this.field&&this.field.options&&this.render()}ngOnInit(){this.triggerHook("onInit")}ngOnChanges(i){this.triggerHook("onChanges",i)}ngOnDestroy(){this.resetRefs(this.field),this.hostObservers.forEach(i=>i.unsubscribe()),this.hooksObservers.forEach(i=>i()),this.valueChangesUnsubscribe(),this.triggerHook("onDestroy")}renderField(i,r,o=[]){if(this.containerRef===i&&(this.resetRefs(this.field),this.containerRef.clear(),o=this.field?.wrappers),o?.length>0){let[a,...s]=o,{component:l}=this.config.getWrapper(a),c=i.createComponent(l);this.attachComponentRef(c,r),Zi(c.instance,["fieldComponent"],({currentValue:d,previousValue:p,firstChange:_})=>{if(d){if(p&&p._lContainer===d._lContainer)return;let b=p?p.detach():null;b&&!b.destroyed?d.insert(b):this.renderField(d,r,s),!_&&c.changeDetectorRef.detectChanges()}})}else if(r?.type){let a=this.form?.templates?.find(l=>l.name===r.type),s;if(a)s=i.createEmbeddedView(a.ref,{$implicit:r});else{let{component:l}=this.config.getType(r.type,!0);s=i.createComponent(l)}this.attachComponentRef(s,r)}}triggerHook(i,r){if((i==="onInit"||i==="onChanges"&&r.field&&!r.field.firstChange)&&(this.valueChangesUnsubscribe(),this.valueChangesUnsubscribe=this.fieldChanges(this.field)),this.field?.hooks?.[i]&&(!r||r.field)){let o=this.field.hooks[i](this.field);if(Gi(o)&&["onInit","afterContentInit","afterViewInit"].indexOf(i)!==-1){let a=o.subscribe();this.hooksObservers.push(()=>a.unsubscribe())}}i==="onChanges"&&r.field&&(this.resetRefs(r.field.previousValue),this.render())}attachComponentRef(i,r){this.componentRefs.push(i),r._componentRefs.push(i),i instanceof Bd&&Object.assign(i.instance,{field:r})}render(){if(this.field){if(!this.field.options){this.detectFieldBuild=!0;return}if(this.detectFieldBuild=!1,this.hostObservers.forEach(i=>i.unsubscribe()),this.hostObservers=[Zi(this.field,["hide"],({firstChange:i,currentValue:r})=>{let o=this.containerRef;this.config.extras.lazyRender===!1?(i&&this.renderField(o,this.field),(!i||i&&r)&&this.elementRef&&this.renderer.setStyle(this.elementRef.nativeElement,"display",r?"none":"")):r?(o.clear(),this.field.className&&this.renderer.removeAttribute(this.elementRef.nativeElement,"class")):(this.renderField(o,this.field),this.field.className&&this.renderer.setAttribute(this.elementRef.nativeElement,"class",this.field.className)),!i&&this.field.options.detectChanges(this.field)}),Zi(this.field,["className"],({firstChange:i,currentValue:r})=>{(!i||i&&r)&&(!this.config.extras.lazyRender||this.field.hide!==!0)&&this.elementRef&&this.renderer.setAttribute(this.elementRef.nativeElement,"class",r)})],!x3())["touched","pristine","status"].forEach(i=>this.hostObservers.push(Zi(this.field,["formControl",i],({firstChange:r})=>!r&&bk(this.field))));else if(this.field.formControl){let i=this.field.formControl.events.subscribe(()=>bk(this.field));this.hostObservers.push(i)}}}resetRefs(i){i&&(i._localFields?i._localFields=[]:hr(this.field,"_localFields",[]),i._componentRefs?i._componentRefs=i._componentRefs.filter(r=>this.componentRefs.indexOf(r)===-1):hr(this.field,"_componentRefs",[])),this.componentRefs=[]}fieldChanges(i){if(!i)return()=>{};let r=v3(i,["props"],()=>i.options.detectChanges(i)),o=[()=>{r()}];for(let s of Object.keys(i._expressions||{})){let l=Zi(i,["_expressions",s],({currentValue:c,previousValue:d})=>{d?.subscription&&(d.subscription.unsubscribe(),d.subscription=null),Gi(c.value$)&&(c.subscription=c.value$.subscribe())});o.push(()=>{i._expressions[s]?.subscription&&i._expressions[s].subscription.unsubscribe(),l.unsubscribe()})}for(let s of[["focus"],["template"],["fieldGroupClassName"],["validation","show"]]){let l=Zi(i,s,({firstChange:c})=>!c&&i.options.detectChanges(i));o.push(()=>l.unsubscribe())}if(i.formControl&&!i.fieldGroup){let s=i.formControl,l=s.valueChanges.pipe(se(_=>(i.parsers?.map(b=>_=b(_,i)),Object.is(_,i.formControl.value)||i.formControl.setValue(_),_)),Nn((_,b)=>!(_!==b||Array.isArray(_)||ts(_))));s.value!==bu(i)&&(l=l.pipe(Ue(s.value)));let{updateOn:c,debounce:d}=i.modelOptions;(!c||c==="change")&&d?.default>0&&(l=l.pipe(Dt(d.default)));let p=l.subscribe(_=>{s._fields?.length>1&&s instanceof aa&&s.patchValue(_,{emitEvent:!1,onlySelf:!0}),bn(i)&&wg(i,_),i.options.fieldChanges.next({value:_,field:i,type:"valueChanges"})});o.push(()=>p.unsubscribe())}let a=[];return Zi(i,["_localFields"],({currentValue:s})=>{a.forEach(l=>l()),a=(s||[]).map(l=>this.fieldChanges(l))}),()=>{o.forEach(s=>s()),a.forEach(s=>s())}}};n.\u0275fac=function(r){return new(r||n)(be(Dk),be(ze),be(Y),be(st),be(hte,8))},n.\u0275cmp=E({type:n,selectors:[["formly-field"]],viewQuery:function(r,o){if(r&1&&ie(Zee,7,st),r&2){let a;j(a=H())&&(o.viewContainerRef=a.first)}},inputs:{field:"field"},features:[Oe],decls:2,vars:0,consts:[["container",""]],template:function(r,o){r&1&&Ba(0,Xee,0,0,"ng-template",null,0,Mi)},styles:["[_nghost-%COMP%]:empty{display:none}"]});let t=n;return t})(),fte=(()=>{let n=class n extends pte{};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-field"]],standalone:!1,features:[le],decls:2,vars:0,consts:[["container",""]],template:function(r,o){r&1&&A(0,Jee,0,0,"ng-template",null,0,Mi)},styles:["[_nghost-%COMP%]:empty{display:none}"]});let t=n;return t})(),gte=(()=>{let n=class n extends Eg{};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-group"]],hostVars:2,hostBindings:function(r,o){r&2&&at(o.field.fieldGroupClassName||"")},standalone:!1,features:[le],ngContentSelectors:ete,decls:3,vars:0,consts:[[3,"field"]],template:function(r,o){r&1&&(Ee(),Mt(0,tte,1,1,"formly-field",0,qi),ne(2)),r&2&&Et(o.field.fieldGroup)},dependencies:[fte],encapsulation:2,changeDetection:0});let t=n;return t})(),Mg=new O("FORMLY_CONFIG");function _te(t){return{types:[{name:"formly-group",component:gte},{name:"formly-template",component:mte}],extensions:[{name:"core",extension:new yk(t),priority:-250},{name:"field-validation",extension:new Ck(t),priority:-200},{name:"field-form",extension:new xk,priority:-150},{name:"field-expression",extension:new vk,priority:-100}]}}var w3=(t=[])=>[{provide:Mg,multi:!0,useFactory:_te,deps:[Dk]},bte(t)],bte=(t=[])=>({provide:Mg,multi:!0,useFactory:()=>{let n=u(Mg,{skipSelf:!0,optional:!0});return n?(n.push(t),n):t}});var vte=(()=>{let n=class n{get props(){return this.field.props||{}}get fieldAttrElements(){return this.field?._elementRefs||[]}constructor(i,r,o){this.renderer=i,this.elementRef=r,this.uiAttributesCache={},this.uiEvents={listeners:[],events:["click","keyup","keydown","keypress","focus","blur","change","wheel"],callback:(a,s)=>{switch(a){case"focus":return this.onFocus(s);case"blur":return this.onBlur(s);case"change":return this.onChange(s);default:return this.props[a](this.field,s)}}},this.document=o}ngOnChanges(i){i.field&&(this.field.name&&this.setAttribute("name",this.field.name),this.uiEvents.listeners.forEach(r=>r()),this.uiEvents.events.forEach(r=>{(this.props?.[r]||["focus","blur","change"].indexOf(r)!==-1)&&this.uiEvents.listeners.push(this.renderer.listen(this.elementRef.nativeElement,r,o=>this.uiEvents.callback(r,o)))}),this.props?.attributes&&Zi(this.field,["props","attributes"],({currentValue:r,previousValue:o})=>{o&&Object.keys(o).forEach(a=>this.removeAttribute(a)),r&&Object.keys(r).forEach(a=>{r[a]!=null&&this.setAttribute(a,r[a])})}),this.detachElementRef(i.field.previousValue),this.attachElementRef(i.field.currentValue),this.fieldAttrElements.length===1&&(!this.id&&this.field.id&&this.setAttribute("id",this.field.id),this.focusObserver=Zi(this.field,["focus"],({currentValue:r})=>{this.toggleFocus(r)}))),i.id&&this.setAttribute("id",this.id)}ngDoCheck(){if(!this.uiAttributes){let i=this.elementRef.nativeElement;this.uiAttributes=[...wk,"tabindex","placeholder","readonly","disabled","step"].filter(r=>!i.hasAttribute||!i.hasAttribute(r))}for(let i=0;ii()),this.detachElementRef(this.field),this.focusObserver?.unsubscribe()}toggleFocus(i){let r=this.fieldAttrElements?this.fieldAttrElements[0]:null;if(!r||!r.nativeElement.focus)return;let o=!!this.document.activeElement&&this.fieldAttrElements.some(({nativeElement:a})=>this.document.activeElement===a||a.contains(this.document.activeElement));i&&!o?Promise.resolve().then(()=>r.nativeElement.focus()):!i&&o&&Promise.resolve().then(()=>r.nativeElement.blur())}onFocus(i){this.focusObserver?.setValue(!0),this.props.focus?.(this.field,i)}onBlur(i){this.focusObserver?.setValue(!1),this.props.blur?.(this.field,i)}onHostChange(i){i instanceof Event||this.onChange(i)}onChange(i){this.props.change?.(this.field,i),this.field.formControl?.markAsDirty()}attachElementRef(i){i&&(i._elementRefs?.indexOf(this.elementRef)===-1?i._elementRefs.push(this.elementRef):hr(i,"_elementRefs",[this.elementRef]))}detachElementRef(i){let r=i?._elementRefs?this.fieldAttrElements.indexOf(this.elementRef):-1;r!==-1&&i._elementRefs.splice(r,1)}setAttribute(i,r){this.renderer.setAttribute(this.elementRef.nativeElement,i,r)}removeAttribute(i){this.renderer.removeAttribute(this.elementRef.nativeElement,i)}};n.\u0275fac=function(r){return new(r||n)(be(ze),be(Y),be(_e))},n.\u0275dir=P({type:n,selectors:[["","formlyAttributes",""]],hostBindings:function(r,o){r&1&&S("change",function(s){return o.onHostChange(s)})},inputs:{field:[0,"formlyAttributes","field"],id:"id"},features:[Oe]});let t=n;return t})(),la=(()=>{let n=class n extends vte{};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275dir=P({type:n,selectors:[["","formlyAttributes",""]],hostBindings:function(r,o){r&1&&S("change",function(s){return o.onHostChange(s)})},standalone:!1,features:[le]});let t=n;return t})(),yte=(()=>{let n=class n{constructor(i){this.config=i}ngOnChanges(){let i=wk.map(r=>`templateOptions.${r}`);this.errorMessage$=it(this.field.formControl.statusChanges,this.field.options?this.field.options.fieldChanges.pipe(ce(({field:r,type:o,property:a})=>r===this.field&&o==="expressionChanges"&&(a.indexOf("validation")!==-1||i.indexOf(a)!==-1))):Q(null)).pipe(Ue(null),je(()=>Gi(this.errorMessage)?this.errorMessage:Q(this.errorMessage)))}get errorMessage(){let i=this.field.formControl;for(let r in i.errors)if(i.errors.hasOwnProperty(r)){let o=this.config.getValidatorMessage(r);if(ts(i.errors[r])){if(i.errors[r].errorPath)return;i.errors[r].message&&(o=i.errors[r].message)}return this.field.validation?.messages?.[r]&&(o=this.field.validation.messages[r]),this.field.validators?.[r]?.message&&(o=this.field.validators[r].message),this.field.asyncValidators?.[r]?.message&&(o=this.field.asyncValidators[r].message),typeof o=="function"?o(i.errors[r],this.field):o}}};n.\u0275fac=function(r){return new(r||n)(be(Dk))},n.\u0275cmp=E({type:n,selectors:[["formly-validation-message"]],inputs:{field:"field"},features:[Oe],decls:2,vars:3,template:function(r,o){r&1&&(f(0),me(1,"async")),r&2&&N(Re(1,1,o.errorMessage$))},dependencies:[cn],encapsulation:2,changeDetection:0});let t=n;return t})(),D3=(()=>{let n=class n extends yte{};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-validation-message"]],standalone:!1,features:[le],decls:2,vars:3,template:function(r,o){r&1&&(f(0),me(1,"async")),r&2&&N(Re(1,1,o.errorMessage$))},dependencies:[cn],encapsulation:2,changeDetection:0});let t=n;return t})();var Sg=(()=>{let n=class n extends Eg{set _formlyControls(i){}set _staticContent(i){this.fieldComponent=i}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275dir=P({type:n,viewQuery:function(r,o){if(r&1&&(ie(b3,5,st),ie(b3,7,st)),r&2){let a;j(a=H())&&(o.fieldComponent=a.first),j(a=H())&&(o._staticContent=a.first)}},standalone:!1,features:[le]});let t=n;return t})();var kg=t=>({field:t}),E3=t=>({content:t});function xte(t,n){t&1&&(m(0,"span",9),f(1,"*"),h())}function Cte(t,n){if(t&1&&(m(0,"mat-label"),f(1),L(2,xte,2,0,"span",9),h()),t&2){let e=x();g(),fe(" ",e.props.label," "),g(),V(e.props.required&&e.props.hideRequiredMarker!==!0?2:-1)}}function wte(t,n){if(t&1&&qe(0,3),t&2){let e=x();v("ngTemplateOutlet",e.props.textPrefix)("ngTemplateOutletContext",$t(2,kg,e.field))}}function Dte(t,n){if(t&1&&qe(0,4),t&2){let e=x();v("ngTemplateOutlet",e.props.prefix)("ngTemplateOutletContext",$t(2,kg,e.field))}}function Mte(t,n){if(t&1&&qe(0,5),t&2){let e=x();v("ngTemplateOutlet",e.props.textSuffix)("ngTemplateOutletContext",$t(2,kg,e.field))}}function Ete(t,n){if(t&1&&qe(0,6),t&2){let e=x();v("ngTemplateOutlet",e.props.suffix)("ngTemplateOutletContext",$t(2,kg,e.field))}}function Ste(t,n){if(t&1&&(m(0,"mat-hint"),qe(1,10),h()),t&2){x();let e=Te(13);g(),v("ngTemplateOutlet",e)("ngTemplateOutletContext",$t(2,E3,n))}}function kte(t,n){if(t&1&&(m(0,"mat-hint",8),qe(1,10),h()),t&2){x();let e=Te(13);g(),v("ngTemplateOutlet",e)("ngTemplateOutletContext",$t(2,E3,n))}}function Tte(t,n){if(t&1&&(lt(0),f(1),ot()),t&2){let e=x().content;g(),N(e)}}function Ite(t,n){if(t&1&&qe(0,10),t&2){let e=x().content,i=x();v("ngTemplateOutlet",e)("ngTemplateOutletContext",$t(2,kg,i.field))}}function Ate(t,n){if(t&1&&L(0,Tte,2,1,"ng-container")(1,Ite,1,4,"ng-container",10),t&2){let e=n.content;V(e.createEmbeddedView?1:0)}}var Ote=["matPrefix"],Rte=["matTextPrefix"],Pte=["matSuffix"],Fte=["matTextSuffix"],Nte=(()=>{let n=class n extends Sg{constructor(i,r,o){super(),this.renderer=i,this.elementRef=r,this.focusMonitor=o}ngOnInit(){hr(this.field,"_formField",this.formField),this.focusMonitor.monitor(this.elementRef,!0).subscribe(i=>{!i&&this.field.focus&&(this.field.focus=!1)})}ngAfterViewInit(){if(this.formField.appearance!=="outline"&&this.props.hideFieldUnderline===!0){let i=this.formField._elementRef.nativeElement.querySelector(".mat-form-field-underline");i&&this.renderer.removeChild(i.parentNode,i)}}ngOnDestroy(){delete this.field._formField,this.focusMonitor.stopMonitoring(this.elementRef)}};n.\u0275fac=function(r){return new(r||n)(be(ze),be(Y),be(oi))},n.\u0275cmp=E({type:n,selectors:[["formly-wrapper-mat-form-field"]],viewQuery:function(r,o){if(r&1&&ie(Xt,7),r&2){let a;j(a=H())&&(o.formField=a.first)}},standalone:!1,features:[le],decls:14,vars:13,consts:[["fieldComponent",""],["stringOrTemplate",""],[3,"hideRequiredMarker","floatLabel","appearance","subscriptSizing","color"],["matTextPrefix","",3,"ngTemplateOutlet","ngTemplateOutletContext"],["matPrefix","",3,"ngTemplateOutlet","ngTemplateOutletContext"],["matTextSuffix","",3,"ngTemplateOutlet","ngTemplateOutletContext"],["matSuffix","",3,"ngTemplateOutlet","ngTemplateOutletContext"],[3,"field"],["align","end"],["aria-hidden","true",1,"mat-form-field-required-marker","mat-mdc-form-field-required-marker"],[3,"ngTemplateOutlet","ngTemplateOutletContext"]],template:function(r,o){if(r&1&&(m(0,"mat-form-field",2),qe(1,null,0),L(3,Cte,3,2,"mat-label"),L(4,wte,1,4,"ng-container",3),L(5,Dte,1,4,"ng-container",4),L(6,Mte,1,4,"ng-container",5),L(7,Ete,1,4,"ng-container",6),m(8,"mat-error"),M(9,"formly-validation-message",7),h(),L(10,Ste,2,4,"mat-hint"),L(11,kte,2,4,"mat-hint",8),h(),A(12,Ate,2,1,"ng-template",null,1,Mi)),r&2){let a,s;v("hideRequiredMarker",!0)("floatLabel",o.props.floatLabel)("appearance",o.props.appearance)("subscriptSizing",o.props.subscriptSizing)("color",o.props.color??"primary"),g(3),V(o.props.label&&o.props.hideLabel!==!0?3:-1),g(),V(o.props.textPrefix?4:-1),g(),V(o.props.prefix?5:-1),g(),V(o.props.textSuffix?6:-1),g(),V(o.props.suffix?7:-1),g(2),v("field",o.field),g(),V((a=o.props.description||o.props.hintStart)?10:-1,a),g(),V((s=o.props.hintEnd)?11:-1,s)}},dependencies:[$n,Xt,gi,ag,Ao,uu,Ka,D3],styles:[`formly-wrapper-mat-form-field .mat-mdc-form-field,formly-wrapper-mat-form-field .mat-form-field{width:100%} +`],encapsulation:2});let t=n;return t})();function S3(){return{wrappers:[{name:"form-field",component:Nte}]}}var uo=(()=>{let n=class n extends Eg{constructor(){super(...arguments),this.errorStateMatcher={isErrorState:()=>this.field&&this.showError},this.stateChanges=new z,this._errorState=!1,this._focused=!1}set matPrefix(i){i&&(this.props.prefix=i)}set matTextPrefix(i){i&&(this.props.textPrefix=i)}set matSuffix(i){i&&(this.props.suffix=i)}set matTextSuffix(i){i&&(this.props.textSuffix=i)}set _controls(i){this.attachControl(i.length===1?i.first:this)}ngOnDestroy(){delete this.formField?._control,this.stateChanges.complete()}setDescribedByIds(i){}onContainerClick(i){this.field.focus=!0,this.stateChanges.next()}get errorState(){let i=this.options.showError(this);return i!==this._errorState&&(this._errorState=i,this.stateChanges.next()),i}get controlType(){if(this.props.type)return this.props.type;let i=this.field.type;return i instanceof Vd?i.prototype.constructor.name:i}get focused(){let i=!!this.field.focus&&!this.disabled;return i!==this._focused&&(this._focused=i,this.stateChanges.next()),i}get disabled(){return!!this.props.disabled}get required(){return!!this.props.required}get placeholder(){return this.props.placeholder||""}get shouldPlaceholderFloat(){return this.shouldLabelFloat}get value(){return this.formControl?.value}set value(i){this.formControl?.patchValue(i)}get ngControl(){return this.formControl}get empty(){return this.value==null||this.value===""}get shouldLabelFloat(){return this.focused||!this.empty}get formField(){return this.field?._formField}attachControl(i){if(this.formField&&i!==this.formField._control){this.formField._control=i;let r=i?.ngControl;r?.valueAccessor?.hasOwnProperty("_formField")&&(r.valueAccessor._formField=this.formField),r?.valueAccessor?.hasOwnProperty("_parentFormField")&&(r.valueAccessor._parentFormField=this.formField),["prefix","suffix","textPrefix","textSuffix"].forEach(a=>Zi(this.field,["props",a],({currentValue:s})=>s&&Promise.resolve().then(()=>{this.options.detectChanges(this.field)})));let o=i.setDescribedByIds.bind(i);i.setDescribedByIds=a=>{setTimeout(()=>o(a))}}}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275dir=P({type:n,viewQuery:function(r,o){if(r&1&&(ie(Ote,5),ie(Rte,5),ie(Pte,5),ie(Fte,5),ie(Za,5)),r&2){let a;j(a=H())&&(o.matPrefix=a.first),j(a=H())&&(o.matTextPrefix=a.first),j(a=H())&&(o.matSuffix=a.first),j(a=H())&&(o.matTextSuffix=a.first),j(a=H())&&(o._controls=a)}},standalone:!1,features:[le]});let t=n;return t})();var k3=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["ng-component"]],hostAttrs:["cdk-text-field-style-loader",""],decls:0,vars:0,template:function(i,r){},styles:[`textarea.cdk-textarea-autosize{resize:none}textarea.cdk-textarea-autosize-measuring{padding:2px 0 !important;box-sizing:content-box !important;height:auto !important;overflow:hidden !important}textarea.cdk-textarea-autosize-measuring-firefox{padding:2px 0 !important;box-sizing:content-box !important;height:0 !important}@keyframes cdk-text-field-autofill-start{/*!*/}@keyframes cdk-text-field-autofill-end{/*!*/}.cdk-text-field-autofill-monitored:-webkit-autofill{animation:cdk-text-field-autofill-start 0s 1ms}.cdk-text-field-autofill-monitored:not(:-webkit-autofill){animation:cdk-text-field-autofill-end 0s 1ms} +`],encapsulation:2,changeDetection:0})}return t})(),Lte={passive:!0},T3=(()=>{class t{_platform=u(Ye);_ngZone=u(ae);_renderer=u(hn).createRenderer(null,null);_styleLoader=u(ft);_monitoredElements=new Map;constructor(){}monitor(e){if(!this._platform.isBrowser)return zi;this._styleLoader.load(k3);let i=Wr(e),r=this._monitoredElements.get(i);if(r)return r.subject;let o=new z,a="cdk-text-field-autofilled",s=c=>{c.animationName==="cdk-text-field-autofill-start"&&!i.classList.contains(a)?(i.classList.add(a),this._ngZone.run(()=>o.next({target:c.target,isAutofilled:!0}))):c.animationName==="cdk-text-field-autofill-end"&&i.classList.contains(a)&&(i.classList.remove(a),this._ngZone.run(()=>o.next({target:c.target,isAutofilled:!1})))},l=this._ngZone.runOutsideAngular(()=>(i.classList.add("cdk-text-field-autofill-monitored"),this._renderer.listen(i,"animationstart",s,Lte)));return this._monitoredElements.set(i,{subject:o,unlisten:l}),o}stopMonitoring(e){let i=Wr(e),r=this._monitoredElements.get(i);r&&(r.unlisten(),r.subject.complete(),i.classList.remove("cdk-text-field-autofill-monitored"),i.classList.remove("cdk-text-field-autofilled"),this._monitoredElements.delete(i))}ngOnDestroy(){this._monitoredElements.forEach((e,i)=>this.stopMonitoring(i))}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var I3=(()=>{class t{_elementRef=u(Y);_platform=u(Ye);_ngZone=u(ae);_renderer=u(ze);_resizeEvents=new z;_previousValue;_initialHeight;_destroyed=new z;_listenerCleanups;_minRows;_maxRows;_enabled=!0;_previousMinRows=-1;_textareaElement;get minRows(){return this._minRows}set minRows(e){this._minRows=Gn(e),this._setMinHeight()}get maxRows(){return this._maxRows}set maxRows(e){this._maxRows=Gn(e),this._setMaxHeight()}get enabled(){return this._enabled}set enabled(e){this._enabled!==e&&((this._enabled=e)?this.resizeToFitContent(!0):this.reset())}get placeholder(){return this._textareaElement.placeholder}set placeholder(e){this._cachedPlaceholderHeight=void 0,e?this._textareaElement.setAttribute("placeholder",e):this._textareaElement.removeAttribute("placeholder"),this._cacheTextareaPlaceholderHeight()}_cachedLineHeight;_cachedPlaceholderHeight;_document=u(_e);_hasFocus;_isViewInited=!1;constructor(){u(ft).load(k3),this._textareaElement=this._elementRef.nativeElement}_setMinHeight(){let e=this.minRows&&this._cachedLineHeight?`${this.minRows*this._cachedLineHeight}px`:null;e&&(this._textareaElement.style.minHeight=e)}_setMaxHeight(){let e=this.maxRows&&this._cachedLineHeight?`${this.maxRows*this._cachedLineHeight}px`:null;e&&(this._textareaElement.style.maxHeight=e)}ngAfterViewInit(){this._platform.isBrowser&&(this._initialHeight=this._textareaElement.style.height,this.resizeToFitContent(),this._ngZone.runOutsideAngular(()=>{this._listenerCleanups=[this._renderer.listen("window","resize",()=>this._resizeEvents.next()),this._renderer.listen(this._textareaElement,"focus",this._handleFocusEvent),this._renderer.listen(this._textareaElement,"blur",this._handleFocusEvent)],this._resizeEvents.pipe(Xl(16)).subscribe(()=>{this._cachedLineHeight=this._cachedPlaceholderHeight=void 0,this.resizeToFitContent(!0)})}),this._isViewInited=!0,this.resizeToFitContent(!0))}ngOnDestroy(){this._listenerCleanups?.forEach(e=>e()),this._resizeEvents.complete(),this._destroyed.next(),this._destroyed.complete()}_cacheTextareaLineHeight(){if(this._cachedLineHeight)return;let e=this._textareaElement.cloneNode(!1),i=e.style;e.rows=1,i.position="absolute",i.visibility="hidden",i.border="none",i.padding="0",i.height="",i.minHeight="",i.maxHeight="",i.top=i.bottom=i.left=i.right="auto",i.overflow="hidden",this._textareaElement.parentNode.appendChild(e),this._cachedLineHeight=e.clientHeight,e.remove(),this._setMinHeight(),this._setMaxHeight()}_measureScrollHeight(){let e=this._textareaElement,i=e.style.marginBottom||"",r=this._platform.FIREFOX,o=r&&this._hasFocus,a=r?"cdk-textarea-autosize-measuring-firefox":"cdk-textarea-autosize-measuring";o&&(e.style.marginBottom=`${e.clientHeight}px`),e.classList.add(a);let s=e.scrollHeight-4;return e.classList.remove(a),o&&(e.style.marginBottom=i),s}_cacheTextareaPlaceholderHeight(){if(!this._isViewInited||this._cachedPlaceholderHeight!=null)return;if(!this.placeholder){this._cachedPlaceholderHeight=0;return}let e=this._textareaElement.value;this._textareaElement.value=this._textareaElement.placeholder,this._cachedPlaceholderHeight=this._measureScrollHeight(),this._textareaElement.value=e}_handleFocusEvent=e=>{this._hasFocus=e.type==="focus"};ngDoCheck(){this._platform.isBrowser&&this.resizeToFitContent()}resizeToFitContent(e=!1){if(!this._enabled||(this._cacheTextareaLineHeight(),this._cacheTextareaPlaceholderHeight(),!this._cachedLineHeight))return;let i=this._elementRef.nativeElement,r=i.value;if(!e&&this._minRows===this._previousMinRows&&r===this._previousValue)return;let o=this._measureScrollHeight(),a=Math.max(o,this._cachedPlaceholderHeight||0);i.style.height=`${a}px`,this._ngZone.runOutsideAngular(()=>{typeof requestAnimationFrame<"u"?requestAnimationFrame(()=>this._scrollToCaretPosition(i)):setTimeout(()=>this._scrollToCaretPosition(i))}),this._previousValue=r,this._previousMinRows=this._minRows}reset(){this._initialHeight!==void 0&&(this._textareaElement.style.height=this._initialHeight)}_noopInputHandler(){}_scrollToCaretPosition(e){let{selectionStart:i,selectionEnd:r}=e;!this._destroyed.isStopped&&this._hasFocus&&e.setSelectionRange(i,r)}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["textarea","cdkTextareaAutosize",""]],hostAttrs:["rows","1",1,"cdk-textarea-autosize"],hostBindings:function(i,r){i&1&&S("input",function(){return r._noopInputHandler()})},inputs:{minRows:[0,"cdkAutosizeMinRows","minRows"],maxRows:[0,"cdkAutosizeMaxRows","maxRows"],enabled:[2,"cdkTextareaAutosize","enabled",B],placeholder:"placeholder"},exportAs:["cdkTextareaAutosize"]})}return t})(),A3=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})();var kh=new O("MAT_INPUT_VALUE_ACCESSOR");var Bte=["button","checkbox","file","hidden","image","radio","range","reset","submit"],jte=new O("MAT_INPUT_CONFIG"),Ci=(()=>{class t{_elementRef=u(Y);_platform=u(Ye);ngControl=u(Kn,{optional:!0,self:!0});_autofillMonitor=u(T3);_ngZone=u(ae);_formField=u(oa,{optional:!0});_renderer=u(ze);_uid=u(et).getId("mat-input-");_previousNativeValue;_inputValueAccessor;_signalBasedValueAccessor;_previousPlaceholder;_errorStateTracker;_config=u(jte,{optional:!0});_cleanupIosKeyup;_cleanupWebkitWheel;_isServer;_isNativeSelect;_isTextarea;_isInFormField;focused=!1;stateChanges=new z;controlType="mat-input";autofilled=!1;get disabled(){return this._disabled}set disabled(e){this._disabled=Vi(e),this.focused&&(this.focused=!1,this.stateChanges.next())}_disabled=!1;get id(){return this._id}set id(e){this._id=e||this._uid}_id;placeholder;name;get required(){return this._required??this.ngControl?.control?.hasValidator(Ve.required)??!1}set required(e){this._required=Vi(e)}_required;get type(){return this._type}set type(e){this._type=e||"text",this._validateType(),!this._isTextarea&&OS().has(this._type)&&(this._elementRef.nativeElement.type=this._type)}_type="text";get errorStateMatcher(){return this._errorStateTracker.matcher}set errorStateMatcher(e){this._errorStateTracker.matcher=e}userAriaDescribedBy;get value(){return this._signalBasedValueAccessor?this._signalBasedValueAccessor.value():this._inputValueAccessor.value}set value(e){e!==this.value&&(this._signalBasedValueAccessor?this._signalBasedValueAccessor.value.set(e):this._inputValueAccessor.value=e,this.stateChanges.next())}get readonly(){return this._readonly}set readonly(e){this._readonly=Vi(e)}_readonly=!1;disabledInteractive;get errorState(){return this._errorStateTracker.errorState}set errorState(e){this._errorStateTracker.errorState=e}_neverEmptyInputTypes=["date","datetime","datetime-local","month","time","week"].filter(e=>OS().has(e));constructor(){let e=u(Oc,{optional:!0}),i=u(nn,{optional:!0}),r=u(kl),o=u(kh,{optional:!0,self:!0}),a=this._elementRef.nativeElement,s=a.nodeName.toLowerCase();o?vs(o.value)?this._signalBasedValueAccessor=o:this._inputValueAccessor=o:this._inputValueAccessor=a,this._previousNativeValue=this.value,this.id=this.id,this._platform.IOS&&this._ngZone.runOutsideAngular(()=>{this._cleanupIosKeyup=this._renderer.listen(a,"keyup",this._iOSKeyupListener)}),this._errorStateTracker=new Sl(r,this.ngControl,i,e,this.stateChanges),this._isServer=!this._platform.isBrowser,this._isNativeSelect=s==="select",this._isTextarea=s==="textarea",this._isInFormField=!!this._formField,this.disabledInteractive=this._config?.disabledInteractive||!1,this._isNativeSelect&&(this.controlType=a.multiple?"mat-native-select-multiple":"mat-native-select"),this._signalBasedValueAccessor&&zr(()=>{this._signalBasedValueAccessor.value(),this.stateChanges.next()})}ngAfterViewInit(){this._platform.isBrowser&&this._autofillMonitor.monitor(this._elementRef.nativeElement).subscribe(e=>{this.autofilled=e.isAutofilled,this.stateChanges.next()})}ngOnChanges(){this.stateChanges.next()}ngOnDestroy(){this.stateChanges.complete(),this._platform.isBrowser&&this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement),this._cleanupIosKeyup?.(),this._cleanupWebkitWheel?.()}ngDoCheck(){this.ngControl&&(this.updateErrorState(),this.ngControl.disabled!==null&&this.ngControl.disabled!==this.disabled&&(this.disabled=this.ngControl.disabled,this.stateChanges.next())),this._dirtyCheckNativeValue(),this._dirtyCheckPlaceholder()}focus(e){this._elementRef.nativeElement.focus(e)}updateErrorState(){this._errorStateTracker.updateErrorState()}_focusChanged(e){if(e!==this.focused){if(!this._isNativeSelect&&e&&this.disabled&&this.disabledInteractive){let i=this._elementRef.nativeElement;i.type==="number"?(i.type="text",i.setSelectionRange(0,0),i.type="number"):i.setSelectionRange(0,0)}this.focused=e,this.stateChanges.next()}}_onInput(){}_dirtyCheckNativeValue(){let e=this._elementRef.nativeElement.value;this._previousNativeValue!==e&&(this._previousNativeValue=e,this.stateChanges.next())}_dirtyCheckPlaceholder(){let e=this._getPlaceholder();if(e!==this._previousPlaceholder){let i=this._elementRef.nativeElement;this._previousPlaceholder=e,e?i.setAttribute("placeholder",e):i.removeAttribute("placeholder")}}_getPlaceholder(){return this.placeholder||null}_validateType(){Bte.indexOf(this._type)>-1}_isNeverEmpty(){return this._neverEmptyInputTypes.indexOf(this._type)>-1}_isBadInput(){let e=this._elementRef.nativeElement.validity;return e&&e.badInput}get empty(){return!this._isNeverEmpty()&&!this._elementRef.nativeElement.value&&!this._isBadInput()&&!this.autofilled}get shouldLabelFloat(){if(this._isNativeSelect){let e=this._elementRef.nativeElement,i=e.options[0];return this.focused||e.multiple||!this.empty||!!(e.selectedIndex>-1&&i&&i.label)}else return this.focused&&!this.disabled||!this.empty}get describedByIds(){return this._elementRef.nativeElement.getAttribute("aria-describedby")?.split(" ")||[]}setDescribedByIds(e){let i=this._elementRef.nativeElement;e.length?i.setAttribute("aria-describedby",e.join(" ")):i.removeAttribute("aria-describedby")}onContainerClick(){this.focused||this.focus()}_isInlineSelect(){let e=this._elementRef.nativeElement;return this._isNativeSelect&&(e.multiple||e.size>1)}_iOSKeyupListener=e=>{let i=e.target;!i.value&&i.selectionStart===0&&i.selectionEnd===0&&(i.setSelectionRange(1,1),i.setSelectionRange(0,0))};_getReadonlyAttribute(){return this._isNativeSelect?null:this.readonly||this.disabled&&this.disabledInteractive?"true":null}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["input","matInput",""],["textarea","matInput",""],["select","matNativeControl",""],["input","matNativeControl",""],["textarea","matNativeControl",""]],hostAttrs:[1,"mat-mdc-input-element"],hostVars:21,hostBindings:function(i,r){i&1&&S("focus",function(){return r._focusChanged(!0)})("blur",function(){return r._focusChanged(!1)})("input",function(){return r._onInput()}),i&2&&(pi("id",r.id)("disabled",r.disabled&&!r.disabledInteractive)("required",r.required),X("name",r.name||null)("readonly",r._getReadonlyAttribute())("aria-disabled",r.disabled&&r.disabledInteractive?"true":null)("aria-invalid",r.empty&&r.required?null:r.errorState)("aria-required",r.required)("id",r.id),G("mat-input-server",r._isServer)("mat-mdc-form-field-textarea-control",r._isInFormField&&r._isTextarea)("mat-mdc-form-field-input-control",r._isInFormField)("mat-mdc-input-disabled-interactive",r.disabledInteractive)("mdc-text-field__input",r._isInFormField)("mat-mdc-native-select-inline",r._isInlineSelect()))},inputs:{disabled:"disabled",id:"id",placeholder:"placeholder",name:"name",required:"required",type:"type",errorStateMatcher:"errorStateMatcher",userAriaDescribedBy:[0,"aria-describedby","userAriaDescribedBy"],value:"value",readonly:"readonly",disabledInteractive:[2,"disabledInteractive","disabledInteractive",B]},exportAs:["matInput"],features:[we([{provide:Za,useExisting:t}]),Oe]})}return t})(),Bi=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,ai,ai,A3,De]})}return t})();function Hte(t,n){if(t&1&&M(0,"input",0),t&2){let e=x();v("id",e.id)("name",e.field.name)("type",e.type||"text")("readonly",e.props.readonly)("required",e.required)("errorStateMatcher",e.errorStateMatcher)("formControl",e.formControl)("formlyAttributes",e.field)("tabIndex",e.props.tabindex)("placeholder",e.props.placeholder)}}function zte(t,n){if(t&1&&M(0,"input",1),t&2){let e=x();v("id",e.id)("name",e.field.name)("readonly",e.props.readonly)("required",e.required)("errorStateMatcher",e.errorStateMatcher)("formControl",e.formControl)("formlyAttributes",e.field)("tabIndex",e.props.tabindex)("placeholder",e.props.placeholder)}}var Ute=(()=>{let n=class n extends uo{get type(){return this.props.type||"text"}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-field-mat-input"]],standalone:!1,features:[le],decls:2,vars:1,consts:[["matInput","",3,"id","name","type","readonly","required","errorStateMatcher","formControl","formlyAttributes","tabIndex","placeholder"],["matInput","","type","number",3,"id","name","readonly","required","errorStateMatcher","formControl","formlyAttributes","tabIndex","placeholder"]],template:function(r,o){r&1&&L(0,Hte,1,10,"input",0)(1,zte,1,9,"input",1),r&2&&V(o.type!=="number"?0:1)},dependencies:[di,gu,Pt,Fo,Po,Ci,la],encapsulation:2,changeDetection:0});let t=n;return t})();function O3(){return{types:[{name:"input",component:Ute,wrappers:["form-field"]},{name:"string",extends:"input"},{name:"number",extends:"input",defaultOptions:{props:{type:"number"}}},{name:"integer",extends:"input",defaultOptions:{props:{type:"number"}}}]}}var $te=(()=>{let n=class n extends uo{constructor(){super(...arguments),this.defaultOptions={props:{cols:1,rows:1}}}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-field-mat-textarea"]],standalone:!1,features:[we([{provide:kh,useExisting:n}]),le],decls:1,vars:16,consts:[["matInput","",3,"id","name","readonly","required","formControl","errorStateMatcher","cols","rows","formlyAttributes","placeholder","tabindex","cdkTextareaAutosize","cdkAutosizeMinRows","cdkAutosizeMaxRows"]],template:function(r,o){r&1&&M(0,"textarea",0),r&2&&(G("cdk-textarea-autosize",o.props.autosize),v("id",o.id)("name",o.field.name)("readonly",o.props.readonly)("required",o.required)("formControl",o.formControl)("errorStateMatcher",o.errorStateMatcher)("cols",o.props.cols)("rows",o.props.rows)("formlyAttributes",o.field)("placeholder",o.props.placeholder)("tabindex",o.props.tabindex)("cdkTextareaAutosize",o.props.autosize)("cdkAutosizeMinRows",o.props.autosizeMinRows)("cdkAutosizeMaxRows",o.props.autosizeMaxRows))},dependencies:[di,Pt,Fo,Po,Ci,I3,la],encapsulation:2,changeDetection:0});let t=n;return t})();function R3(){return{types:[{name:"textarea",component:$te,wrappers:["form-field"]}]}}var Wte=["input"],Gte=["formField"],qte=["*"],Sx=class{source;value;constructor(n,e){this.source=n,this.value=e}},Yte={provide:dr,useExisting:li(()=>Tg),multi:!0},P3=new O("MatRadioGroup"),Qte=new O("mat-radio-default-options",{providedIn:"root",factory:Kte});function Kte(){return{color:"accent",disabledInteractive:!1}}var Tg=(()=>{class t{_changeDetector=u(ye);_value=null;_name=u(et).getId("mat-radio-group-");_selected=null;_isInitialized=!1;_labelPosition="after";_disabled=!1;_required=!1;_buttonChanges;_controlValueAccessorChangeFn=()=>{};onTouched=()=>{};change=new U;_radios;color;get name(){return this._name}set name(e){this._name=e,this._updateRadioButtonNames()}get labelPosition(){return this._labelPosition}set labelPosition(e){this._labelPosition=e==="before"?"before":"after",this._markRadiosForCheck()}get value(){return this._value}set value(e){this._value!==e&&(this._value=e,this._updateSelectedRadioFromValue(),this._checkSelectedRadioButton())}_checkSelectedRadioButton(){this._selected&&!this._selected.checked&&(this._selected.checked=!0)}get selected(){return this._selected}set selected(e){this._selected=e,this.value=e?e.value:null,this._checkSelectedRadioButton()}get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._markRadiosForCheck()}get required(){return this._required}set required(e){this._required=e,this._markRadiosForCheck()}get disabledInteractive(){return this._disabledInteractive}set disabledInteractive(e){this._disabledInteractive=e,this._markRadiosForCheck()}_disabledInteractive=!1;constructor(){}ngAfterContentInit(){this._isInitialized=!0,this._buttonChanges=this._radios.changes.subscribe(()=>{this.selected&&!this._radios.find(e=>e===this.selected)&&(this._selected=null)})}ngOnDestroy(){this._buttonChanges?.unsubscribe()}_touch(){this.onTouched&&this.onTouched()}_updateRadioButtonNames(){this._radios&&this._radios.forEach(e=>{e.name=this.name,e._markForCheck()})}_updateSelectedRadioFromValue(){let e=this._selected!==null&&this._selected.value===this._value;this._radios&&!e&&(this._selected=null,this._radios.forEach(i=>{i.checked=this.value===i.value,i.checked&&(this._selected=i)}))}_emitChangeEvent(){this._isInitialized&&this.change.emit(new Sx(this._selected,this._value))}_markRadiosForCheck(){this._radios&&this._radios.forEach(e=>e._markForCheck())}writeValue(e){this.value=e,this._changeDetector.markForCheck()}registerOnChange(e){this._controlValueAccessorChangeFn=e}registerOnTouched(e){this.onTouched=e}setDisabledState(e){this.disabled=e,this._changeDetector.markForCheck()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-radio-group"]],contentQueries:function(i,r,o){if(i&1&&Ce(o,Mk,5),i&2){let a;j(a=H())&&(r._radios=a)}},hostAttrs:["role","radiogroup",1,"mat-mdc-radio-group"],inputs:{color:"color",name:"name",labelPosition:"labelPosition",value:"value",selected:"selected",disabled:[2,"disabled","disabled",B],required:[2,"required","required",B],disabledInteractive:[2,"disabledInteractive","disabledInteractive",B]},outputs:{change:"change"},exportAs:["matRadioGroup"],features:[we([Yte,{provide:P3,useExisting:t}])]})}return t})(),Mk=(()=>{class t{_elementRef=u(Y);_changeDetector=u(ye);_focusMonitor=u(oi);_radioDispatcher=u(hu);_defaultOptions=u(Qte,{optional:!0});_ngZone=u(ae);_renderer=u(ze);_uniqueId=u(et).getId("mat-radio-");_cleanupClick;id=this._uniqueId;name;ariaLabel;ariaLabelledby;ariaDescribedby;disableRipple=!1;tabIndex=0;get checked(){return this._checked}set checked(e){this._checked!==e&&(this._checked=e,e&&this.radioGroup&&this.radioGroup.value!==this.value?this.radioGroup.selected=this:!e&&this.radioGroup&&this.radioGroup.value===this.value&&(this.radioGroup.selected=null),e&&this._radioDispatcher.notify(this.id,this.name),this._changeDetector.markForCheck())}get value(){return this._value}set value(e){this._value!==e&&(this._value=e,this.radioGroup!==null&&(this.checked||(this.checked=this.radioGroup.value===e),this.checked&&(this.radioGroup.selected=this)))}get labelPosition(){return this._labelPosition||this.radioGroup&&this.radioGroup.labelPosition||"after"}set labelPosition(e){this._labelPosition=e}_labelPosition;get disabled(){return this._disabled||this.radioGroup!==null&&this.radioGroup.disabled}set disabled(e){this._setDisabled(e)}get required(){return this._required||this.radioGroup&&this.radioGroup.required}set required(e){e!==this._required&&this._changeDetector.markForCheck(),this._required=e}get color(){return this._color||this.radioGroup&&this.radioGroup.color||this._defaultOptions&&this._defaultOptions.color||"accent"}set color(e){this._color=e}_color;get disabledInteractive(){return this._disabledInteractive||this.radioGroup!==null&&this.radioGroup.disabledInteractive}set disabledInteractive(e){this._disabledInteractive=e}_disabledInteractive;change=new U;radioGroup;get inputId(){return`${this.id||this._uniqueId}-input`}_checked=!1;_disabled;_required;_value=null;_removeUniqueSelectionListener=()=>{};_previousTabIndex;_inputElement;_rippleTrigger;_noopAnimations=Qe();_injector=u(de);constructor(){u(ft).load(Oi);let e=u(P3,{optional:!0}),i=u(new Li("tabindex"),{optional:!0});this.radioGroup=e,this._disabledInteractive=this._defaultOptions?.disabledInteractive??!1,i&&(this.tabIndex=ht(i,0))}focus(e,i){i?this._focusMonitor.focusVia(this._inputElement,i,e):this._inputElement.nativeElement.focus(e)}_markForCheck(){this._changeDetector.markForCheck()}ngOnInit(){this.radioGroup&&(this.checked=this.radioGroup.value===this._value,this.checked&&(this.radioGroup.selected=this),this.name=this.radioGroup.name),this._removeUniqueSelectionListener=this._radioDispatcher.listen((e,i)=>{e!==this.id&&i===this.name&&(this.checked=!1)})}ngDoCheck(){this._updateTabIndex()}ngAfterViewInit(){this._updateTabIndex(),this._focusMonitor.monitor(this._elementRef,!0).subscribe(e=>{!e&&this.radioGroup&&this.radioGroup._touch()}),this._ngZone.runOutsideAngular(()=>{this._cleanupClick=this._renderer.listen(this._inputElement.nativeElement,"click",this._onInputClick)})}ngOnDestroy(){this._cleanupClick?.(),this._focusMonitor.stopMonitoring(this._elementRef),this._removeUniqueSelectionListener()}_emitChangeEvent(){this.change.emit(new Sx(this,this._value))}_isRippleDisabled(){return this.disableRipple||this.disabled}_onInputInteraction(e){if(e.stopPropagation(),!this.checked&&!this.disabled){let i=this.radioGroup&&this.value!==this.radioGroup.value;this.checked=!0,this._emitChangeEvent(),this.radioGroup&&(this.radioGroup._controlValueAccessorChangeFn(this.value),i&&this.radioGroup._emitChangeEvent())}}_onTouchTargetClick(e){this._onInputInteraction(e),(!this.disabled||this.disabledInteractive)&&this._inputElement?.nativeElement.focus()}_setDisabled(e){this._disabled!==e&&(this._disabled=e,this._changeDetector.markForCheck())}_onInputClick=e=>{this.disabled&&this.disabledInteractive&&e.preventDefault()};_updateTabIndex(){let e=this.radioGroup,i;if(!e||!e.selected||this.disabled?i=this.tabIndex:i=e.selected===this?this.tabIndex:-1,i!==this._previousTabIndex){let r=this._inputElement?.nativeElement;r&&(r.setAttribute("tabindex",i+""),this._previousTabIndex=i,vt(()=>{queueMicrotask(()=>{e&&e.selected&&e.selected!==this&&document.activeElement===r&&(e.selected?._inputElement.nativeElement.focus(),document.activeElement===r&&this._inputElement.nativeElement.blur())})},{injector:this._injector}))}}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-radio-button"]],viewQuery:function(i,r){if(i&1&&(ie(Wte,5),ie(Gte,7,Y)),i&2){let o;j(o=H())&&(r._inputElement=o.first),j(o=H())&&(r._rippleTrigger=o.first)}},hostAttrs:[1,"mat-mdc-radio-button"],hostVars:19,hostBindings:function(i,r){i&1&&S("focus",function(){return r._inputElement.nativeElement.focus()}),i&2&&(X("id",r.id)("tabindex",null)("aria-label",null)("aria-labelledby",null)("aria-describedby",null),G("mat-primary",r.color==="primary")("mat-accent",r.color==="accent")("mat-warn",r.color==="warn")("mat-mdc-radio-checked",r.checked)("mat-mdc-radio-disabled",r.disabled)("mat-mdc-radio-disabled-interactive",r.disabledInteractive)("_mat-animation-noopable",r._noopAnimations))},inputs:{id:"id",name:"name",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],ariaDescribedby:[0,"aria-describedby","ariaDescribedby"],disableRipple:[2,"disableRipple","disableRipple",B],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:ht(e)],checked:[2,"checked","checked",B],value:"value",labelPosition:"labelPosition",disabled:[2,"disabled","disabled",B],required:[2,"required","required",B],color:"color",disabledInteractive:[2,"disabledInteractive","disabledInteractive",B]},outputs:{change:"change"},exportAs:["matRadioButton"],ngContentSelectors:qte,decls:13,vars:17,consts:[["formField",""],["input",""],["mat-internal-form-field","",3,"labelPosition"],[1,"mdc-radio"],[1,"mat-mdc-radio-touch-target",3,"click"],["type","radio","aria-invalid","false",1,"mdc-radio__native-control",3,"change","id","checked","disabled","required"],[1,"mdc-radio__background"],[1,"mdc-radio__outer-circle"],[1,"mdc-radio__inner-circle"],["mat-ripple","",1,"mat-radio-ripple","mat-focus-indicator",3,"matRippleTrigger","matRippleDisabled","matRippleCentered"],[1,"mat-ripple-element","mat-radio-persistent-ripple"],[1,"mdc-label",3,"for"]],template:function(i,r){if(i&1){let o=q();Ee(),m(0,"div",2,0)(2,"div",3)(3,"div",4),S("click",function(s){return k(o),T(r._onTouchTargetClick(s))}),h(),m(4,"input",5,1),S("change",function(s){return k(o),T(r._onInputInteraction(s))}),h(),m(6,"div",6),M(7,"div",7)(8,"div",8),h(),m(9,"div",9),M(10,"div",10),h()(),m(11,"label",11),ne(12),h()()}i&2&&(v("labelPosition",r.labelPosition),g(2),G("mdc-radio--disabled",r.disabled),g(2),v("id",r.inputId)("checked",r.checked)("disabled",r.disabled&&!r.disabledInteractive)("required",r.required),X("name",r.name)("value",r.value)("aria-label",r.ariaLabel)("aria-labelledby",r.ariaLabelledby)("aria-describedby",r.ariaDescribedby)("aria-disabled",r.disabled&&r.disabledInteractive?"true":null),g(5),v("matRippleTrigger",r._rippleTrigger.nativeElement)("matRippleDisabled",r._isRippleDisabled())("matRippleCentered",!0),g(2),v("for",r.inputId))},dependencies:[qn,Zm],styles:[`.mat-mdc-radio-button{-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-radio-button .mdc-radio{display:inline-block;position:relative;flex:0 0 auto;box-sizing:content-box;width:20px;height:20px;cursor:pointer;will-change:opacity,transform,border-color,color;padding:calc((var(--mat-radio-state-layer-size, 40px) - 20px)/2)}.mat-mdc-radio-button .mdc-radio:hover>.mdc-radio__native-control:not([disabled]):not(:focus)~.mdc-radio__background::before{opacity:.04;transform:scale(1)}.mat-mdc-radio-button .mdc-radio:hover>.mdc-radio__native-control:not([disabled])~.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-unselected-hover-icon-color, var(--mat-sys-on-surface))}.mat-mdc-radio-button .mdc-radio:hover>.mdc-radio__native-control:enabled:checked+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-selected-hover-icon-color, var(--mat-sys-primary))}.mat-mdc-radio-button .mdc-radio:hover>.mdc-radio__native-control:enabled:checked+.mdc-radio__background>.mdc-radio__inner-circle{background-color:var(--mat-radio-selected-hover-icon-color, var(--mat-sys-primary, currentColor))}.mat-mdc-radio-button .mdc-radio:active>.mdc-radio__native-control:enabled:not(:checked)+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-unselected-pressed-icon-color, var(--mat-sys-on-surface))}.mat-mdc-radio-button .mdc-radio:active>.mdc-radio__native-control:enabled:checked+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-selected-pressed-icon-color, var(--mat-sys-primary))}.mat-mdc-radio-button .mdc-radio:active>.mdc-radio__native-control:enabled:checked+.mdc-radio__background>.mdc-radio__inner-circle{background-color:var(--mat-radio-selected-pressed-icon-color, var(--mat-sys-primary, currentColor))}.mat-mdc-radio-button .mdc-radio__background{display:inline-block;position:relative;box-sizing:border-box;width:20px;height:20px}.mat-mdc-radio-button .mdc-radio__background::before{position:absolute;transform:scale(0, 0);border-radius:50%;opacity:0;pointer-events:none;content:"";transition:opacity 90ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms cubic-bezier(0.4, 0, 0.6, 1);width:var(--mat-radio-state-layer-size, 40px);height:var(--mat-radio-state-layer-size, 40px);top:calc(-1*(var(--mat-radio-state-layer-size, 40px) - 20px)/2);left:calc(-1*(var(--mat-radio-state-layer-size, 40px) - 20px)/2)}.mat-mdc-radio-button .mdc-radio__outer-circle{position:absolute;top:0;left:0;box-sizing:border-box;width:100%;height:100%;border-width:2px;border-style:solid;border-radius:50%;transition:border-color 90ms cubic-bezier(0.4, 0, 0.6, 1)}.mat-mdc-radio-button .mdc-radio__inner-circle{position:absolute;top:0;left:0;box-sizing:border-box;width:100%;height:100%;transform:scale(0);border-radius:50%;transition:transform 90ms cubic-bezier(0.4, 0, 0.6, 1),background-color 90ms cubic-bezier(0.4, 0, 0.6, 1)}@media(forced-colors: active){.mat-mdc-radio-button .mdc-radio__inner-circle{background-color:CanvasText !important}}.mat-mdc-radio-button .mdc-radio__native-control{position:absolute;margin:0;padding:0;opacity:0;top:0;right:0;left:0;cursor:inherit;z-index:1;width:var(--mat-radio-state-layer-size, 40px);height:var(--mat-radio-state-layer-size, 40px)}.mat-mdc-radio-button .mdc-radio__native-control:checked+.mdc-radio__background,.mat-mdc-radio-button .mdc-radio__native-control:disabled+.mdc-radio__background{transition:opacity 90ms cubic-bezier(0, 0, 0.2, 1),transform 90ms cubic-bezier(0, 0, 0.2, 1)}.mat-mdc-radio-button .mdc-radio__native-control:checked+.mdc-radio__background>.mdc-radio__outer-circle,.mat-mdc-radio-button .mdc-radio__native-control:disabled+.mdc-radio__background>.mdc-radio__outer-circle{transition:border-color 90ms cubic-bezier(0, 0, 0.2, 1)}.mat-mdc-radio-button .mdc-radio__native-control:checked+.mdc-radio__background>.mdc-radio__inner-circle,.mat-mdc-radio-button .mdc-radio__native-control:disabled+.mdc-radio__background>.mdc-radio__inner-circle{transition:transform 90ms cubic-bezier(0, 0, 0.2, 1),background-color 90ms cubic-bezier(0, 0, 0.2, 1)}.mat-mdc-radio-button .mdc-radio__native-control:focus+.mdc-radio__background::before{transform:scale(1);opacity:.12;transition:opacity 90ms cubic-bezier(0, 0, 0.2, 1),transform 90ms cubic-bezier(0, 0, 0.2, 1)}.mat-mdc-radio-button .mdc-radio__native-control:disabled:not(:checked)+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-disabled-unselected-icon-color, var(--mat-sys-on-surface));opacity:var(--mat-radio-disabled-unselected-icon-opacity, 0.38)}.mat-mdc-radio-button .mdc-radio__native-control:disabled+.mdc-radio__background{cursor:default}.mat-mdc-radio-button .mdc-radio__native-control:disabled+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-disabled-selected-icon-color, var(--mat-sys-on-surface));opacity:var(--mat-radio-disabled-selected-icon-opacity, 0.38)}.mat-mdc-radio-button .mdc-radio__native-control:disabled+.mdc-radio__background>.mdc-radio__inner-circle{background-color:var(--mat-radio-disabled-selected-icon-color, var(--mat-sys-on-surface, currentColor));opacity:var(--mat-radio-disabled-selected-icon-opacity, 0.38)}.mat-mdc-radio-button .mdc-radio__native-control:enabled:not(:checked)+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-unselected-icon-color, var(--mat-sys-on-surface-variant))}.mat-mdc-radio-button .mdc-radio__native-control:enabled:checked+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-selected-icon-color, var(--mat-sys-primary))}.mat-mdc-radio-button .mdc-radio__native-control:enabled:checked+.mdc-radio__background>.mdc-radio__inner-circle{background-color:var(--mat-radio-selected-icon-color, var(--mat-sys-primary, currentColor))}.mat-mdc-radio-button .mdc-radio__native-control:enabled:focus:checked+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-selected-focus-icon-color, var(--mat-sys-primary))}.mat-mdc-radio-button .mdc-radio__native-control:enabled:focus:checked+.mdc-radio__background>.mdc-radio__inner-circle{background-color:var(--mat-radio-selected-focus-icon-color, var(--mat-sys-primary, currentColor))}.mat-mdc-radio-button .mdc-radio__native-control:checked+.mdc-radio__background>.mdc-radio__inner-circle{transform:scale(0.5);transition:transform 90ms cubic-bezier(0, 0, 0.2, 1),background-color 90ms cubic-bezier(0, 0, 0.2, 1)}.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled{pointer-events:auto}.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled .mdc-radio__native-control:not(:checked)+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-disabled-unselected-icon-color, var(--mat-sys-on-surface));opacity:var(--mat-radio-disabled-unselected-icon-opacity, 0.38)}.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled:hover .mdc-radio__native-control:checked+.mdc-radio__background>.mdc-radio__outer-circle,.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled .mdc-radio__native-control:checked:focus+.mdc-radio__background>.mdc-radio__outer-circle,.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled .mdc-radio__native-control+.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-disabled-selected-icon-color, var(--mat-sys-on-surface));opacity:var(--mat-radio-disabled-selected-icon-opacity, 0.38)}.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled:hover .mdc-radio__native-control:checked+.mdc-radio__background>.mdc-radio__inner-circle,.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled .mdc-radio__native-control:checked:focus+.mdc-radio__background>.mdc-radio__inner-circle,.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled .mdc-radio__native-control+.mdc-radio__background>.mdc-radio__inner-circle{background-color:var(--mat-radio-disabled-selected-icon-color, var(--mat-sys-on-surface, currentColor));opacity:var(--mat-radio-disabled-selected-icon-opacity, 0.38)}.mat-mdc-radio-button._mat-animation-noopable .mdc-radio__background::before,.mat-mdc-radio-button._mat-animation-noopable .mdc-radio__outer-circle,.mat-mdc-radio-button._mat-animation-noopable .mdc-radio__inner-circle{transition:none !important}.mat-mdc-radio-button label{cursor:pointer}.mat-mdc-radio-button .mdc-radio__background::before{background-color:var(--mat-radio-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-radio-button.mat-mdc-radio-checked .mat-ripple-element,.mat-mdc-radio-button.mat-mdc-radio-checked .mdc-radio__background::before{background-color:var(--mat-radio-checked-ripple-color, var(--mat-sys-primary))}.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled .mat-ripple-element,.mat-mdc-radio-button.mat-mdc-radio-disabled-interactive .mdc-radio--disabled .mdc-radio__background::before{background-color:var(--mat-radio-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-radio-button .mat-internal-form-field{color:var(--mat-radio-label-text-color, var(--mat-sys-on-surface));font-family:var(--mat-radio-label-text-font, var(--mat-sys-body-medium-font));line-height:var(--mat-radio-label-text-line-height, var(--mat-sys-body-medium-line-height));font-size:var(--mat-radio-label-text-size, var(--mat-sys-body-medium-size));letter-spacing:var(--mat-radio-label-text-tracking, var(--mat-sys-body-medium-tracking));font-weight:var(--mat-radio-label-text-weight, var(--mat-sys-body-medium-weight))}.mat-mdc-radio-button .mdc-radio--disabled+label{color:var(--mat-radio-disabled-label-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-mdc-radio-button .mat-radio-ripple{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none;border-radius:50%}.mat-mdc-radio-button .mat-radio-ripple>.mat-ripple-element{opacity:.14}.mat-mdc-radio-button .mat-radio-ripple::before{border-radius:50%}.mat-mdc-radio-button .mdc-radio>.mdc-radio__native-control:focus:enabled:not(:checked)~.mdc-radio__background>.mdc-radio__outer-circle{border-color:var(--mat-radio-unselected-focus-icon-color, var(--mat-sys-on-surface))}.mat-mdc-radio-button.cdk-focused .mat-focus-indicator::before{content:""}.mat-mdc-radio-disabled{cursor:default;pointer-events:none}.mat-mdc-radio-disabled.mat-mdc-radio-disabled-interactive{pointer-events:auto}.mat-mdc-radio-touch-target{position:absolute;top:50%;left:50%;height:var(--mat-radio-touch-target-size, 48px);width:var(--mat-radio-touch-target-size, 48px);transform:translate(-50%, -50%);display:var(--mat-radio-touch-target-display, block)}[dir=rtl] .mat-mdc-radio-touch-target{left:auto;right:50%;transform:translate(50%, -50%)} +`],encapsulation:2,changeDetection:0})}return t})();var Xte=(()=>{let n=class n{transform(i,r){return i instanceof Ne?this.dispose():i=this.observableOf(i,r),i.pipe(se(o=>this.transformOptions(o,r)))}ngOnDestroy(){this.dispose()}transformOptions(i,r){let o=this.transformSelectProps(r),a=[],s={};return i?.forEach(l=>{let c=this.transformOption(l,o);if(c.group){let d=s[c.label];d===void 0?s[c.label]=a.push(c)-1:c.group.forEach(p=>a[d].group.push(p))}else a.push(c)}),a}transformOption(i,r){let o=r.groupProp(i);return Array.isArray(o)?{label:r.labelProp(i),group:o.map(a=>this.transformOption(a,r))}:(i={label:r.labelProp(i),value:r.valueProp(i),disabled:!!r.disabledProp(i)},o?{label:o,group:[i]}:i)}transformSelectProps(i){let r=i?.props||i?.templateOptions||{},o=a=>typeof a=="function"?a:s=>s[a];return{groupProp:o(r.groupProp||"group"),labelProp:o(r.labelProp||"label"),valueProp:o(r.valueProp||"value"),disabledProp:o(r.disabledProp||"disabled")}}dispose(){this._options&&(this._options.complete(),this._options=null),this._subscription&&(this._subscription.unsubscribe(),this._subscription=null)}observableOf(i,r){return this.dispose(),r&&r.options&&r.options.fieldChanges&&(this._subscription=r.options.fieldChanges.pipe(ce(({property:o,type:a,field:s})=>a==="expressionChanges"&&(o.indexOf("templateOptions.options")===0||o.indexOf("props.options")===0)&&s===r&&Array.isArray(s.props.options)&&!!this._options),He(()=>this._options.next(r.props.options))).subscribe()),this._options=new rt(i),this._options.asObservable()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275pipe=io({name:"formlySelectOptions",type:n,pure:!0});let t=n;return t})(),Ih=(()=>{let n=class n extends Xte{};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275pipe=io({name:"formlySelectOptions",type:n,pure:!0,standalone:!1});let t=n;return t})();function Jte(t,n){if(t&1&&(m(0,"mat-radio-button",1),f(1),h()),t&2){let e=n.$implicit,i=n.$index,r=x();v("id",r.id+"_"+i)("color",r.props.color)("labelPosition",r.props.labelPosition)("disabled",e.disabled)("value",e.value),g(),fe(" ",e.label," ")}}var eie=(()=>{let n=class n extends uo{constructor(){super(...arguments),this.defaultOptions={props:{hideFieldUnderline:!0,floatLabel:"always",tabindex:-1}}}ngAfterViewInit(){this.focusObserver=Zi(this.field,["focus"],({currentValue:i})=>{this.props.tabindex===-1&&i&&this.radioGroup._radios.length>0&&setTimeout(()=>{(this.radioGroup.selected?this.radioGroup.selected:this.radioGroup._radios.first).focus()})})}onContainerClick(){}ngOnDestroy(){super.ngOnDestroy(),this.focusObserver&&this.focusObserver.unsubscribe()}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-field-mat-radio"]],viewQuery:function(r,o){if(r&1&&ie(Tg,7),r&2){let a;j(a=H())&&(o.radioGroup=a.first)}},standalone:!1,features:[le],decls:5,vars:9,consts:[[3,"formControl","formlyAttributes","required","tabindex"],[3,"id","color","labelPosition","disabled","value"]],template:function(r,o){r&1&&(m(0,"mat-radio-group",0),Mt(1,Jte,2,6,"mat-radio-button",1,qi),me(3,"formlySelectOptions"),me(4,"async"),h()),r&2&&(v("formControl",o.formControl)("formlyAttributes",o.field)("required",o.required)("tabindex",o.props.tabindex),g(),Et(Re(4,7,Ui(3,4,o.props.options,o.field))))},dependencies:[Pt,Fo,Po,Tg,Mk,la,cn,Ih],encapsulation:2,changeDetection:0});let t=n;return t})();function F3(){return{types:[{name:"radio",component:eie,wrappers:["form-field"]}]}}var tie=["input"],iie=["label"],nie=["*"],rie=new O("mat-checkbox-default-options",{providedIn:"root",factory:L3});function L3(){return{color:"accent",clickAction:"check-indeterminate",disabledInteractive:!1}}var Ar=(function(t){return t[t.Init=0]="Init",t[t.Checked=1]="Checked",t[t.Unchecked=2]="Unchecked",t[t.Indeterminate=3]="Indeterminate",t})(Ar||{}),Sk=class{source;checked},N3=L3(),is=(()=>{class t{_elementRef=u(Y);_changeDetectorRef=u(ye);_ngZone=u(ae);_animationsDisabled=Qe();_options=u(rie,{optional:!0});focus(){this._inputElement.nativeElement.focus()}_createChangeEvent(e){let i=new Sk;return i.source=this,i.checked=e,i}_getAnimationTargetElement(){return this._inputElement?.nativeElement}_animationClasses={uncheckedToChecked:"mdc-checkbox--anim-unchecked-checked",uncheckedToIndeterminate:"mdc-checkbox--anim-unchecked-indeterminate",checkedToUnchecked:"mdc-checkbox--anim-checked-unchecked",checkedToIndeterminate:"mdc-checkbox--anim-checked-indeterminate",indeterminateToChecked:"mdc-checkbox--anim-indeterminate-checked",indeterminateToUnchecked:"mdc-checkbox--anim-indeterminate-unchecked"};ariaLabel="";ariaLabelledby=null;ariaDescribedby;ariaExpanded;ariaControls;ariaOwns;_uniqueId;id;get inputId(){return`${this.id||this._uniqueId}-input`}required;labelPosition="after";name=null;change=new U;indeterminateChange=new U;value;disableRipple;_inputElement;_labelElement;tabIndex;color;disabledInteractive;_onTouched=()=>{};_currentAnimationClass="";_currentCheckState=Ar.Init;_controlValueAccessorChangeFn=()=>{};_validatorChangeFn=()=>{};constructor(){u(ft).load(Oi);let e=u(new Li("tabindex"),{optional:!0});this._options=this._options||N3,this.color=this._options.color||N3.color,this.tabIndex=e==null?0:parseInt(e)||0,this.id=this._uniqueId=u(et).getId("mat-mdc-checkbox-"),this.disabledInteractive=this._options?.disabledInteractive??!1}ngOnChanges(e){e.required&&this._validatorChangeFn()}ngAfterViewInit(){this._syncIndeterminate(this.indeterminate)}get checked(){return this._checked}set checked(e){e!=this.checked&&(this._checked=e,this._changeDetectorRef.markForCheck())}_checked=!1;get disabled(){return this._disabled}set disabled(e){e!==this.disabled&&(this._disabled=e,this._changeDetectorRef.markForCheck())}_disabled=!1;get indeterminate(){return this._indeterminate()}set indeterminate(e){let i=e!=this._indeterminate();this._indeterminate.set(e),i&&(e?this._transitionCheckState(Ar.Indeterminate):this._transitionCheckState(this.checked?Ar.Checked:Ar.Unchecked),this.indeterminateChange.emit(e)),this._syncIndeterminate(e)}_indeterminate=he(!1);_isRippleDisabled(){return this.disableRipple||this.disabled}_onLabelTextChange(){this._changeDetectorRef.detectChanges()}writeValue(e){this.checked=!!e}registerOnChange(e){this._controlValueAccessorChangeFn=e}registerOnTouched(e){this._onTouched=e}setDisabledState(e){this.disabled=e}validate(e){return this.required&&e.value!==!0?{required:!0}:null}registerOnValidatorChange(e){this._validatorChangeFn=e}_transitionCheckState(e){let i=this._currentCheckState,r=this._getAnimationTargetElement();if(!(i===e||!r)&&(this._currentAnimationClass&&r.classList.remove(this._currentAnimationClass),this._currentAnimationClass=this._getAnimationClassForCheckStateTransition(i,e),this._currentCheckState=e,this._currentAnimationClass.length>0)){r.classList.add(this._currentAnimationClass);let o=this._currentAnimationClass;this._ngZone.runOutsideAngular(()=>{setTimeout(()=>{r.classList.remove(o)},1e3)})}}_emitChangeEvent(){this._controlValueAccessorChangeFn(this.checked),this.change.emit(this._createChangeEvent(this.checked)),this._inputElement&&(this._inputElement.nativeElement.checked=this.checked)}toggle(){this.checked=!this.checked,this._controlValueAccessorChangeFn(this.checked)}_handleInputClick(){let e=this._options?.clickAction;!this.disabled&&e!=="noop"?(this.indeterminate&&e!=="check"&&Promise.resolve().then(()=>{this._indeterminate.set(!1),this.indeterminateChange.emit(!1)}),this._checked=!this._checked,this._transitionCheckState(this._checked?Ar.Checked:Ar.Unchecked),this._emitChangeEvent()):(this.disabled&&this.disabledInteractive||!this.disabled&&e==="noop")&&(this._inputElement.nativeElement.checked=this.checked,this._inputElement.nativeElement.indeterminate=this.indeterminate)}_onInteractionEvent(e){e.stopPropagation()}_onBlur(){Promise.resolve().then(()=>{this._onTouched(),this._changeDetectorRef.markForCheck()})}_getAnimationClassForCheckStateTransition(e,i){if(this._animationsDisabled)return"";switch(e){case Ar.Init:if(i===Ar.Checked)return this._animationClasses.uncheckedToChecked;if(i==Ar.Indeterminate)return this._checked?this._animationClasses.checkedToIndeterminate:this._animationClasses.uncheckedToIndeterminate;break;case Ar.Unchecked:return i===Ar.Checked?this._animationClasses.uncheckedToChecked:this._animationClasses.uncheckedToIndeterminate;case Ar.Checked:return i===Ar.Unchecked?this._animationClasses.checkedToUnchecked:this._animationClasses.checkedToIndeterminate;case Ar.Indeterminate:return i===Ar.Checked?this._animationClasses.indeterminateToChecked:this._animationClasses.indeterminateToUnchecked}return""}_syncIndeterminate(e){let i=this._inputElement;i&&(i.nativeElement.indeterminate=e)}_onInputClick(){this._handleInputClick()}_onTouchTargetClick(){this._handleInputClick(),this.disabled||this._inputElement.nativeElement.focus()}_preventBubblingFromLabel(e){e.target&&this._labelElement.nativeElement.contains(e.target)&&e.stopPropagation()}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-checkbox"]],viewQuery:function(i,r){if(i&1&&(ie(tie,5),ie(iie,5)),i&2){let o;j(o=H())&&(r._inputElement=o.first),j(o=H())&&(r._labelElement=o.first)}},hostAttrs:[1,"mat-mdc-checkbox"],hostVars:16,hostBindings:function(i,r){i&2&&(pi("id",r.id),X("tabindex",null)("aria-label",null)("aria-labelledby",null),at(r.color?"mat-"+r.color:"mat-accent"),G("_mat-animation-noopable",r._animationsDisabled)("mdc-checkbox--disabled",r.disabled)("mat-mdc-checkbox-disabled",r.disabled)("mat-mdc-checkbox-checked",r.checked)("mat-mdc-checkbox-disabled-interactive",r.disabledInteractive))},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],ariaDescribedby:[0,"aria-describedby","ariaDescribedby"],ariaExpanded:[2,"aria-expanded","ariaExpanded",B],ariaControls:[0,"aria-controls","ariaControls"],ariaOwns:[0,"aria-owns","ariaOwns"],id:"id",required:[2,"required","required",B],labelPosition:"labelPosition",name:"name",value:"value",disableRipple:[2,"disableRipple","disableRipple",B],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?void 0:ht(e)],color:"color",disabledInteractive:[2,"disabledInteractive","disabledInteractive",B],checked:[2,"checked","checked",B],disabled:[2,"disabled","disabled",B],indeterminate:[2,"indeterminate","indeterminate",B]},outputs:{change:"change",indeterminateChange:"indeterminateChange"},exportAs:["matCheckbox"],features:[we([{provide:dr,useExisting:li(()=>t),multi:!0},{provide:sa,useExisting:t,multi:!0}]),Oe],ngContentSelectors:nie,decls:15,vars:23,consts:[["checkbox",""],["input",""],["label",""],["mat-internal-form-field","",3,"click","labelPosition"],[1,"mdc-checkbox"],[1,"mat-mdc-checkbox-touch-target",3,"click"],["type","checkbox",1,"mdc-checkbox__native-control",3,"blur","click","change","checked","indeterminate","disabled","id","required","tabIndex"],[1,"mdc-checkbox__ripple"],[1,"mdc-checkbox__background"],["focusable","false","viewBox","0 0 24 24","aria-hidden","true",1,"mdc-checkbox__checkmark"],["fill","none","d","M1.73,12.91 8.1,19.28 22.79,4.59",1,"mdc-checkbox__checkmark-path"],[1,"mdc-checkbox__mixedmark"],["mat-ripple","",1,"mat-mdc-checkbox-ripple","mat-focus-indicator",3,"matRippleTrigger","matRippleDisabled","matRippleCentered"],[1,"mdc-label",3,"for"]],template:function(i,r){if(i&1){let o=q();Ee(),m(0,"div",3),S("click",function(s){return k(o),T(r._preventBubblingFromLabel(s))}),m(1,"div",4,0)(3,"div",5),S("click",function(){return k(o),T(r._onTouchTargetClick())}),h(),m(4,"input",6,1),S("blur",function(){return k(o),T(r._onBlur())})("click",function(){return k(o),T(r._onInputClick())})("change",function(s){return k(o),T(r._onInteractionEvent(s))}),h(),M(6,"div",7),m(7,"div",8),ii(),m(8,"svg",9),M(9,"path",10),h(),Qo(),M(10,"div",11),h(),M(11,"div",12),h(),m(12,"label",13,2),ne(14),h()()}if(i&2){let o=Te(2);v("labelPosition",r.labelPosition),g(4),G("mdc-checkbox--selected",r.checked),v("checked",r.checked)("indeterminate",r.indeterminate)("disabled",r.disabled&&!r.disabledInteractive)("id",r.inputId)("required",r.required)("tabIndex",r.disabled&&!r.disabledInteractive?-1:r.tabIndex),X("aria-label",r.ariaLabel||null)("aria-labelledby",r.ariaLabelledby)("aria-describedby",r.ariaDescribedby)("aria-checked",r.indeterminate?"mixed":null)("aria-controls",r.ariaControls)("aria-disabled",r.disabled&&r.disabledInteractive?!0:null)("aria-expanded",r.ariaExpanded)("aria-owns",r.ariaOwns)("name",r.name)("value",r.value),g(7),v("matRippleTrigger",o)("matRippleDisabled",r.disableRipple||r.disabled)("matRippleCentered",!0),g(),v("for",r.inputId)}},dependencies:[qn,Zm],styles:[`.mdc-checkbox{display:inline-block;position:relative;flex:0 0 18px;box-sizing:content-box;width:18px;height:18px;line-height:0;white-space:nowrap;cursor:pointer;vertical-align:bottom;padding:calc((var(--mat-checkbox-state-layer-size, 40px) - 18px)/2);margin:calc((var(--mat-checkbox-state-layer-size, 40px) - var(--mat-checkbox-state-layer-size, 40px))/2)}.mdc-checkbox:hover>.mdc-checkbox__ripple{opacity:var(--mat-checkbox-unselected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity));background-color:var(--mat-checkbox-unselected-hover-state-layer-color, var(--mat-sys-on-surface))}.mdc-checkbox:hover>.mat-mdc-checkbox-ripple>.mat-ripple-element{background-color:var(--mat-checkbox-unselected-hover-state-layer-color, var(--mat-sys-on-surface))}.mdc-checkbox .mdc-checkbox__native-control:focus+.mdc-checkbox__ripple{opacity:var(--mat-checkbox-unselected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity));background-color:var(--mat-checkbox-unselected-focus-state-layer-color, var(--mat-sys-on-surface))}.mdc-checkbox .mdc-checkbox__native-control:focus~.mat-mdc-checkbox-ripple .mat-ripple-element{background-color:var(--mat-checkbox-unselected-focus-state-layer-color, var(--mat-sys-on-surface))}.mdc-checkbox:active>.mdc-checkbox__native-control+.mdc-checkbox__ripple{opacity:var(--mat-checkbox-unselected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity));background-color:var(--mat-checkbox-unselected-pressed-state-layer-color, var(--mat-sys-primary))}.mdc-checkbox:active>.mdc-checkbox__native-control~.mat-mdc-checkbox-ripple .mat-ripple-element{background-color:var(--mat-checkbox-unselected-pressed-state-layer-color, var(--mat-sys-primary))}.mdc-checkbox:hover .mdc-checkbox__native-control:checked+.mdc-checkbox__ripple{opacity:var(--mat-checkbox-selected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity));background-color:var(--mat-checkbox-selected-hover-state-layer-color, var(--mat-sys-primary))}.mdc-checkbox:hover .mdc-checkbox__native-control:checked~.mat-mdc-checkbox-ripple .mat-ripple-element{background-color:var(--mat-checkbox-selected-hover-state-layer-color, var(--mat-sys-primary))}.mdc-checkbox .mdc-checkbox__native-control:focus:checked+.mdc-checkbox__ripple{opacity:var(--mat-checkbox-selected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity));background-color:var(--mat-checkbox-selected-focus-state-layer-color, var(--mat-sys-primary))}.mdc-checkbox .mdc-checkbox__native-control:focus:checked~.mat-mdc-checkbox-ripple .mat-ripple-element{background-color:var(--mat-checkbox-selected-focus-state-layer-color, var(--mat-sys-primary))}.mdc-checkbox:active>.mdc-checkbox__native-control:checked+.mdc-checkbox__ripple{opacity:var(--mat-checkbox-selected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity));background-color:var(--mat-checkbox-selected-pressed-state-layer-color, var(--mat-sys-on-surface))}.mdc-checkbox:active>.mdc-checkbox__native-control:checked~.mat-mdc-checkbox-ripple .mat-ripple-element{background-color:var(--mat-checkbox-selected-pressed-state-layer-color, var(--mat-sys-on-surface))}.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox .mdc-checkbox__native-control~.mat-mdc-checkbox-ripple .mat-ripple-element,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox .mdc-checkbox__native-control+.mdc-checkbox__ripple{background-color:var(--mat-checkbox-unselected-hover-state-layer-color, var(--mat-sys-on-surface))}.mdc-checkbox .mdc-checkbox__native-control{position:absolute;margin:0;padding:0;opacity:0;cursor:inherit;z-index:1;width:var(--mat-checkbox-state-layer-size, 40px);height:var(--mat-checkbox-state-layer-size, 40px);top:calc((var(--mat-checkbox-state-layer-size, 40px) - var(--mat-checkbox-state-layer-size, 40px))/2);right:calc((var(--mat-checkbox-state-layer-size, 40px) - var(--mat-checkbox-state-layer-size, 40px))/2);left:calc((var(--mat-checkbox-state-layer-size, 40px) - var(--mat-checkbox-state-layer-size, 40px))/2)}.mdc-checkbox--disabled{cursor:default;pointer-events:none}.mdc-checkbox__background{display:inline-flex;position:absolute;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:2px solid currentColor;border-radius:2px;background-color:rgba(0,0,0,0);pointer-events:none;will-change:background-color,border-color;transition:background-color 90ms cubic-bezier(0.4, 0, 0.6, 1),border-color 90ms cubic-bezier(0.4, 0, 0.6, 1);-webkit-print-color-adjust:exact;color-adjust:exact;border-color:var(--mat-checkbox-unselected-icon-color, var(--mat-sys-on-surface-variant));top:calc((var(--mat-checkbox-state-layer-size, 40px) - 18px)/2);left:calc((var(--mat-checkbox-state-layer-size, 40px) - 18px)/2)}.mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background,.mdc-checkbox__native-control:enabled:indeterminate~.mdc-checkbox__background{border-color:var(--mat-checkbox-selected-icon-color, var(--mat-sys-primary));background-color:var(--mat-checkbox-selected-icon-color, var(--mat-sys-primary))}.mdc-checkbox--disabled .mdc-checkbox__background{border-color:var(--mat-checkbox-disabled-unselected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}@media(forced-colors: active){.mdc-checkbox--disabled .mdc-checkbox__background{border-color:GrayText}}.mdc-checkbox__native-control:disabled:checked~.mdc-checkbox__background,.mdc-checkbox__native-control:disabled:indeterminate~.mdc-checkbox__background{background-color:var(--mat-checkbox-disabled-selected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));border-color:rgba(0,0,0,0)}@media(forced-colors: active){.mdc-checkbox__native-control:disabled:checked~.mdc-checkbox__background,.mdc-checkbox__native-control:disabled:indeterminate~.mdc-checkbox__background{border-color:GrayText}}.mdc-checkbox:hover>.mdc-checkbox__native-control:not(:checked)~.mdc-checkbox__background,.mdc-checkbox:hover>.mdc-checkbox__native-control:not(:indeterminate)~.mdc-checkbox__background{border-color:var(--mat-checkbox-unselected-hover-icon-color, var(--mat-sys-on-surface));background-color:rgba(0,0,0,0)}.mdc-checkbox:hover>.mdc-checkbox__native-control:checked~.mdc-checkbox__background,.mdc-checkbox:hover>.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background{border-color:var(--mat-checkbox-selected-hover-icon-color, var(--mat-sys-primary));background-color:var(--mat-checkbox-selected-hover-icon-color, var(--mat-sys-primary))}.mdc-checkbox__native-control:focus:focus:not(:checked)~.mdc-checkbox__background,.mdc-checkbox__native-control:focus:focus:not(:indeterminate)~.mdc-checkbox__background{border-color:var(--mat-checkbox-unselected-focus-icon-color, var(--mat-sys-on-surface))}.mdc-checkbox__native-control:focus:focus:checked~.mdc-checkbox__background,.mdc-checkbox__native-control:focus:focus:indeterminate~.mdc-checkbox__background{border-color:var(--mat-checkbox-selected-focus-icon-color, var(--mat-sys-primary));background-color:var(--mat-checkbox-selected-focus-icon-color, var(--mat-sys-primary))}.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox:hover>.mdc-checkbox__native-control~.mdc-checkbox__background,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox .mdc-checkbox__native-control:focus~.mdc-checkbox__background,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__background{border-color:var(--mat-checkbox-disabled-unselected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}@media(forced-colors: active){.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox:hover>.mdc-checkbox__native-control~.mdc-checkbox__background,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox .mdc-checkbox__native-control:focus~.mdc-checkbox__background,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__background{border-color:GrayText}}.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__native-control:checked~.mdc-checkbox__background,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background{background-color:var(--mat-checkbox-disabled-selected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent));border-color:rgba(0,0,0,0)}.mdc-checkbox__checkmark{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;opacity:0;transition:opacity 180ms cubic-bezier(0.4, 0, 0.6, 1);color:var(--mat-checkbox-selected-checkmark-color, var(--mat-sys-on-primary))}@media(forced-colors: active){.mdc-checkbox__checkmark{color:CanvasText}}.mdc-checkbox--disabled .mdc-checkbox__checkmark,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__checkmark{color:var(--mat-checkbox-disabled-selected-checkmark-color, var(--mat-sys-surface))}@media(forced-colors: active){.mdc-checkbox--disabled .mdc-checkbox__checkmark,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__checkmark{color:GrayText}}.mdc-checkbox__checkmark-path{transition:stroke-dashoffset 180ms cubic-bezier(0.4, 0, 0.6, 1);stroke:currentColor;stroke-width:3.12px;stroke-dashoffset:29.7833385;stroke-dasharray:29.7833385}.mdc-checkbox__mixedmark{width:100%;height:0;transform:scaleX(0) rotate(0deg);border-width:1px;border-style:solid;opacity:0;transition:opacity 90ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms cubic-bezier(0.4, 0, 0.6, 1);border-color:var(--mat-checkbox-selected-checkmark-color, var(--mat-sys-on-primary))}@media(forced-colors: active){.mdc-checkbox__mixedmark{margin:0 1px}}.mdc-checkbox--disabled .mdc-checkbox__mixedmark,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__mixedmark{border-color:var(--mat-checkbox-disabled-selected-checkmark-color, var(--mat-sys-surface))}@media(forced-colors: active){.mdc-checkbox--disabled .mdc-checkbox__mixedmark,.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__mixedmark{border-color:GrayText}}.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__background,.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__background,.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__background,.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__background{animation-duration:180ms;animation-timing-function:linear}.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__checkmark-path{animation:mdc-checkbox-unchecked-checked-checkmark-path 180ms linear;transition:none}.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__mixedmark{animation:mdc-checkbox-unchecked-indeterminate-mixedmark 90ms linear;transition:none}.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__checkmark-path{animation:mdc-checkbox-checked-unchecked-checkmark-path 90ms linear;transition:none}.mdc-checkbox--anim-checked-indeterminate .mdc-checkbox__checkmark{animation:mdc-checkbox-checked-indeterminate-checkmark 90ms linear;transition:none}.mdc-checkbox--anim-checked-indeterminate .mdc-checkbox__mixedmark{animation:mdc-checkbox-checked-indeterminate-mixedmark 90ms linear;transition:none}.mdc-checkbox--anim-indeterminate-checked .mdc-checkbox__checkmark{animation:mdc-checkbox-indeterminate-checked-checkmark 500ms linear;transition:none}.mdc-checkbox--anim-indeterminate-checked .mdc-checkbox__mixedmark{animation:mdc-checkbox-indeterminate-checked-mixedmark 500ms linear;transition:none}.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__mixedmark{animation:mdc-checkbox-indeterminate-unchecked-mixedmark 300ms linear;transition:none}.mdc-checkbox__native-control:checked~.mdc-checkbox__background,.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background{transition:border-color 90ms cubic-bezier(0, 0, 0.2, 1),background-color 90ms cubic-bezier(0, 0, 0.2, 1)}.mdc-checkbox__native-control:checked~.mdc-checkbox__background>.mdc-checkbox__checkmark>.mdc-checkbox__checkmark-path,.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background>.mdc-checkbox__checkmark>.mdc-checkbox__checkmark-path{stroke-dashoffset:0}.mdc-checkbox__native-control:checked~.mdc-checkbox__background>.mdc-checkbox__checkmark{transition:opacity 180ms cubic-bezier(0, 0, 0.2, 1),transform 180ms cubic-bezier(0, 0, 0.2, 1);opacity:1}.mdc-checkbox__native-control:checked~.mdc-checkbox__background>.mdc-checkbox__mixedmark{transform:scaleX(1) rotate(-45deg)}.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background>.mdc-checkbox__checkmark{transform:rotate(45deg);opacity:0;transition:opacity 90ms cubic-bezier(0.4, 0, 0.6, 1),transform 90ms cubic-bezier(0.4, 0, 0.6, 1)}.mdc-checkbox__native-control:indeterminate~.mdc-checkbox__background>.mdc-checkbox__mixedmark{transform:scaleX(1) rotate(0deg);opacity:1}@keyframes mdc-checkbox-unchecked-checked-checkmark-path{0%,50%{stroke-dashoffset:29.7833385}50%{animation-timing-function:cubic-bezier(0, 0, 0.2, 1)}100%{stroke-dashoffset:0}}@keyframes mdc-checkbox-unchecked-indeterminate-mixedmark{0%,68.2%{transform:scaleX(0)}68.2%{animation-timing-function:cubic-bezier(0, 0, 0, 1)}100%{transform:scaleX(1)}}@keyframes mdc-checkbox-checked-unchecked-checkmark-path{from{animation-timing-function:cubic-bezier(0.4, 0, 1, 1);opacity:1;stroke-dashoffset:0}to{opacity:0;stroke-dashoffset:-29.7833385}}@keyframes mdc-checkbox-checked-indeterminate-checkmark{from{animation-timing-function:cubic-bezier(0, 0, 0.2, 1);transform:rotate(0deg);opacity:1}to{transform:rotate(45deg);opacity:0}}@keyframes mdc-checkbox-indeterminate-checked-checkmark{from{animation-timing-function:cubic-bezier(0.14, 0, 0, 1);transform:rotate(45deg);opacity:0}to{transform:rotate(360deg);opacity:1}}@keyframes mdc-checkbox-checked-indeterminate-mixedmark{from{animation-timing-function:cubic-bezier(0, 0, 0.2, 1);transform:rotate(-45deg);opacity:0}to{transform:rotate(0deg);opacity:1}}@keyframes mdc-checkbox-indeterminate-checked-mixedmark{from{animation-timing-function:cubic-bezier(0.14, 0, 0, 1);transform:rotate(0deg);opacity:1}to{transform:rotate(315deg);opacity:0}}@keyframes mdc-checkbox-indeterminate-unchecked-mixedmark{0%{animation-timing-function:linear;transform:scaleX(1);opacity:1}32.8%,100%{transform:scaleX(0);opacity:0}}.mat-mdc-checkbox{display:inline-block;position:relative;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-checkbox._mat-animation-noopable>.mat-internal-form-field>.mdc-checkbox>.mat-mdc-checkbox-touch-target,.mat-mdc-checkbox._mat-animation-noopable>.mat-internal-form-field>.mdc-checkbox>.mdc-checkbox__native-control,.mat-mdc-checkbox._mat-animation-noopable>.mat-internal-form-field>.mdc-checkbox>.mdc-checkbox__ripple,.mat-mdc-checkbox._mat-animation-noopable>.mat-internal-form-field>.mdc-checkbox>.mat-mdc-checkbox-ripple::before,.mat-mdc-checkbox._mat-animation-noopable>.mat-internal-form-field>.mdc-checkbox>.mdc-checkbox__background,.mat-mdc-checkbox._mat-animation-noopable>.mat-internal-form-field>.mdc-checkbox>.mdc-checkbox__background>.mdc-checkbox__checkmark,.mat-mdc-checkbox._mat-animation-noopable>.mat-internal-form-field>.mdc-checkbox>.mdc-checkbox__background>.mdc-checkbox__checkmark>.mdc-checkbox__checkmark-path,.mat-mdc-checkbox._mat-animation-noopable>.mat-internal-form-field>.mdc-checkbox>.mdc-checkbox__background>.mdc-checkbox__mixedmark{transition:none !important;animation:none !important}.mat-mdc-checkbox label{cursor:pointer}.mat-mdc-checkbox .mat-internal-form-field{color:var(--mat-checkbox-label-text-color, var(--mat-sys-on-surface));font-family:var(--mat-checkbox-label-text-font, var(--mat-sys-body-medium-font));line-height:var(--mat-checkbox-label-text-line-height, var(--mat-sys-body-medium-line-height));font-size:var(--mat-checkbox-label-text-size, var(--mat-sys-body-medium-size));letter-spacing:var(--mat-checkbox-label-text-tracking, var(--mat-sys-body-medium-tracking));font-weight:var(--mat-checkbox-label-text-weight, var(--mat-sys-body-medium-weight))}.mat-mdc-checkbox.mat-mdc-checkbox-disabled.mat-mdc-checkbox-disabled-interactive{pointer-events:auto}.mat-mdc-checkbox.mat-mdc-checkbox-disabled.mat-mdc-checkbox-disabled-interactive input{cursor:default}.mat-mdc-checkbox.mat-mdc-checkbox-disabled label{cursor:default;color:var(--mat-checkbox-disabled-label-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}@media(forced-colors: active){.mat-mdc-checkbox.mat-mdc-checkbox-disabled label{color:GrayText}}.mat-mdc-checkbox label:empty{display:none}.mat-mdc-checkbox .mdc-checkbox__ripple{opacity:0}.mat-mdc-checkbox .mat-mdc-checkbox-ripple,.mdc-checkbox__ripple{top:0;left:0;right:0;bottom:0;position:absolute;border-radius:50%;pointer-events:none}.mat-mdc-checkbox .mat-mdc-checkbox-ripple:not(:empty),.mdc-checkbox__ripple:not(:empty){transform:translateZ(0)}.mat-mdc-checkbox-ripple .mat-ripple-element{opacity:.1}.mat-mdc-checkbox-touch-target{position:absolute;top:50%;left:50%;height:var(--mat-checkbox-touch-target-size, 48px);width:var(--mat-checkbox-touch-target-size, 48px);transform:translate(-50%, -50%);display:var(--mat-checkbox-touch-target-display, block)}.mat-mdc-checkbox .mat-mdc-checkbox-ripple::before{border-radius:50%}.mdc-checkbox__native-control:focus~.mat-focus-indicator::before{content:""} +`],encapsulation:2,changeDetection:0})}return t})(),kx=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[is,De,De]})}return t})();function oie(t,n){t&1&&(m(0,"span",1),f(1,"*"),h())}var aie=(()=>{let n=class n extends uo{constructor(i,r){super(),this.renderer=i,this.focusMonitor=r,this.defaultOptions={props:{hideFieldUnderline:!0,indeterminate:!0,floatLabel:"always",hideLabel:!0,color:"accent"}}}onContainerClick(i){this.checkbox.focus(),super.onContainerClick(i)}ngAfterViewInit(){this.checkbox&&this.focusMonitor.monitor(this.checkbox._inputElement,!0).subscribe(i=>{this.field.focus=!!i,this.stateChanges.next(),i?this.props.focus&&this.props.focus(this.field):this.props.blur&&this.props.blur(this.field)})}ngAfterViewChecked(){if(this.required!==this._required&&this.checkbox&&this.checkbox._inputElement){this._required=this.required;let i=this.checkbox._inputElement.nativeElement;this.required?this.renderer.setAttribute(i,"required","required"):this.renderer.removeAttribute(i,"required")}}ngOnDestroy(){super.ngOnDestroy(),this.checkbox&&this.focusMonitor.stopMonitoring(this.checkbox._inputElement)}};n.\u0275fac=function(r){return new(r||n)(be(ze),be(oi))},n.\u0275cmp=E({type:n,selectors:[["formly-field-mat-checkbox"]],viewQuery:function(r,o){if(r&1&&ie(is,7),r&2){let a;j(a=H())&&(o.checkbox=a.first)}},standalone:!1,features:[le],decls:3,vars:10,consts:[[3,"formControl","id","name","formlyAttributes","tabIndex","indeterminate","color","labelPosition"],["aria-hidden","true",1,"mat-form-field-required-marker","mat-mdc-form-field-required-marker"]],template:function(r,o){r&1&&(m(0,"mat-checkbox",0),f(1),L(2,oie,2,0,"span",1),h()),r&2&&(v("formControl",o.formControl)("id",o.id)("name",o.field.name)("formlyAttributes",o.field)("tabIndex",o.props.tabindex)("indeterminate",o.props.indeterminate&&o.formControl.value==null)("color",o.props.color)("labelPosition",o.props.labelPosition),g(),fe(" ",o.props.label," "),g(),V(o.props.required&&o.props.hideRequiredMarker!==!0?2:-1))},dependencies:[Pt,Po,is,la],encapsulation:2,changeDetection:0});let t=n;return t})();function V3(){return{types:[{name:"checkbox",component:aie,wrappers:["form-field"]},{name:"boolean",extends:"checkbox"}]}}function sie(t,n){if(t&1){let e=q();m(0,"mat-checkbox",1),S("change",function(r){let o=k(e).$implicit,a=x();return T(a.onChange(o.value,r.checked))}),f(1),h()}if(t&2){let e=n.$implicit,i=n.$index,r=x();v("id",r.id+"_"+i)("formlyAttributes",r.field)("tabIndex",r.props.tabindex)("color",r.props.color)("labelPosition",r.props.labelPosition)("checked",r.isChecked(e))("disabled",r.formControl.disabled||e.disabled),g(),fe(" ",e.label," ")}}var lie=(()=>{let n=class n extends uo{constructor(){super(...arguments),this.defaultOptions={props:{hideFieldUnderline:!0,floatLabel:"always",color:"accent"}}}onChange(i,r){this.formControl.markAsDirty(),this.props.type==="array"?this.formControl.patchValue(r?[...this.formControl.value||[],i]:[...this.formControl.value||[]].filter(o=>o!==i)):this.formControl.patchValue(Me(I({},this.formControl.value),{[i]:r})),this.formControl.markAsTouched()}onContainerClick(){}isChecked(i){let r=this.formControl.value;return r&&(this.props.type==="array"?r.indexOf(i.value)!==-1:r[i.value])}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-field-mat-multicheckbox"]],viewQuery:function(r,o){if(r&1&&ie(is,5),r&2){let a;j(a=H())&&(o.checkboxes=a)}},hostVars:1,hostBindings:function(r,o){r&2&&pi("id",o.id)},standalone:!1,features:[le],decls:4,vars:5,consts:[[3,"id","formlyAttributes","tabIndex","color","labelPosition","checked","disabled"],[3,"change","id","formlyAttributes","tabIndex","color","labelPosition","checked","disabled"]],template:function(r,o){r&1&&(Mt(0,sie,2,8,"mat-checkbox",0,qi),me(2,"formlySelectOptions"),me(3,"async")),r&2&&Et(Re(3,3,Ui(2,0,o.props.options,o.field)))},dependencies:[is,la,cn,Ih],encapsulation:2,changeDetection:0});let t=n;return t})();function B3(){return{types:[{name:"multicheckbox",component:lie,wrappers:["form-field"]}]}}var cie=t=>({selectOptions:t});function die(t,n){if(t&1){let e=q();m(0,"mat-option",2),S("click",function(){let r=k(e).selectOptions,o=x();return T(o.toggleSelectAll(r))}),M(1,"mat-pseudo-checkbox",3),f(2),h()}if(t&2){let e=n.selectOptions,i=x();g(),v("state",i.getSelectAllState(e)),g(),fe(" ",i.props.selectAllOption," ")}}function uie(t,n){if(t&1&&qe(0,4),t&2){let e=x();x();let i=Te(1);v("ngTemplateOutlet",i)("ngTemplateOutletContext",$t(2,cie,e))}}function mie(t,n){if(t&1&&(m(0,"mat-option",6),f(1),h()),t&2){let e=n.$implicit;v("value",e.value)("disabled",e.disabled),g(),fe(" ",e.label," ")}}function hie(t,n){if(t&1&&(m(0,"mat-optgroup",5),Mt(1,mie,2,3,"mat-option",6,qi),h()),t&2){let e=x().$implicit;v("label",e.label),g(),Et(e.group)}}function pie(t,n){if(t&1&&(m(0,"mat-option",6),f(1),h()),t&2){let e=x().$implicit;v("value",e.value)("disabled",e.disabled),g(),N(e.label)}}function fie(t,n){if(t&1&&L(0,hie,3,1,"mat-optgroup",5)(1,pie,2,3,"mat-option",6),t&2){let e=n.$implicit;V(e.group?0:1)}}function gie(t,n){if(t&1&&(L(0,uie,1,4,"ng-container",4),Mt(1,fie,2,1,null,null,qi)),t&2){let e=x();V(e.props.multiple&&e.props.selectAllOption?0:-1),g(),Et(n)}}var _ie=(()=>{let n=class n extends uo{constructor(){super(...arguments),this.defaultOptions={props:{compareWith(i,r){return i===r}}}}set select(i){Zi(i,["_parentFormField","_textField"],({currentValue:r})=>{r&&(i._preferredOverlayOrigin=i._parentFormField.getConnectedOverlayOrigin())})}getSelectAllState(i){return this.empty||this.value.length===0?null:this.value.length!==this.getSelectAllValue(i).length?"indeterminate":"checked"}toggleSelectAll(i){let r=this.getSelectAllValue(i);this.formControl.markAsDirty(),this.formControl.setValue(!this.value||this.value.length!==r.length?r:[])}change(i){this.props.change?.(this.field,i)}_getAriaLabelledby(){return this.props.attributes?.["aria-labelledby"]?this.props.attributes["aria-labelledby"]:this.formField?._labelId}_getAriaLabel(){return this.props.attributes?.["aria-label"]}getSelectAllValue(i){if(!this.selectAllValue||i!==this.selectAllValue.options){let r=[];i.forEach(o=>o.group?r.push(...o.group):r.push(o)),this.selectAllValue={options:i,value:r.filter(o=>!o.disabled).map(o=>o.value)}}return this.selectAllValue.value}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-field-mat-select"]],viewQuery:function(r,o){if(r&1&&ie(es,7),r&2){let a;j(a=H())&&(o.select=a.first)}},standalone:!1,features:[le],decls:6,vars:20,consts:[["selectAll",""],[3,"selectionChange","id","formControl","formlyAttributes","placeholder","tabIndex","required","compareWith","multiple","errorStateMatcher","aria-label","aria-labelledby","disableOptionCentering","typeaheadDebounceInterval","panelClass"],[3,"click"],[1,"mat-option-pseudo-checkbox",3,"state"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[3,"label"],[3,"value","disabled"]],template:function(r,o){if(r&1){let a=q();A(0,die,3,2,"ng-template",null,0,Mi),m(2,"mat-select",1),S("selectionChange",function(l){return k(a),T(o.change(l))}),L(3,gie,3,1),me(4,"formlySelectOptions"),me(5,"async"),h()}if(r&2){let a;g(2),v("id",o.id)("formControl",o.formControl)("formlyAttributes",o.field)("placeholder",o.props.placeholder)("tabIndex",o.props.tabindex)("required",o.required)("compareWith",o.props.compareWith)("multiple",o.props.multiple)("errorStateMatcher",o.errorStateMatcher),pc("aria-label",o._getAriaLabel())("aria-labelledby",o._getAriaLabelledby()),v("disableOptionCentering",o.props.disableOptionCentering)("typeaheadDebounceInterval",o.props.typeaheadDebounceInterval)("panelClass",o.props.panelClass),g(),V((a=Re(5,18,Ui(4,15,o.props.options,o.field)))?3:-1,a)}},dependencies:[$n,Pt,Fo,Po,es,Sn,PS,nu,la,cn,Ih],encapsulation:2,changeDetection:0});let t=n;return t})();function j3(){return{types:[{name:"select",component:_ie,wrappers:["form-field"]},{name:"enum",extends:"select"}]}}function H3(){return[S3(),O3(),R3(),F3(),V3(),B3(),j3()]}var Og=class{},U3=(()=>{class t{handle(e){return e.key}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),Ah=class{},$3=(()=>{class t extends Ah{compile(e,i){return e}compileTranslations(e,i){return e}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),Vc=class{},W3=(()=>{class t extends Vc{getTranslation(e){return Q({})}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();function Rg(t,n){if(t===n)return!0;if(t===null||n===null)return!1;if(t!==t&&n!==n)return!0;let e=typeof t,i=typeof n,r;if(e==i&&e=="object")if(Array.isArray(t)){if(!Array.isArray(n))return!1;if((r=t.length)==n.length){for(let o=0;oTx(n));if(Il(t)){let n={};return Object.keys(t).forEach(e=>{n[e]=Tx(t[e])}),n}else return t}function Rk(t,n){if(!Ag(t))return Tx(n);let e=Tx(t);return Ag(e)&&Ag(n)&&Object.keys(n).forEach(i=>{Il(n[i])?i in t?e[i]=Rk(t[i],n[i]):Object.assign(e,{[i]:n[i]}):Object.assign(e,{[i]:n[i]})}),e}function G3(t,n){let e=n.split(".");n="";do{n+=e.shift();let i=!e.length;if(Lo(t)){if(Il(t)&&z3(t[n])&&(Il(t[n])||yu(t[n])||i)){t=t[n],n="";continue}if(yu(t)){let r=parseInt(n,10);if(z3(t[r])&&(Il(t[r])||yu(t[r])||i)){t=t[r],n="";continue}}}if(i){t=void 0;continue}n+="."}while(e.length);return t}function vie(t,n,e){return Rk(t,yie(n,e))}function yie(t,n){return t.split(".").reduceRight((e,i)=>({[i]:e}),n)}var Oh=class{},q3=(()=>{class t extends Oh{templateMatcher=/{{\s?([^{}\s]*)\s?}}/g;interpolate(e,i){if(Pg(e))return this.interpolateString(e,i);if(bie(e))return this.interpolateFunction(e,i)}interpolateFunction(e,i){return e(i)}interpolateString(e,i){return i?e.replace(this.templateMatcher,(r,o)=>{let a=this.getInterpolationReplacement(i,o);return a!==void 0?a:r}):e}getInterpolationReplacement(e,i){return this.formatValue(G3(e,i))}formatValue(e){if(Pg(e))return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(e===null)return"null";if(yu(e))return e.join(", ");if(Ag(e))return typeof e.toString=="function"&&e.toString!==Object.prototype.toString?e.toString():JSON.stringify(e)}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),Ik=(()=>{class t{_onTranslationChange=new z;_onLangChange=new z;_onFallbackLangChange=new z;fallbackLang=null;currentLang;translations={};languages=[];getTranslations(e){return this.translations[e]}setTranslations(e,i,r){this.translations[e]=r&&this.hasTranslationFor(e)?Rk(this.translations[e],i):i,this.addLanguages([e]),this._onTranslationChange.next({lang:e,translations:this.getTranslations(e)})}getLanguages(){return this.languages}getCurrentLang(){return this.currentLang}getFallbackLang(){return this.fallbackLang}setFallbackLang(e,i=!0){this.fallbackLang=e,i&&this._onFallbackLangChange.next({lang:e,translations:this.translations[e]})}setCurrentLang(e,i=!0){this.currentLang=e,i&&this._onLangChange.next({lang:e,translations:this.translations[e]})}get onTranslationChange(){return this._onTranslationChange.asObservable()}get onLangChange(){return this._onLangChange.asObservable()}get onFallbackLangChange(){return this._onFallbackLangChange.asObservable()}addLanguages(e){this.languages=Array.from(new Set([...this.languages,...e]))}hasTranslationFor(e){return typeof this.translations[e]<"u"}deleteTranslations(e){delete this.translations[e]}getTranslation(e){let i=this.getValue(this.currentLang,e);return i===void 0&&this.fallbackLang!=null&&this.fallbackLang!==this.currentLang&&(i=this.getValue(this.fallbackLang,e)),i}getValue(e,i){return G3(this.getTranslations(e),i)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),Ak=new O("TRANSLATE_CONFIG"),Ig=t=>Gi(t)?t:Q(t);var ca=(()=>{class t{loadingTranslations;pending=!1;_translationRequests={};lastUseLanguage=null;currentLoader=u(Vc);compiler=u(Ah);parser=u(Oh);missingTranslationHandler=u(Og);store=u(Ik);extend=!1;get onTranslationChange(){return this.store.onTranslationChange}get onLangChange(){return this.store.onLangChange}get onFallbackLangChange(){return this.store.onFallbackLangChange}get onDefaultLangChange(){return this.store.onFallbackLangChange}constructor(){let e=I({extend:!1,fallbackLang:null},u(Ak,{optional:!0}));e.lang&&this.use(e.lang),e.fallbackLang&&this.setFallbackLang(e.fallbackLang),e.extend&&(this.extend=!0)}setFallbackLang(e){this.getFallbackLang()||this.store.setFallbackLang(e,!1);let i=this.loadOrExtendLanguage(e);return Gi(i)?(i.pipe(mt(1)).subscribe({next:()=>{this.store.setFallbackLang(e)},error:()=>{}}),i):(this.store.setFallbackLang(e),Q(this.store.getTranslations(e)))}use(e){this.lastUseLanguage=e,this.getCurrentLang()||this.store.setCurrentLang(e,!1);let i=this.loadOrExtendLanguage(e);return Gi(i)?(i.pipe(mt(1)).subscribe({next:()=>{this.changeLang(e)},error:()=>{}}),i):(this.changeLang(e),Q(this.store.getTranslations(e)))}loadOrExtendLanguage(e){if(!this.store.hasTranslationFor(e)||this.extend)return this._translationRequests[e]=this._translationRequests[e]||this.loadAndCompileTranslations(e),this._translationRequests[e]}changeLang(e){e===this.lastUseLanguage&&this.store.setCurrentLang(e)}getCurrentLang(){return this.store.getCurrentLang()}loadAndCompileTranslations(e){this.pending=!0;let i=this.currentLoader.getTranslation(e).pipe(vd(1),mt(1));return this.loadingTranslations=i.pipe(se(r=>this.compiler.compileTranslations(r,e)),vd(1),mt(1)),this.loadingTranslations.subscribe({next:r=>{this.store.setTranslations(e,r,this.extend),this.pending=!1},error:r=>{this.pending=!1}}),i}setTranslation(e,i,r=!1){let o=this.compiler.compileTranslations(i,e);this.store.setTranslations(e,o,r||this.extend)}getLangs(){return this.store.getLanguages()}addLangs(e){this.store.addLanguages(e)}getParsedResultForKey(e,i){let r=this.getTextToInterpolate(e);if(Lo(r))return this.runInterpolation(r,i);let o=this.missingTranslationHandler.handle(I({key:e,translateService:this},i!==void 0&&{interpolateParams:i}));return o!==void 0?o:e}getFallbackLang(){return this.store.getFallbackLang()}getTextToInterpolate(e){return this.store.getTranslation(e)}runInterpolation(e,i){if(Lo(e))return yu(e)?this.runInterpolationOnArray(e,i):Il(e)?this.runInterpolationOnDict(e,i):this.parser.interpolate(e,i)}runInterpolationOnArray(e,i){return e.map(r=>this.runInterpolation(r,i))}runInterpolationOnDict(e,i){let r={};for(let o in e){let a=this.runInterpolation(e[o],i);a!==void 0&&(r[o]=a)}return r}getParsedResult(e,i){return e instanceof Array?this.getParsedResultForArray(e,i):this.getParsedResultForKey(e,i)}getParsedResultForArray(e,i){let r={},o=!1;for(let s of e)r[s]=this.getParsedResultForKey(s,i),o=o||Gi(r[s]);if(!o)return r;let a=e.map(s=>Ig(r[s]));return cs(a).pipe(se(s=>{let l={};return s.forEach((c,d)=>{l[e[d]]=c}),l}))}get(e,i){if(!Lo(e)||!e.length)throw new Error('Parameter "key" is required and cannot be empty');return this.pending?this.loadingTranslations.pipe(jo(()=>Ig(this.getParsedResult(e,i)))):Ig(this.getParsedResult(e,i))}getStreamOnTranslationChange(e,i){if(!Lo(e)||!e.length)throw new Error('Parameter "key" is required and cannot be empty');return Co(Fn(()=>this.get(e,i)),this.onTranslationChange.pipe(je(()=>{let r=this.getParsedResult(e,i);return Ig(r)})))}stream(e,i){if(!Lo(e)||!e.length)throw new Error('Parameter "key" required');return Co(Fn(()=>this.get(e,i)),this.onLangChange.pipe(je(()=>{let r=this.getParsedResult(e,i);return Ig(r)})))}instant(e,i){if(!Lo(e)||e.length===0)throw new Error('Parameter "key" is required and cannot be empty');let r=this.getParsedResult(e,i);return Gi(r)?Array.isArray(e)?e.reduce((o,a)=>(o[a]=a,o),{}):e:r}set(e,i,r=this.getCurrentLang()){this.store.setTranslations(r,vie(this.store.getTranslations(r),e,Pg(i)?this.compiler.compile(i,r):this.compiler.compileTranslations(i,r)),!1)}reloadLang(e){return this.resetLang(e),this.loadAndCompileTranslations(e)}resetLang(e){delete this._translationRequests[e],this.store.deleteTranslations(e)}static getBrowserLang(){if(typeof window>"u"||!window.navigator)return;let e=this.getBrowserCultureLang();return e?e.split(/[-_]/)[0]:void 0}static getBrowserCultureLang(){if(!(typeof window>"u"||typeof window.navigator>"u"))return window.navigator.languages?window.navigator.languages[0]:window.navigator.language||window.navigator.browserLanguage||window.navigator.userLanguage}getBrowserLang(){return t.getBrowserLang()}getBrowserCultureLang(){return t.getBrowserCultureLang()}get defaultLang(){return this.getFallbackLang()}get currentLang(){return this.store.getCurrentLang()}get langs(){return this.store.getLanguages()}setDefaultLang(e){return this.setFallbackLang(e)}getDefaultLang(){return this.getFallbackLang()}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),Y3=(()=>{class t{translateService=u(ca);element=u(Y);_ref=u(ye);key;lastParams;currentParams;onLangChangeSub;onFallbackLangChangeSub;onTranslationChangeSub;set translate(e){e&&(this.key=e,this.checkNodes())}set translateParams(e){Rg(this.currentParams,e)||(this.currentParams=e,this.checkNodes(!0))}constructor(){this.onTranslationChangeSub||(this.onTranslationChangeSub=this.translateService.onTranslationChange.subscribe(e=>{e.lang===this.translateService.currentLang&&this.checkNodes(!0,e.translations)})),this.onLangChangeSub||(this.onLangChangeSub=this.translateService.onLangChange.subscribe(e=>{this.checkNodes(!0,e.translations)})),this.onFallbackLangChangeSub||(this.onFallbackLangChangeSub=this.translateService.onFallbackLangChange.subscribe(e=>{this.checkNodes(!0)}))}ngAfterViewChecked(){this.checkNodes()}checkNodes(e=!1,i){let r=this.element.nativeElement.childNodes;r.length||(this.setContent(this.element.nativeElement,this.key),r=this.element.nativeElement.childNodes),r.forEach(o=>{let a=o;if(a.nodeType===3){let s;if(e&&(a.lastKey=null),Lo(a.lookupKey))s=a.lookupKey;else if(this.key)s=this.key;else{let l=this.getContent(a),c=l.trim();c.length&&(a.lookupKey=c,l!==a.currentValue?(s=c,a.originalContent=l||a.originalContent):a.originalContent&&(s=a.originalContent.trim()))}this.updateValue(s,a,i)}})}updateValue(e,i,r){if(e){if(i.lastKey===e&&this.lastParams===this.currentParams)return;this.lastParams=this.currentParams;let o=a=>{(a!==e||!i.lastKey)&&(i.lastKey=e),i.originalContent||(i.originalContent=this.getContent(i)),Pg(a)?i.currentValue=a:Lo(a)?i.currentValue=JSON.stringify(a):i.currentValue=i.originalContent||e,this.setContent(i,this.key?i.currentValue:i.originalContent.replace(e,i.currentValue)),this._ref.markForCheck()};if(Lo(r)){let a=this.translateService.getParsedResult(e,this.currentParams);Gi(a)?a.subscribe({next:o}):o(a)}else this.translateService.get(e,this.currentParams).subscribe(o)}}getContent(e){return Lo(e.textContent)?e.textContent:e.data}setContent(e,i){Lo(e.textContent)?e.textContent=i:e.data=i}ngOnDestroy(){this.onLangChangeSub&&this.onLangChangeSub.unsubscribe(),this.onFallbackLangChangeSub&&this.onFallbackLangChangeSub.unsubscribe(),this.onTranslationChangeSub&&this.onTranslationChangeSub.unsubscribe()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","translate",""],["","ngx-translate",""]],inputs:{translate:"translate",translateParams:"translateParams"}})}return t})(),Or=(()=>{class t{translate=u(ca);_ref=u(ye);value="";lastKey=null;lastParams=[];onTranslationChange;onLangChange;onFallbackLangChange;updateValue(e,i,r){let o=a=>{this.value=a!==void 0?a:e,this.lastKey=e,this._ref.markForCheck()};if(r){let a=this.translate.getParsedResult(e,i);Gi(a)?a.subscribe(o):o(a)}this.translate.get(e,i).subscribe(o)}transform(e,...i){if(!e||!e.length)return e;if(Rg(e,this.lastKey)&&Rg(i,this.lastParams))return this.value;let r;if(Lo(i[0])&&i.length)if(Pg(i[0])&&i[0].length){let o=i[0].replace(/(')?([a-zA-Z0-9_]+)(')?(\s)?:/g,'"$2":').replace(/:(\s)?(')(.*?)(')/g,':"$3"');try{r=JSON.parse(o)}catch(a){throw new SyntaxError(`Wrong parameter in TranslatePipe. Expected a valid Object, received: ${i[0]}`)}}else Il(i[0])&&(r=i[0]);return this.lastKey=e,this.lastParams=i,this.updateValue(e,r),this._dispose(),this.onTranslationChange||(this.onTranslationChange=this.translate.onTranslationChange.subscribe(o=>{(this.lastKey&&o.lang===this.translate.getCurrentLang()||o.lang===this.translate.getFallbackLang())&&(this.lastKey=null,this.updateValue(e,r,o.translations))})),this.onLangChange||(this.onLangChange=this.translate.onLangChange.subscribe(o=>{this.lastKey&&(this.lastKey=null,this.updateValue(e,r,o.translations))})),this.onFallbackLangChange||(this.onFallbackLangChange=this.translate.onFallbackLangChange.subscribe(()=>{this.lastKey&&(this.lastKey=null,this.updateValue(e,r))})),this.value}_dispose(){typeof this.onTranslationChange<"u"&&(this.onTranslationChange.unsubscribe(),this.onTranslationChange=void 0),typeof this.onLangChange<"u"&&(this.onLangChange.unsubscribe(),this.onLangChange=void 0),typeof this.onFallbackLangChange<"u"&&(this.onFallbackLangChange.unsubscribe(),this.onFallbackLangChange=void 0)}ngOnDestroy(){this._dispose()}static \u0275fac=function(i){return new(i||t)};static \u0275pipe=io({name:"translate",type:t,pure:!1});static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();function Q3(t){return{provide:Vc,useClass:t}}function K3(t){return{provide:Ah,useClass:t}}function Z3(t){return{provide:Oh,useClass:t}}function X3(t){return{provide:Og,useClass:t}}function J3(t={}){return Ok(I({compiler:K3($3),parser:Z3(q3),loader:Q3(W3),missingTranslationHandler:X3(U3)},t),!0)}function Ok(t={},n){let e=[];t.loader&&e.push(t.loader),t.compiler&&e.push(t.compiler),t.parser&&e.push(t.parser),t.missingTranslationHandler&&e.push(t.missingTranslationHandler),n&&e.push(Ik),(t.useDefaultLang||t.defaultLanguage)&&(console.warn("The `useDefaultLang` and `defaultLanguage` options are deprecated. Please use `fallbackLang` instead."),t.useDefaultLang===!0&&t.defaultLanguage&&(t.fallbackLang=t.defaultLanguage));let i={fallbackLang:t.fallbackLang??null,lang:t.lang,extend:t.extend??!1};return e.push({provide:Ak,useValue:i}),e.push({provide:ca,useClass:ca,deps:[Ik,Vc,Ah,Oh,Og,Ak]}),e}var Rr=(()=>{class t{static forRoot(e={}){return{ngModule:t,providers:[...Ok(I({compiler:K3($3),parser:Z3(q3),loader:Q3(W3),missingTranslationHandler:X3(U3)},e),!0)]}}static forChild(e={}){return{ngModule:t,providers:[...Ok(e,e.isolate??!1)]}}static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})();var Pk=new O("TRANSLATE_HTTP_LOADER_CONFIG"),xie=(()=>{class t{http;config;constructor(){this.config=I({prefix:"/assets/i18n/",suffix:".json",enforceLoading:!1,useHttpBackend:!1},u(Pk)),this.http=this.config.useHttpBackend?new kr(u(fc)):u(kr)}getTranslation(e){let i=this.config.enforceLoading?`?enforceLoading=${Date.now()}`:"";return this.http.get(`${this.config.prefix}${e}${this.config.suffix}${i}`)}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();function ej(t={}){let n=t.useHttpBackend??!1;return[{provide:Pk,useValue:t},{provide:Vc,useClass:xie,deps:[n?fc:kr,Pk]}]}var Fk=class{constructor(n){this.options=n,n.vcr?this.ref=n.vcr.createComponent(n.component,{index:n.vcr.length,injector:n.injector||n.vcr.injector}):(this.ref=Am(n.component,{elementInjector:n.injector,environmentInjector:n.environmentInjector}),n.appRef.attachView(this.ref.hostView))}setInput(n,e){return this.ref.setInput(n,e),this}setInputs(n){return Object.keys(n).forEach(e=>{this.ref.setInput(e,n[e])}),this}detectChanges(){return this.ref.hostView.detectChanges(),this}updateContext(n){return this.options.contextSignal?.set(n),this}appendTo(n){return n.appendChild(this.getElement()),this}removeFrom(n){return n.removeChild(this.getElement()),this}getRawContent(){return this.getElement().outerHTML}getElement(){return this.ref.location.nativeElement}destroy(){this.ref.destroy(),!this.options.vcr&&this.options.appRef.detachView(this.ref.hostView),this.ref=null}};function Fg(t){return t instanceof te}function Cu(t){return typeof t=="function"}function tj(t){return typeof t=="string"}var Nk=class{constructor(n){this.args=n,this.args.vcr?(this.ref=this.args.vcr.createEmbeddedView(this.args.tpl,this.args.context||{},{injector:n.injector}),this.ref.detectChanges()):(this.ref=this.args.tpl.createEmbeddedView(this.args.context||{},n.injector),this.ref.detectChanges(),this.args.appRef.attachView(this.ref))}detectChanges(){return this.ref.detectChanges(),this}getElement(){let n=this.ref.rootNodes;return n.length===1&&n[0]===Node.ELEMENT_NODE?this.element=n[0]:(this.element=document.createElement("div"),this.element.append(...n)),this.element}destroy(){this.ref.rootNodes[0]!==1&&(this.element?.parentNode.removeChild(this.element),this.element=null),this.args.vcr||this.args.appRef.detachView(this.ref),this.ref.destroy(),this.ref=null}updateContext(n){return Object.assign(this.ref.context,n),this}},Lk=class{constructor(n){this.value=n}getElement(){return this.value}detectChanges(){return this}updateContext(){return this}destroy(){}},Cie=new O("Component context"),Vk=(()=>{let n=class n{constructor(){this.injector=u(de),this.appRef=u(tr),this.environmentInjector=u(ti)}createComponent(i,r={}){let o=r.injector??this.injector,a;return r.context&&(a=he(r.context),o=de.create({providers:[{provide:Cie,useValue:a.asReadonly()}],parent:o})),new Fk({component:i,vcr:r.vcr,injector:o,appRef:this.appRef,environmentInjector:r.environmentInjector||this.environmentInjector,contextSignal:a})}createTemplate(i,r={}){return new Nk({vcr:r.vcr,appRef:this.appRef,tpl:i,context:r.context,injector:r.injector})}createView(i,r={}){if(Fg(i))return this.createTemplate(i,r);if(Cu(i))return this.createComponent(i,r);if(tj(i))return new Lk(i);throw"Type of content is not supported"}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var wie=(()=>{let n=class n{constructor(){this.content=re()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["dynamic-view"]],inputs:{content:[1,"content"]},decls:1,vars:1,consts:[[3,"innerHTML"]],template:function(r,o){r&1&&ni(0,"div",0),r&2&&pi("innerHTML",o.content(),rf)},encapsulation:2});let t=n;return t})(),Ix=(()=>{let n=class n{constructor(){this.view=re(void 0,{alias:"dynamicView"}),this.injector=re(void 0,{alias:"dynamicViewInjector"}),this.context=re(void 0,{alias:"dynamicViewContext"}),this.inputs=re(void 0,{alias:"dynamicViewInputs"}),this.defaultTpl=u(te),this.vcr=u(st),this.viewService=u(Vk)}ngOnInit(){this.resolveContentType()}ngOnChanges(i){let r=i.view&&!i.view.isFirstChange(),o=i.context&&!i.context.isFirstChange(),a=i.inputs&&!i.inputs.isFirstChange();r?this.resolveContentType():o?this.viewRef.updateContext(this.context()):Cu(this.view())&&a&&this.viewRef.setInputs(this.inputs()||{})}resolveContentType(){this.viewRef?.destroy();let i=this.view(),r=this.injector(),o=this.context();if(tj(i))(this.viewRef=this.viewService.createComponent(wie,{vcr:this.vcr,injector:r})).setInput("content",i).detectChanges();else if(Cu(i)){this.viewRef=this.viewService.createComponent(i,{vcr:this.vcr,injector:r??this.vcr.injector,context:o});let a=this.inputs();a&&this.viewRef.setInputs(a)}else this.viewRef=this.viewService.createView(i||this.defaultTpl,{vcr:this.vcr,injector:r??this.vcr.injector,context:o})}ngOnDestroy(){this.viewRef?.destroy()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","dynamicView",""]],inputs:{view:[1,"dynamicView","view"],injector:[1,"dynamicViewInjector","injector"],context:[1,"dynamicViewContext","context"],inputs:[1,"dynamicViewInputs","inputs"]},features:[Oe]});let t=n;return t})();function Die(t,n){if(t&1&&(f(0,` + `),M(1,"hot-toast-loader",1),f(2,` + `)),t&2){let e=x(2);g(),v("theme",e.theme)}}function Mie(t,n){if(t&1&&(f(0,` + `),m(1,"div"),f(2,` + `),M(3,"hot-toast-error",1),f(4,` + `),h(),f(5,` + `)),t&2){let e=x(3);g(3),v("theme",e.theme)}}function Eie(t,n){if(t&1&&(f(0,` + `),m(1,"div"),f(2,` + `),M(3,"hot-toast-checkmark",1),f(4,` + `),h(),f(5,` + `)),t&2){let e=x(3);g(3),v("theme",e.theme)}}function Sie(t,n){if(t&1&&(f(0,` + `),m(1,"div"),f(2,` + `),M(3,"hot-toast-warning",1),f(4,` + `),h(),f(5,` + `)),t&2){let e=x(3);g(3),v("theme",e.theme)}}function kie(t,n){if(t&1&&(f(0,` + `),m(1,"div"),f(2,` + `),M(3,"hot-toast-info",1),f(4,` + `),h(),f(5,` + `)),t&2){let e=x(3);g(3),v("theme",e.theme)}}function Tie(t,n){if(t&1&&(f(0,` + `),m(1,"div",2),f(2,` + `),m(3,"div"),f(4,` + `),L(5,Mie,6,1)(6,Eie,6,1)(7,Sie,6,1)(8,kie,6,1),f(9,` + `),h(),f(10,` + `),h(),f(11,` + `)),t&2){let e,i=x(2);g(5),V((e=i.type)==="error"?5:e==="success"?6:e==="warning"?7:e==="info"?8:-1)}}function Iie(t,n){if(t&1&&(f(0,` +`),m(1,"div",0),f(2,` + `),L(3,Die,3,1),L(4,Tie,12,1),h(),f(5,` +`)),t&2){let e=x();g(3),V(e.type==="loading"?3:-1),g(),V(e.type!=="loading"?4:-1)}}function Aie(t,n){t&1&&qe(0)}var oj=["hotToastBarBase"];function Oie(t,n){if(t&1&&(f(0,` + `),m(1,"hot-toast-animated-icon",7),f(2),h(),f(3,` + `)),t&2){let e=x(2);g(),v("iconTheme",e.toast.iconTheme),g(),N(e.toast.icon)}}function Rie(t,n){t&1&&qe(0)}function Pie(t,n){if(t&1&&(f(0,` + `),m(1,"div"),f(2,` + `),A(3,Rie,1,0,"ng-container",8),f(4,` + `),h(),f(5,` + `)),t&2){let e=x(2);g(3),v("dynamicView",e.toast.icon)}}function Fie(t,n){if(t&1&&(f(0," "),L(1,Oie,4,2)(2,Pie,6,1)),t&2){let e=x();g(),V(e.isIconString?1:2)}}function Nie(t,n){if(t&1&&(f(0,` + `),M(1,"hot-toast-indicator",9),f(2,` + `)),t&2){let e=x();g(),v("theme",e.toast.iconTheme)("type",e.toast.type)}}function Lie(t,n){t&1&&qe(0)}function Vie(t,n){if(t&1){let e=q();f(0,` + `),m(1,"button",10),S("click",function(){k(e);let r=x();return T(r.close())}),h(),f(2,` + `)}if(t&2){let e=x();g(),gl(e.toast.closeStyle)}}var Bie=(t,n)=>n.id;function jie(t,n){if(t&1&&(f(0,` + `),M(1,"hot-toast-animated-icon",7),f(2,` + `)),t&2){let e=x(2);g(),v("iconTheme",e.toast.iconTheme)("icon",e.toast.icon)}}function Hie(t,n){t&1&&qe(0)}function zie(t,n){if(t&1&&(f(0,` + `),m(1,"div"),f(2,` + `),A(3,Hie,1,0,"ng-container",8),f(4,` + `),h(),f(5,` + `)),t&2){let e=x(2);g(3),v("dynamicView",e.toast.icon)}}function Uie(t,n){if(t&1&&(f(0," "),L(1,jie,3,2)(2,zie,6,1)),t&2){let e=x();g(),V(e.isIconString?1:2)}}function $ie(t,n){if(t&1&&(f(0,` + `),M(1,"hot-toast-indicator",9),f(2,` + `)),t&2){let e=x();g(),v("theme",e.toast.iconTheme)("type",e.toast.type)}}function Wie(t,n){t&1&&qe(0)}function Gie(t,n){if(t&1){let e=q();f(0,` + `),m(1,"button",10),S("click",function(){k(e);let r=x();return T(r.toggleToastGroup())}),h(),f(2,` + `)}if(t&2){let e=x();g(),gl(e.toast.group.btnStyle),G("expanded",e.isExpanded),X("aria-label",e.isExpanded?"Collapse":"Expand")}}function qie(t,n){if(t&1){let e=q();f(0,` + `),m(1,"button",11),S("click",function(){k(e);let r=x();return T(r.close())}),h(),f(2,` + `)}if(t&2){let e=x();g(),gl(e.toast.closeStyle)}}function Yie(t,n){if(t&1){let e=q();f(0,` + `),m(1,"hot-toast-group-item",13),S("height",function(r){let o=k(e).$implicit,a=x(2);return T(a.updateHeight(r,o))})("beforeClosed",function(){let r=k(e).$implicit,o=x(2);return T(o.beforeClosedGroupItem(r))})("afterClosed",function(r){k(e);let o=x(2);return T(o.afterClosedGroupItem(r))}),h(),f(2,` + `)}if(t&2){let e=n.$implicit,i=n.$index,r=x(2);g(),v("toast",e)("offset",r.calculateOffset(e.id))("toastRef",r.toastRef.groupRefs[i])("toastsAfter",(e.autoClose?r.groupChildrenToasts.length:r.visibleToasts.length)-1-i)("defaultConfig",r.defaultConfig)("isShowingAllToasts",r.isShowingAllToasts)}}function Qie(t,n){if(t&1&&(f(0,` + `),m(1,"div",12),f(2,` + `),Mt(3,Yie,3,6,null,null,Bie),h(),f(5,` + `)),t&2){let e=x();g(),at(e.toast.group==null?null:e.toast.group.className),At("--hot-toast-group-height",e.groupHeight+"px"),g(2),Et(e.groupChildrenToasts)}}function Kie(t,n){t&1&&f(0,` + `)}function Zie(t,n){if(t&1){let e=q();f(0,` + `),m(1,"hot-toast",2),S("showAllToasts",function(r){k(e);let o=x(2);return T(o.showAllToasts(r))})("height",function(r){k(e);let o=x().$implicit,a=x();return T(a.updateHeight(r,o))})("beforeClosed",function(){k(e);let r=x().$implicit,o=x();return T(o.beforeClosed(r))})("afterClosed",function(r){k(e);let o=x(2);return T(o.afterClosed(r))})("toggleGroup",function(r){k(e);let o=x(2);return T(o.toggleGroup(r))}),h(),f(2,` + `)}if(t&2){let e=x(),i=e.$implicit,r=e.$index,o=x();g(),v("toast",i)("offset",o.calculateOffset(i.id,i.position))("toastRef",o.toastRefs[r])("toastsAfter",(i.autoClose?o.toasts.length:o.getVisibleToasts(i.position).length)-1-r)("defaultConfig",o.defaultConfig)("isShowingAllToasts",o.isShowingAllToasts)}}function Xie(t,n){if(t&1&&(f(0,` + `),L(1,Kie,1,0)(2,Zie,3,6)),t&2){let e=n.$implicit;g(),V(e.group!=null&&e.group.parent?1:2)}}var ij={blank:4e3,error:4e3,success:4e3,loading:3e4,warning:4e3,info:4e3},Rh=800,Ax=350,Jie=8,jk=.05,ene=1,Ng=class{constructor(n){this.toast=n,this.groupRefs=[],this.groupExpanded=!1,this._onClosed=new z,this._onGroupToggle=new z}set data(n){this.toast.data=n}get data(){return this.toast.data}set dispose(n){this._dispose=n}getToast(){return this.toast}appendTo(n,e){let{dispose:i,updateMessage:r,updateToast:o,afterClosed:a,afterGroupToggled:s,afterGroupRefsAttached:l}=n.addToast(this,e);return this.dispose=i,this.updateMessage=r,this.updateToast=o,this.afterClosed=Ku(this._onClosed.asObservable(),a),this.afterGroupToggled=Ku(this._onGroupToggle.asObservable(),s),this.afterGroupRefsAttached=l,this}close(n={dismissedByAction:!1}){this.groupRefs.forEach(e=>e.close()),this._dispose(),this._onClosed.next({dismissedByAction:n.dismissedByAction,id:this.toast.id}),this._onClosed.complete()}toggleGroup(n={byAction:!1}){this.groupExpanded=!this.groupExpanded,this._onGroupToggle.next({byAction:n.byAction,id:this.toast.id,event:this.groupExpanded?"expand":"collapse"})}show(){this.toast.visible=!0}},Ox=(t,n,e)=>{t.setStyle(n,"animation",e)},tne=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-loader"]],inputs:{theme:"theme"},decls:2,vars:4,consts:[[1,"hot-toast-loader-icon"]],template:function(r,o){r&1&&(ni(0,"div",0),f(1,` +`)),r&2&&At("border-color",o.theme==null?null:o.theme.primary)("border-right-color",o.theme==null?null:o.theme.secondary)},encapsulation:2,changeDetection:0});let t=n;return t})(),ine=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-error"]],inputs:{theme:"theme"},decls:2,vars:4,consts:[[1,"hot-toast-error-icon"]],template:function(r,o){r&1&&(ni(0,"div",0),f(1,` +`)),r&2&&At("--error-primary",o.theme==null?null:o.theme.primary)("--error-secondary",o.theme==null?null:o.theme.secondary)},encapsulation:2,changeDetection:0});let t=n;return t})(),nne=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-checkmark"]],inputs:{theme:"theme"},decls:2,vars:4,consts:[[1,"hot-toast-checkmark-icon"]],template:function(r,o){r&1&&(ni(0,"div",0),f(1,` +`)),r&2&&At("--check-primary",o.theme==null?null:o.theme.primary)("--check-secondary",o.theme==null?null:o.theme.secondary)},encapsulation:2,changeDetection:0});let t=n;return t})(),rne=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-warning"]],inputs:{theme:"theme"},decls:2,vars:4,consts:[[1,"hot-toast-warning-icon"]],template:function(r,o){r&1&&(ni(0,"div",0),f(1,` +`)),r&2&&At("--warn-primary",o.theme==null?null:o.theme.primary)("--warn-secondary",o.theme==null?null:o.theme.secondary)},encapsulation:2,changeDetection:0});let t=n;return t})(),one=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-info"]],inputs:{theme:"theme"},decls:2,vars:4,consts:[[1,"hot-toast-info-icon"]],template:function(r,o){r&1&&(ni(0,"div",0),f(1,` +`)),r&2&&At("--info-primary",o.theme==null?null:o.theme.primary)("--info-secondary",o.theme==null?null:o.theme.secondary)},encapsulation:2,changeDetection:0});let t=n;return t})(),aj=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-indicator"]],inputs:{theme:"theme",type:"type"},decls:1,vars:1,consts:[[1,"hot-toast-indicator-wrapper"],[3,"theme"],[1,"hot-toast-status-wrapper"]],template:function(r,o){r&1&&L(0,Iie,6,2),r&2&&V(o.type!=="blank"?0:-1)},dependencies:[tne,ine,nne,rne,one],encapsulation:2,changeDetection:0});let t=n;return t})(),sj=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-animated-icon"]],inputs:{iconTheme:"iconTheme",icon:"icon"},decls:5,vars:3,consts:[[1,"hot-toast-animated-icon"],[4,"dynamicView"]],template:function(r,o){r&1&&(m(0,"div",0),f(1,` + `),A(2,Aie,1,0,"ng-container",1),f(3,` +`),h(),f(4,` +`)),r&2&&(At("color",o.iconTheme==null?null:o.iconTheme.primary),g(2),v("dynamicView",o.icon))},dependencies:[Ix],encapsulation:2,changeDetection:0});let t=n;return t})(),ane=(()=>{let n=class n{constructor(){this.offset=0,this._toastsAfter=0,this.isShowingAllToasts=!1,this.height=new U,this.beforeClosed=new U,this.afterClosed=new U,this.showAllToasts=new U,this.toggleGroup=new U,this.isManualClose=!1,this.toastBarBaseStylesSignal=he({}),this.unlisteners=[],this.softClosed=!1,this.injector=u(de),this.renderer=u(ze),this.ngZone=u(ae),this.cdr=u(ye)}set toast(i){this._toast=i;let r=this.toastBarBaseStylesSignal(),o=I({},i.style);if(r.animation?.includes("hotToastExitAnimation"))o.animation=r.animation;else{let s=`hotToastEnterAnimation${i.position.includes("top")?"Negative":"Positive"} ${Ax}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`;o.animation=s}this.toastBarBaseStylesSignal.set(o)}get toast(){return this._toast}get toastsAfter(){return this._toastsAfter}set toastsAfter(i){this._toastsAfter=i}get toastBarBaseHeight(){return this.toastBarBase.nativeElement.offsetHeight}get scale(){return this.defaultConfig.stacking!=="vertical"&&!this.isShowingAllToasts?this.toastsAfter*-jk+1:1}get translateY(){return this.offset*(this.top?1:-1)+"px"}get exitAnimationDelay(){return this.toast.duration+"ms"}get top(){return this.toast.position.includes("top")}get containerPositionStyle(){let i=this.top?{top:0}:{bottom:0},r="translateY(var(--hot-toast-translate-y)) scale(var(--hot-toast-scale))",o=this.toast.position.includes("left")?{left:0}:this.toast.position.includes("right")?{right:0}:{left:0,right:0,justifyContent:"center"};return I(I({transform:r},i),o)}get isIconString(){return typeof this.toast.icon=="string"}get groupChildrenToastRefs(){return this.toastRef.groupRefs.filter(i=>!!i)}set groupChildrenToastRefs(i){this.toastRef.groupRefs=i}get groupChildrenToasts(){return this.groupChildrenToastRefs.map(i=>i.getToast())}get groupHeight(){return this.visibleToasts.map(i=>i.height).reduce((i,r)=>i+r,0)}get isExpanded(){return this.toastRef.groupExpanded}ngOnChanges(i){i.toast&&!i.toast.firstChange&&i.toast.currentValue?.message&&requestAnimationFrame(()=>{this.height.emit(this.toastBarBase.nativeElement.offsetHeight)})}ngOnInit(){Fg(this.toast.message)&&(this.context={$implicit:this.toastRef}),Cu(this.toast.message)&&(this.toastComponentInjector=de.create({providers:[{provide:Ng,useValue:this.toastRef}],parent:this.toast.injector||this.injector}));let i=this.toastBarBase.nativeElement;this.ngZone.runOutsideAngular(()=>{this.unlisteners.push(this.renderer.listen(i,"animationstart",r=>{this.isExitAnimation(r)&&this.ngZone.run(()=>{this.renderer.setStyle(i,"pointer-events","none"),this.renderer.setStyle(i.parentElement,"pointer-events","none"),this.beforeClosed.emit()})}),this.renderer.listen(i,"animationend",r=>{this.isEnterAnimation(r)&&this.ngZone.run(()=>{if(this.toast.autoClose){let o=`hotToastExitAnimation${this.top?"Negative":"Positive"} ${Rh}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1) var(--hot-toast-exit-animation-delay) var(--hot-toast-exit-animation-state)`;this.toastBarBaseStylesSignal.set(Me(I({},this.toast.style),{animation:o}))}}),this.isExitAnimation(r)&&this.ngZone.run(()=>this.afterClosed.emit({dismissedByAction:this.isManualClose,id:this.toast.id}))}))})}ngAfterViewInit(){let i=this.toastBarBase.nativeElement;requestAnimationFrame(()=>{this.height.emit(i.offsetHeight)}),this.setToastAttributes()}softClose(){let i=`hotToastExitSoftAnimation${this.top?"Negative":"Positive"} ${Rh}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`,r=this.toastBarBase.nativeElement;Ox(this.renderer,r,i),this.softClosed=!0}softOpen(){let i=`hotToastEnterSoftAnimation${top?"Negative":"Positive"} ${Ax}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`,r=this.toastBarBase.nativeElement;Ox(this.renderer,r,i),this.softClosed=!1}close(){this.isManualClose=!0,this.cdr.markForCheck();let i=`hotToastExitAnimation${this.top?"Negative":"Positive"} ${Rh}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`;this.toastBarBaseStylesSignal.set(Me(I({},this.toast.style),{animation:i}))}handleMouseEnter(){this.showAllToasts.emit(!0)}handleMouseLeave(){this.showAllToasts.emit(!1)}ngOnDestroy(){for(this.close();this.unlisteners.length;)this.unlisteners.pop()()}isExitAnimation(i){return i.animationName.includes("hotToastExitAnimation")}isEnterAnimation(i){return i.animationName.includes("hotToastEnterAnimation")}setToastAttributes(){let i=this.toast.attributes;for(let[r,o]of Object.entries(i))this.renderer.setAttribute(this.toastBarBase.nativeElement,r,o)}get visibleToasts(){return this.groupChildrenToasts.filter(i=>i.visible)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-group-item"]],viewQuery:function(r,o){if(r&1&&ie(oj,7),r&2){let a;j(a=H())&&(o.toastBarBase=a.first)}},inputs:{toast:"toast",offset:"offset",defaultConfig:"defaultConfig",toastRef:"toastRef",toastsAfter:"toastsAfter",isShowingAllToasts:"isShowingAllToasts"},outputs:{height:"height",beforeClosed:"beforeClosed",afterClosed:"afterClosed",showAllToasts:"showAllToasts",toggleGroup:"toggleGroup"},features:[Oe],decls:21,vars:25,consts:[["hotToastBarBase",""],[1,"hot-toast-bar-base-container"],[1,"hot-toast-bar-base-wrapper",3,"mouseenter","mouseleave"],[1,"hot-toast-bar-base"],["aria-hidden","true",1,"hot-toast-icon"],[1,"hot-toast-message"],[4,"dynamicView","dynamicViewContext","dynamicViewInjector"],[3,"iconTheme"],[4,"dynamicView"],[3,"theme","type"],["type","button","aria-label","Close",1,"hot-toast-close-btn",3,"click"]],template:function(r,o){if(r&1){let a=q();m(0,"div",1),f(1,` + `),m(2,"div",2),S("mouseenter",function(){return k(a),T(o.handleMouseEnter())})("mouseleave",function(){return k(a),T(o.handleMouseLeave())}),f(3,` + `),m(4,"div",3,0),f(6,` + `),m(7,"div",4),f(8,` + `),L(9,Fie,3,1)(10,Nie,3,2),h(),f(11,` + `),m(12,"div",5),f(13,` + `),A(14,Lie,1,0,"ng-container",6),f(15,` + `),h(),f(16,` + `),L(17,Vie,3,2),h(),f(18,` + `),h(),f(19,` +`),h(),f(20,` +`)}r&2&&(gl(o.containerPositionStyle),at("hot-toast-theme-"+o.toast.theme),At("--hot-toast-scale",o.scale)("--hot-toast-translate-y",o.translateY),g(4),gl(o.toastBarBaseStylesSignal()),at(o.toast.className),At("--hot-toast-animation-state",o.isManualClose?"running":"paused")("--hot-toast-exit-animation-state",o.isShowingAllToasts?"paused":"running")("--hot-toast-exit-animation-delay",o.exitAnimationDelay),X("aria-live",o.toast.ariaLive)("role",o.toast.role),g(5),V(o.toast.icon!==void 0?9:10),g(5),v("dynamicView",o.toast.message)("dynamicViewContext",o.context)("dynamicViewInjector",o.toastComponentInjector),g(3),V(o.toast.dismissible?17:-1))},dependencies:[sj,aj,Ix],encapsulation:2,changeDetection:0});let t=n;return t})(),nj=(()=>{let n=class n{constructor(){this.offset=0,this._toastsAfter=0,this.isShowingAllToasts=!1,this.height=new U,this.beforeClosed=new U,this.afterClosed=new U,this.showAllToasts=new U,this.toggleGroup=new U,this.isManualClose=!1,this.isExpanded=!1,this.toastBarBaseStylesSignal=he({}),this.unlisteners=[],this.softClosed=!1,this.groupRefs=[],this.injector=u(de),this.renderer=u(ze),this.ngZone=u(ae),this.cdr=u(ye)}set toast(i){this._toast=i;let r=this.toastBarBaseStylesSignal(),o=I({},i.style);if(r.animation?.includes("hotToastExitAnimation"))o.animation=r.animation;else{let s=`hotToastEnterAnimation${i.position.includes("top")?"Negative":"Positive"} ${Ax}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`;o.animation=s}this.toastBarBaseStylesSignal.set(o)}get toast(){return this._toast}get toastsAfter(){return this._toastsAfter}set toastsAfter(i){this._toastsAfter=i,this.defaultConfig?.visibleToasts>0&&(this.toast.autoClose||(i>=this.defaultConfig?.visibleToasts?this.softClose():this.softClosed&&this.softOpen()))}get toastBarBaseHeight(){return this.toastBarBase.nativeElement.offsetHeight}get scale(){return this.defaultConfig.stacking!=="vertical"&&!this.isShowingAllToasts?this.toastsAfter*-jk+1:1}get translateY(){return this.offset*(this.top?1:-1)+"px"}get exitAnimationDelay(){return this.toast.duration+"ms"}get top(){return this.toast.position.includes("top")}get containerPositionStyle(){let i=this.top?{top:0}:{bottom:0},r="translateY(var(--hot-toast-translate-y)) scale(var(--hot-toast-scale))",o=this.toast.position.includes("left")?{left:0}:this.toast.position.includes("right")?{right:0}:{left:0,right:0,justifyContent:"center"};return I(I({transform:r},i),o)}get isIconString(){return typeof this.toast.icon=="string"}get groupChildrenToastRefs(){return this.groupRefs.filter(i=>!!i)}set groupChildrenToastRefs(i){this.groupRefs=i,this.toastRef.groupRefs=i}get groupChildrenToasts(){return this.groupChildrenToastRefs.map(i=>i.getToast())}get groupHeight(){return this.visibleToasts.slice(-this.defaultConfig.visibleToasts).map(i=>i.height).reduce((i,r)=>i+r,0)}get visibleToasts(){return this.groupChildrenToasts.filter(i=>i.visible)}ngDoCheck(){this.toastRef.groupRefs.length!==this.groupRefs.length&&(this.groupRefs=this.toastRef.groupRefs.slice(),this.cdr.markForCheck(),this.emiHeightWithGroup(this.isExpanded)),this.toastRef.groupExpanded!==this.isExpanded&&(this.isExpanded=this.toastRef.groupExpanded,this.cdr.markForCheck(),this.emiHeightWithGroup(this.isExpanded))}ngOnChanges(i){i.toast&&!i.toast.firstChange&&i.toast.currentValue?.message&&this.emiHeightWithGroup(this.isExpanded)}ngOnInit(){Fg(this.toast.message)&&(this.context={$implicit:this.toastRef}),Cu(this.toast.message)&&(this.toastComponentInjector=de.create({providers:[{provide:Ng,useValue:this.toastRef}],parent:this.toast.injector||this.injector}));let i=this.toastBarBase.nativeElement;this.ngZone.runOutsideAngular(()=>{this.unlisteners.push(this.renderer.listen(i,"animationstart",r=>{this.isExitAnimation(r)&&this.ngZone.run(()=>{this.renderer.setStyle(i,"pointer-events","none"),this.renderer.setStyle(i.parentElement,"pointer-events","none"),this.beforeClosed.emit()})}),this.renderer.listen(i,"animationend",r=>{this.isEnterAnimation(r)&&this.ngZone.run(()=>{if(this.toast.autoClose){let o=`hotToastExitAnimation${this.top?"Negative":"Positive"} ${Rh}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1) var(--hot-toast-exit-animation-delay) var(--hot-toast-exit-animation-state)`;this.toastBarBaseStylesSignal.set(Me(I({},this.toast.style),{animation:o}))}}),this.isExitAnimation(r)&&this.ngZone.run(()=>this.afterClosed.emit({dismissedByAction:this.isManualClose,id:this.toast.id}))}))})}ngAfterViewInit(){let i=this.toastBarBase.nativeElement;requestAnimationFrame(()=>{this.height.emit(i.offsetHeight)}),this.setToastAttributes()}softClose(){let i=`hotToastExitSoftAnimation${this.top?"Negative":"Positive"} ${Rh}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`,r=this.toastBarBase.nativeElement;Ox(this.renderer,r,i),this.softClosed=!0,this.isExpanded&&this.toggleToastGroup()}softOpen(){let i=`hotToastEnterSoftAnimation${top?"Negative":"Positive"} ${Ax}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`,r=this.toastBarBase.nativeElement;Ox(this.renderer,r,i),this.softClosed=!1}close(){this.isManualClose=!0,this.cdr.markForCheck();let i=`hotToastExitAnimation${this.top?"Negative":"Positive"} ${Rh}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`;this.toastBarBaseStylesSignal.set(Me(I({},this.toast.style),{animation:i}))}handleMouseEnter(){this.showAllToasts.emit(!0)}handleMouseLeave(){this.showAllToasts.emit(!1)}ngOnDestroy(){for(this.close();this.unlisteners.length;)this.unlisteners.pop()()}isExitAnimation(i){return i.animationName.includes("hotToastExitAnimation")}isEnterAnimation(i){return i.animationName.includes("hotToastEnterAnimation")}setToastAttributes(){let i=this.toast.attributes;for(let[r,o]of Object.entries(i))this.renderer.setAttribute(this.toastBarBase.nativeElement,r,o)}calculateOffset(i){let r=this.visibleToasts,o=r.findIndex(s=>s.id===i);return o!==-1?r.slice(...this.defaultConfig.reverseOrder?[o+1]:[0,o]).reduce((s,l,c)=>this.defaultConfig.visibleToasts!==0&&co.id===i.id)>-1&&(this.groupChildrenToastRefs=this.groupChildrenToastRefs.filter(o=>o.getToast().id!==i.id),this.cdr.markForCheck())}toggleToastGroup(){let i=this.isExpanded?"collapse":"expand";this.toggleGroup.emit({byAction:!0,event:i,id:this.toast.id}),this.emiHeightWithGroup(i==="expand")}emiHeightWithGroup(i){requestAnimationFrame(i?()=>{this.height.emit(this.toastBarBase.nativeElement.offsetHeight+this.groupHeight)}:()=>{this.height.emit(this.toastBarBase.nativeElement.offsetHeight)})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast"]],viewQuery:function(r,o){if(r&1&&ie(oj,7),r&2){let a;j(a=H())&&(o.toastBarBase=a.first)}},inputs:{toast:"toast",offset:"offset",defaultConfig:"defaultConfig",toastRef:"toastRef",toastsAfter:"toastsAfter",isShowingAllToasts:"isShowingAllToasts"},outputs:{height:"height",beforeClosed:"beforeClosed",afterClosed:"afterClosed",showAllToasts:"showAllToasts",toggleGroup:"toggleGroup"},features:[Oe],decls:23,vars:29,consts:[["hotToastBarBase",""],[1,"hot-toast-bar-base-container"],[1,"hot-toast-bar-base-wrapper",3,"mouseenter","mouseleave"],[1,"hot-toast-bar-base"],["aria-hidden","true",1,"hot-toast-icon"],[1,"hot-toast-message"],[4,"dynamicView","dynamicViewContext","dynamicViewInjector"],[3,"iconTheme","icon"],[4,"dynamicView"],[3,"theme","type"],["type","button",1,"hot-toast-group-btn",3,"click"],["type","button","aria-label","Close",1,"hot-toast-close-btn",3,"click"],["role","list",1,"hot-toast-bar-base-group"],[3,"height","beforeClosed","afterClosed","toast","offset","toastRef","toastsAfter","defaultConfig","isShowingAllToasts"]],template:function(r,o){if(r&1){let a=q();m(0,"div",1),f(1,` + `),m(2,"div",2),S("mouseenter",function(){return k(a),T(o.handleMouseEnter())})("mouseleave",function(){return k(a),T(o.handleMouseLeave())}),f(3,` + `),m(4,"div",3,0),f(6,` + `),m(7,"div",4),f(8,` + `),L(9,Uie,3,1)(10,$ie,3,2),h(),f(11,` + + `),m(12,"div",5),f(13,` + `),A(14,Wie,1,0,"ng-container",6),f(15,` + `),h(),f(16,` + + `),L(17,Gie,3,5),L(18,qie,3,2),h(),f(19,` + + `),L(20,Qie,6,4),h(),f(21,` +`),h(),f(22,` +`)}r&2&&(gl(o.containerPositionStyle),at("hot-toast-theme-"+o.toast.theme),At("--hot-toast-scale",o.scale)("--hot-toast-translate-y",o.translateY),g(2),G("expanded",o.isExpanded),g(2),gl(o.toastBarBaseStylesSignal()),at(o.toast.className),At("--hot-toast-animation-state",o.isManualClose?"running":"paused")("--hot-toast-exit-animation-state",o.isShowingAllToasts?"paused":"running")("--hot-toast-exit-animation-delay",o.exitAnimationDelay),X("aria-live",o.toast.ariaLive)("role",o.toast.role),g(5),V(o.toast.icon!==void 0?9:10),g(5),v("dynamicView",o.toast.message)("dynamicViewContext",o.context)("dynamicViewInjector",o.toastComponentInjector),g(3),V(o.toast.group!=null&&o.toast.group.expandAndCollapsible&&(o.toast.group!=null&&o.toast.group.children)&&o.visibleToasts.length>0?17:-1),g(),V(o.toast.dismissible?18:-1),g(2),V(o.toast.visible?20:-1))},dependencies:[Ix,aj,sj,ane],encapsulation:2,changeDetection:0});let t=n;return t})(),rj=(()=>{let n=class n{constructor(){this.toasts=[],this.toastRefs=[],this.isShowingAllToasts=!1,this._onClosed=new z,this._onGroupToggle=new z,this._onGroupRefAttached=new z,this.onClosed$=this._onClosed.asObservable(),this.onGroupToggle$=this._onGroupToggle.asObservable(),this.onGroupRefAttached$=this._onGroupRefAttached.asObservable(),this.cdr=u(ye),this.toastService=u(Vg)}trackById(i,r){return r.id}getVisibleToasts(i){return this.unGroupedToasts.filter(r=>r.visible&&r.position===i)}get unGroupedToasts(){return this.toasts.filter(i=>i.group?.parent===void 0||i.group?.children===void 0||i.group?.children.length===0)}calculateOffset(i,r){let o=this.getVisibleToasts(r),a=o.findIndex(l=>l.id===i);return a!==-1?o.slice(...this.defaultConfig.reverseOrder?[a+1]:[0,a]).reduce((l,c,d)=>{let p=o.length-1-d;return this.defaultConfig.visibleToasts!==0&&dthis.defaultConfig.visibleToasts&&this.toasts.slice(0,this.toasts.length-this.defaultConfig.visibleToasts).forEach(s=>{s.autoClose&&this.closeToast(s.id)}),this.cdr.markForCheck(),this.attachGroupRefs(o,i,r),{dispose:()=>{this.closeToast(o.id)},updateMessage:a=>{o.message=a,this.updateToasts(o),this.cdr.markForCheck()},updateToast:a=>{this.updateToasts(o,a),this.cdr.markForCheck()},afterClosed:this.getAfterClosed(o),afterGroupToggled:this.getAfterGroupToggled(o),afterGroupRefsAttached:this.getAfterGroupRefsAttached(o).pipe(se(a=>a.groupRefs))}}attachGroupRefs(i,r,o){return yn(this,null,function*(){let a=[];if(i.group){if(i.group.children){a=yield this.createGroupRefs(i,r);let s=this.toastRefs.findIndex(l=>l.getToast().id===i.id);s>-1&&(this.toastRefs[s].groupRefs=a,this.cdr.markForCheck(),this._onGroupRefAttached.next({groupRefs:a,id:i.id}))}else if(i.group.parent&&!o){let l=i.group.parent.getToast(),c=this.toastRefs.findIndex(p=>p.getToast().id===l.id),d=this.toasts.findIndex(p=>p.id===l.id);if(c>-1&&d>-1){this.toastRefs[c].groupRefs.push(r);let p=this.toasts[c].group??{},_=this.toasts[c].group?.children??[];_.push({options:Me(I({},i),{type:i.type,message:i.message})}),p.children=_,this.toasts[c].group=I({},p),this.cdr.markForCheck(),this._onGroupRefAttached.next({groupRefs:a,id:l.id})}}}})}createGroupRefs(i,r){return new Promise(a=>{let l=i.group.children.map(c=>new Promise(d=>{c.options.group={parent:r},setTimeout(()=>{try{let p=this.toastService.show(c.options.message,c.options,!0);d(p)}catch(p){console.error("Error creating toast",p),d(null)}})}));Promise.all(l).then(c=>a(c))})}closeToast(i){if(i){let r=this.hotToastComponentList.find(o=>o.toast.id===i);r&&(r.close(),this.cdr.markForCheck())}else this.hotToastComponentList.forEach(r=>r.close()),this.cdr.markForCheck()}beforeClosed(i){i.visible=!1,this.cdr.markForCheck()}afterClosed(i){this.toasts.findIndex(o=>o.id===i.id)>-1&&(this._onClosed.next(i),this.toasts=this.toasts.filter(o=>o.id!==i.id),this.toastRefs=this.toastRefs.filter(o=>o.getToast().id!==i.id),this.cdr.markForCheck())}toggleGroup(i){let r=this.toastRefs.findIndex(o=>o.getToast().id===i.id);r>-1&&(this._onGroupToggle.next(i),this.toastRefs[r].groupExpanded=i.event==="expand",this.cdr.markForCheck())}hasToast(i){return this.toasts.findIndex(r=>r.id===i)>-1}showAllToasts(i){this.isShowingAllToasts=i}getAfterClosed(i){return this.onClosed$.pipe(ce(r=>r.id===i.id))}getAfterGroupToggled(i){return this.onGroupToggle$.pipe(ce(r=>r.id===i.id))}getAfterGroupRefsAttached(i){return this.onGroupRefAttached$.pipe(ce(r=>r.id===i.id))}updateToasts(i,r){this.toasts=this.toasts.map(o=>I(I({},o),o.id===i.id&&I(I({},i),r))),this.cdr.markForCheck()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["hot-toast-container"]],viewQuery:function(r,o){if(r&1&&ie(nj,5),r&2){let a;j(a=H())&&(o.hotToastComponentList=a)}},inputs:{defaultConfig:"defaultConfig"},decls:11,vars:0,consts:[[1,"hot-toast-container-overlay"],[1,"hot-toast-container-wrapper"],[3,"showAllToasts","height","beforeClosed","afterClosed","toggleGroup","toast","offset","toastRef","toastsAfter","defaultConfig","isShowingAllToasts"]],template:function(r,o){r&1&&(m(0,"div",0),f(1,` + `),m(2,"div",1),f(3,` + `),m(4,"div"),f(5,` + `),Mt(6,Xie,3,1,null,null,o.trackById,!0),h(),f(8,` + `),h(),f(9,` +`),h(),f(10,` +`)),r&2&&(g(6),Et(o.toasts))},dependencies:[nj],styles:[".hot-toast-container-overlay[_ngcontent-%COMP%]{position:fixed;z-index:var(--hot-toast-container-overlay-z-index, 9999);inset:0;pointer-events:none}.hot-toast-container-wrapper[_ngcontent-%COMP%]{position:relative;height:100%}"],changeDetection:0});let t=n;return t})(),Lg=class{constructor(){this.reverseOrder=!1,this.visibleToasts=5,this.stacking="vertical",this.ariaLive="polite",this.role="status",this.position="top-center",this.autoClose=!0,this.theme="toast",this.attributes={},this.info={content:""},this.success={content:""},this.error={content:""},this.loading={content:""},this.blank={content:""},this.warning={content:""}}},sne=t=>typeof t=="function",lne=t=>typeof t=="function"&&!!t.\u0275cmp,cne=(t,n)=>lne(t)?t:sne(t)?t(n):t,Bk=class{constructor(){this.storage="local",this.key="ngxpert/hototast-${id}",this.count=1,this.enabled=!1}},dne=new O("HOT_TOAST_CONTAINER_TOKEN"),Vg=(()=>{let n=class n{constructor(){this._isInitialized=!1,this._defaultGlobalConfig=new Lg,this._defaultPersistConfig=new Bk,this._viewService=u(Vk),this._platformId=u(hl),this._globalConfig=u(Lg,{optional:!0}),this._container=u(dne,{optional:!0}),this._globalConfig&&(this._defaultGlobalConfig=I(I({},this._defaultGlobalConfig),this._globalConfig))}get defaultConfig(){return this._defaultGlobalConfig}set defaultConfig(i){this._defaultGlobalConfig=I(I({},this._defaultGlobalConfig),i),this._componentRef&&this._componentRef.setInput("defaultConfig",this._defaultGlobalConfig)}show(i,r,o){return this.createToast({message:i||this._defaultGlobalConfig.blank.content,type:r?.type??"blank",options:I(I({},this._defaultGlobalConfig),r),skipAttachToParent:o})}error(i,r){return this.createToast({message:i||this._defaultGlobalConfig.error.content,type:"error",options:I(I(I({},this._defaultGlobalConfig),this._defaultGlobalConfig?.error),r)})}success(i,r){return this.createToast({message:i||this._defaultGlobalConfig.success.content,type:"success",options:I(I(I({},this._defaultGlobalConfig),this._defaultGlobalConfig?.success),r)})}loading(i,r){return this.createToast({message:i||this._defaultGlobalConfig.loading.content,type:"loading",options:I(I(I({},this._defaultGlobalConfig),this._defaultGlobalConfig?.loading),r)})}warning(i,r){return this.createToast({message:i||this._defaultGlobalConfig.warning.content,type:"warning",options:I(I(I({},this._defaultGlobalConfig),this._defaultGlobalConfig?.warning),r)})}info(i,r){return this.createToast({message:i||this._defaultGlobalConfig.info.content,type:"info",options:I(I(I({},this._defaultGlobalConfig),this._defaultGlobalConfig?.info),r)})}observe(i){return r=>{let o,a=0,s=i.loading??this._defaultGlobalConfig.loading?.content,l=i.success??this._defaultGlobalConfig.success?.content,c=i.error??this._defaultGlobalConfig.error?.content;return Fn(()=>(s&&(o=this.createLoadingToast(s),a=Date.now()),r.pipe(He(I(I({},l&&{next:d=>{o=this.createOrUpdateToast(i,d,o,"success",a===0?a:Date.now()-a)}}),c&&{error:d=>{o=this.createOrUpdateToast(i,d,o,"error",a===0?a:Date.now()-a)}})))))}}close(i){this._componentRef&&this._componentRef.ref.instance.closeToast(i)}init(){if(!K2(this._platformId))if(this._container){let i=document.querySelector(this._container);i||(console.warn(`No container element found for selector: ${this._container}, using document.body instead as toast container.`),i=document.body),this._componentRef=this._viewService.createComponent(rj).setInput("defaultConfig",this._defaultGlobalConfig).appendTo(i)}else this._componentRef=this._viewService.createComponent(rj).setInput("defaultConfig",this._defaultGlobalConfig).appendTo(document.body)}createOrUpdateToast(i,r,o,a,s){try{let l=null,c={};if({content:l,options:c}=this.getContentAndOptions(a,i[a]||(this._defaultGlobalConfig[a]?this._defaultGlobalConfig[a].content:"")),l=cne(l,r),o){c.data&&(o.data=c.data),o.updateMessage(l);let d=I(I({type:a,duration:s+ij[a]},c),c.duration&&{duration:s+c.duration});o.updateToast(d)}else this.createToast({message:l,type:a,options:c});return o}catch(l){console.error(l)}}createToast({message:i,type:r,options:o,observableMessages:a,skipAttachToParent:s}){this._isInitialized||(this._isInitialized=!0,this.init());let l=o?.id??`toast-${n.nextId++}`;if(!this.isDuplicate(l)&&(!o.persist?.enabled||o.persist?.enabled&&this.handleStorageValue(l,o))){let c=I({ariaLive:o?.ariaLive??"polite",createdAt:Date.now(),duration:o?.duration??ij[r],id:l,message:i,role:o?.role??"status",type:r,visible:!0,observableMessages:a??void 0},o);return new Ng(c).appendTo(this._componentRef.ref.instance,s)}}isDuplicate(i){return this._componentRef.ref.instance.hasToast(i)}handleStorageValue(i,r){let o=1,a=I(I({},this._defaultPersistConfig),r.persist),s=a.storage==="local"?localStorage:sessionStorage,l=a.key.replace(/\${id}/g,i),c=s.getItem(l);return c?(c=parseInt(c,10),c>0?o=c-1:o=c):o=a.count,s.setItem(l,o.toString()),o}getContentAndOptions(i,r){var s;let o,a=I(I({},this._defaultGlobalConfig),this._defaultGlobalConfig[i]);if(typeof r=="string"||Fg(r)||Cu(r))o=r;else{let l;s=r,{content:o}=s,l=cd(s,["content"]),a=I(I({},a),l)}return{content:o,options:a}}createLoadingToast(i){let r=null,o={};return{content:r,options:o}=this.getContentAndOptions("loading",i),this.loading(r,o)}};n.nextId=0,n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();function lj(t){return Jr([{provide:Lg,useValue:t}])}var Px={REMOVE:"remove",SHOW:"show"},dj=(()=>{let n=class n{constructor(){this.strategiesSource=new rt({}),this.strategies$=this.strategiesSource.asObservable()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),zk=new O("USE_CONFIGURATION_STORE"),Uk=(()=>{let n=class n{constructor(i=!1,r){this.isolate=i,this.configurationStore=r,this.strategiesSource=this.isolate?new rt({}):this.configurationStore.strategiesSource,this.strategies$=this.strategiesSource.asObservable(),this.onAuthorisedDefaultStrategy=this.isolate?void 0:this.configurationStore.onAuthorisedDefaultStrategy,this.onUnAuthorisedDefaultStrategy=this.isolate?void 0:this.configurationStore.onUnAuthorisedDefaultStrategy}setDefaultOnAuthorizedStrategy(i){this.isolate?this.onAuthorisedDefaultStrategy=this.getDefinedStrategy(i):(this.configurationStore.onAuthorisedDefaultStrategy=this.getDefinedStrategy(i),this.onAuthorisedDefaultStrategy=this.configurationStore.onAuthorisedDefaultStrategy)}setDefaultOnUnauthorizedStrategy(i){this.isolate?this.onUnAuthorisedDefaultStrategy=this.getDefinedStrategy(i):(this.configurationStore.onUnAuthorisedDefaultStrategy=this.getDefinedStrategy(i),this.onUnAuthorisedDefaultStrategy=this.configurationStore.onUnAuthorisedDefaultStrategy)}addPermissionStrategy(i,r){this.strategiesSource.value[i]=r}getStrategy(i){return this.strategiesSource.value[i]}getAllStrategies(){return this.strategiesSource.value}getDefinedStrategy(i){if(this.strategiesSource.value[i]||this.isPredefinedStrategy(i))return i;throw new Error(`No ' ${i} ' strategy is found please define one`)}isPredefinedStrategy(i){return i===Px.SHOW||i===Px.REMOVE}};n.\u0275fac=function(r){return new(r||n)(pe(zk),pe(dj))},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})();function ho(t){return typeof t=="function"}function Hk(t){if(Object.prototype.toString.call(t)!=="[object Object]")return!1;{let n=Object.getPrototypeOf(t);return n===null||n===Object.prototype}}function uj(t){return!!t&&typeof t=="string"}function Fx(t){return typeof t=="boolean"}function une(t){return Object.prototype.toString.call(t)==="[object Promise]"}function Rx(t){return Array.isArray(t)?t.length>0:!!t}function Nx(t){return uj(t)?[t]:t}var mj=(()=>{let n=class n{constructor(){this.permissionsSource=new rt({}),this.permissions$=this.permissionsSource.asObservable()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),$k=new O("USE_PERMISSIONS_STORE"),wu=(()=>{let n=class n{constructor(i=!1,r){this.isolate=i,this.permissionsStore=r,this.permissionsSource=this.isolate?new rt({}):this.permissionsStore.permissionsSource,this.permissions$=this.permissionsSource.asObservable()}flushPermissions(){this.permissionsSource.next({})}hasPermission(i){return!i||Array.isArray(i)&&i.length===0?Promise.resolve(!0):(i=Nx(i),this.hasArrayPermission(i))}loadPermissions(i,r){let o=i.reduce((a,s)=>this.reducePermission(a,s,r),{});this.permissionsSource.next(o)}addPermission(i,r){if(Array.isArray(i)){let o=i.reduce((a,s)=>this.reducePermission(a,s,r),this.permissionsSource.value);this.permissionsSource.next(o)}else{let o=this.reducePermission(this.permissionsSource.value,i,r);this.permissionsSource.next(o)}}removePermission(i){let r=I({},this.permissionsSource.value);delete r[i],this.permissionsSource.next(r)}getPermission(i){return this.permissionsSource.value[i]}getPermissions(){return this.permissionsSource.value}reducePermission(i,r,o){return o&&ho(o)?Me(I({},i),{[r]:{name:r,validationFunction:o}}):Me(I({},i),{[r]:{name:r}})}hasArrayPermission(i){let r=i.map(o=>{if(this.hasPermissionValidationFunction(o)){let a=this.permissionsSource.value[o].validationFunction,s=I({},this.permissionsSource.value);return Q(null).pipe(se(()=>a(o,s)),je(l=>Fx(l)?Q(l):l),ei(()=>Q(!1)))}return Q(!!this.permissionsSource.value[o])});return Ut(r).pipe(xo(),xn(o=>o!==!1,!1),se(o=>o!==!1)).toPromise().then(o=>o)}hasPermissionValidationFunction(i){return!!this.permissionsSource.value[i]&&!!this.permissionsSource.value[i].validationFunction&&ho(this.permissionsSource.value[i].validationFunction)}};n.\u0275fac=function(r){return new(r||n)(pe($k),pe(mj))},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),Lx=class{constructor(){this.rolesSource=new rt({}),this.roles$=this.rolesSource.asObservable()}},Wk=new O("USE_ROLES_STORE"),Ph=(()=>{let n=class n{constructor(i=!1,r,o){this.isolate=i,this.rolesStore=r,this.permissionsService=o,this.rolesSource=this.isolate?new rt({}):this.rolesStore.rolesSource,this.roles$=this.rolesSource.asObservable()}addRole(i,r){let o=Me(I({},this.rolesSource.value),{[i]:{name:i,validationFunction:r}});this.rolesSource.next(o)}addRoleWithPermissions(i,r){this.permissionsService.addPermission(r),this.addRole(i,r)}addRoles(i){Object.keys(i).forEach((r,o)=>{this.addRole(r,i[r])})}addRolesWithPermissions(i){Object.keys(i).forEach((r,o)=>{this.addRoleWithPermissions(r,i[r])})}flushRoles(){this.rolesSource.next({})}flushRolesAndPermissions(){this.flushRoles(),this.permissionsService.flushPermissions()}removeRole(i){let r=I({},this.rolesSource.value);delete r[i],this.rolesSource.next(r)}getRoles(){return this.rolesSource.value}getRole(i){return this.rolesSource.value[i]}hasOnlyRoles(i){return!i||Array.isArray(i)&&i.length===0?Promise.resolve(!0):(i=Nx(i),Promise.all([this.hasRoleKey(i),this.hasRolePermission(this.rolesSource.value,i)]).then(([o,a])=>o||a))}hasRoleKey(i){let r=i.map(o=>{if(!!this.rolesSource.value[o]&&!!this.rolesSource.value[o].validationFunction&&ho(this.rolesSource.value[o].validationFunction)&&!une(this.rolesSource.value[o].validationFunction)){let s=this.rolesSource.value[o].validationFunction,l=I({},this.rolesSource.value);return Q(null).pipe(se(()=>s(o,l)),je(c=>Fx(c)?Q(c):c),ei(()=>Q(!1)))}return Q(!1)});return Ut(r).pipe(xo(),xn(o=>o!==!1,!1),se(o=>o!==!1)).toPromise().then(o=>o)}hasRolePermission(i,r){return Ut(r).pipe(Vt(o=>i[o]&&Array.isArray(i[o].validationFunction)?Ut(i[o].validationFunction).pipe(Vt(a=>this.permissionsService.hasPermission(a)),tD(a=>a===!0)):Q(!1)),xn(o=>o===!0,!1)).toPromise()}};n.\u0275fac=function(r){return new(r||n)(pe(Wk),pe(Lx),pe(wu))},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),Fh=(()=>{let n=class n{constructor(){this.permissionsAuthorized=new U,this.permissionsUnauthorized=new U,this.firstMergeUnusedRun=1,this.permissionsService=u(wu),this.configurationService=u(Uk),this.rolesService=u(Ph),this.viewContainer=u(st),this.changeDetector=u(ye),this.templateRef=u(te)}ngOnInit(){this.viewContainer.clear(),this.initPermissionSubscription=this.validateExceptOnlyPermissions()}ngOnChanges(i){let r=i.ngxPermissionsOnly,o=i.ngxPermissionsExcept;if(r||o){if(r&&r.firstChange||o&&o.firstChange)return;it(this.permissionsService.permissions$,this.rolesService.roles$).pipe(us(this.firstMergeUnusedRun),mt(1)).subscribe(()=>{if(Rx(this.ngxPermissionsExcept)){this.validateExceptAndOnlyPermissions();return}if(Rx(this.ngxPermissionsOnly)){this.validateOnlyPermissions();return}this.handleAuthorisedPermission(this.getAuthorisedTemplates())})}}ngOnDestroy(){this.initPermissionSubscription&&this.initPermissionSubscription.unsubscribe()}validateExceptOnlyPermissions(){return it(this.permissionsService.permissions$,this.rolesService.roles$).pipe(us(this.firstMergeUnusedRun)).subscribe(()=>{if(Rx(this.ngxPermissionsExcept)){this.validateExceptAndOnlyPermissions();return}if(Rx(this.ngxPermissionsOnly)){this.validateOnlyPermissions();return}this.handleAuthorisedPermission(this.getAuthorisedTemplates())})}validateExceptAndOnlyPermissions(){Promise.all([this.permissionsService.hasPermission(this.ngxPermissionsExcept),this.rolesService.hasOnlyRoles(this.ngxPermissionsExcept)]).then(([i,r])=>{if(i||r){this.handleUnauthorisedPermission(this.ngxPermissionsExceptElse||this.ngxPermissionsElse);return}if(this.ngxPermissionsOnly)throw!1;this.handleAuthorisedPermission(this.ngxPermissionsExceptThen||this.ngxPermissionsThen||this.templateRef)}).catch(()=>{this.ngxPermissionsOnly?this.validateOnlyPermissions():this.handleAuthorisedPermission(this.ngxPermissionsExceptThen||this.ngxPermissionsThen||this.templateRef)})}validateOnlyPermissions(){Promise.all([this.permissionsService.hasPermission(this.ngxPermissionsOnly),this.rolesService.hasOnlyRoles(this.ngxPermissionsOnly)]).then(([i,r])=>{i||r?this.handleAuthorisedPermission(this.ngxPermissionsOnlyThen||this.ngxPermissionsThen||this.templateRef):this.handleUnauthorisedPermission(this.ngxPermissionsOnlyElse||this.ngxPermissionsElse)}).catch(()=>{this.handleUnauthorisedPermission(this.ngxPermissionsOnlyElse||this.ngxPermissionsElse)})}handleUnauthorisedPermission(i){if(!(Fx(this.currentAuthorizedState)&&!this.currentAuthorizedState)){if(this.currentAuthorizedState=!1,this.permissionsUnauthorized.emit(),this.getUnAuthorizedStrategyInput()){this.applyStrategyAccordingToStrategyType(this.getUnAuthorizedStrategyInput());return}this.configurationService.onUnAuthorisedDefaultStrategy&&!this.elseBlockDefined()?this.applyStrategy(this.configurationService.onUnAuthorisedDefaultStrategy):this.showTemplateBlockInView(i)}}handleAuthorisedPermission(i){if(!(Fx(this.currentAuthorizedState)&&this.currentAuthorizedState)){if(this.currentAuthorizedState=!0,this.permissionsAuthorized.emit(),this.getAuthorizedStrategyInput()){this.applyStrategyAccordingToStrategyType(this.getAuthorizedStrategyInput());return}this.configurationService.onAuthorisedDefaultStrategy&&!this.thenBlockDefined()?this.applyStrategy(this.configurationService.onAuthorisedDefaultStrategy):this.showTemplateBlockInView(i)}}applyStrategyAccordingToStrategyType(i){if(uj(i)){this.applyStrategy(i);return}if(ho(i)){this.showTemplateBlockInView(this.templateRef),i(this.templateRef);return}}showTemplateBlockInView(i){this.viewContainer.clear(),i&&(this.viewContainer.createEmbeddedView(i),this.changeDetector.markForCheck())}getAuthorisedTemplates(){return this.ngxPermissionsOnlyThen||this.ngxPermissionsExceptThen||this.ngxPermissionsThen||this.templateRef}elseBlockDefined(){return!!this.ngxPermissionsExceptElse||!!this.ngxPermissionsElse}thenBlockDefined(){return!!this.ngxPermissionsExceptThen||!!this.ngxPermissionsThen}getAuthorizedStrategyInput(){return this.ngxPermissionsOnlyAuthorisedStrategy||this.ngxPermissionsExceptAuthorisedStrategy||this.ngxPermissionsAuthorisedStrategy}getUnAuthorizedStrategyInput(){return this.ngxPermissionsOnlyUnauthorisedStrategy||this.ngxPermissionsExceptUnauthorisedStrategy||this.ngxPermissionsUnauthorisedStrategy}applyStrategy(i){if(i===Px.SHOW){this.showTemplateBlockInView(this.templateRef);return}if(i===Px.REMOVE){this.viewContainer.clear();return}let r=this.configurationService.getStrategy(i);this.showTemplateBlockInView(this.templateRef),r(this.templateRef)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ngxPermissionsOnly",""],["","ngxPermissionsExcept",""]],inputs:{ngxPermissionsOnly:"ngxPermissionsOnly",ngxPermissionsOnlyThen:"ngxPermissionsOnlyThen",ngxPermissionsOnlyElse:"ngxPermissionsOnlyElse",ngxPermissionsExcept:"ngxPermissionsExcept",ngxPermissionsExceptElse:"ngxPermissionsExceptElse",ngxPermissionsExceptThen:"ngxPermissionsExceptThen",ngxPermissionsThen:"ngxPermissionsThen",ngxPermissionsElse:"ngxPermissionsElse",ngxPermissionsOnlyAuthorisedStrategy:"ngxPermissionsOnlyAuthorisedStrategy",ngxPermissionsOnlyUnauthorisedStrategy:"ngxPermissionsOnlyUnauthorisedStrategy",ngxPermissionsExceptUnauthorisedStrategy:"ngxPermissionsExceptUnauthorisedStrategy",ngxPermissionsExceptAuthorisedStrategy:"ngxPermissionsExceptAuthorisedStrategy",ngxPermissionsUnauthorisedStrategy:"ngxPermissionsUnauthorisedStrategy",ngxPermissionsAuthorisedStrategy:"ngxPermissionsAuthorisedStrategy"},outputs:{permissionsAuthorized:"permissionsAuthorized",permissionsUnauthorized:"permissionsUnauthorized"},standalone:!1,features:[Oe]});let t=n;return t})(),mne="default";var cj=(()=>{let n=class n{constructor(i,r,o){this.permissionsService=i,this.rolesService=r,this.router=o}canActivate(i,r){return this.hasPermissions(i,r)}canActivateChild(i,r){return this.hasPermissions(i,r)}canLoad(i){return this.hasPermissions(i)}canMatch(i){return this.hasPermissions(i)}hasPermissions(i,r){let o=i&&i.data?i.data.permissions:{},a=this.transformPermission(o,i,r);return this.isParameterAvailable(a.except)?this.passingExceptPermissionsValidation(a,i,r):this.isParameterAvailable(a.only)?this.passingOnlyPermissionsValidation(a,i,r):!0}transformPermission(i,r,o){let a=ho(i.only)?i.only(r,o):Nx(i.only),s=ho(i.except)?i.except(r,o):Nx(i.except),l=i.redirectTo;return{only:a,except:s,redirectTo:l}}isParameterAvailable(i){return!!i&&i.length>0}passingExceptPermissionsValidation(i,r,o){if(i.redirectTo&&(ho(i.redirectTo)||Hk(i.redirectTo)&&!this.isRedirectionWithParameters(i.redirectTo))){let a="";return Ut(i.except).pipe(Vt(s=>cs([this.permissionsService.hasPermission(s),this.rolesService.hasOnlyRoles(s)]).pipe(He(l=>{l.every(d=>d===!1)||(a=s)}))),xn(s=>s.some(l=>l===!0),!1),Vt(s=>a?(this.handleRedirectOfFailedPermission(i,a,r,o),Q(!1)):!s&&i.only?this.onlyRedirectCheck(i,r,o):Q(!s))).toPromise()}return Promise.all([this.permissionsService.hasPermission(i.except),this.rolesService.hasOnlyRoles(i.except)]).then(([a,s])=>a||s?(i.redirectTo&&this.redirectToAnotherRoute(i.redirectTo,r,o),!1):i.only?this.checkOnlyPermissions(i,r,o):!0)}redirectToAnotherRoute(i,r,o,a){let s=ho(i)?i(a,r,o):i;if(this.isRedirectionWithParameters(s)){s.navigationCommands=this.transformNavigationCommands(s.navigationCommands,r,o),s.navigationExtras=this.transformNavigationExtras(s.navigationExtras,r,o),this.router.navigate(s.navigationCommands,s.navigationExtras);return}Array.isArray(s)?this.router.navigate(s):this.router.navigate([s])}isRedirectionWithParameters(i){return Hk(i)&&(!!i.navigationCommands||!!i.navigationExtras)}transformNavigationCommands(i,r,o){return ho(i)?i(r,o):i}transformNavigationExtras(i,r,o){return ho(i)?i(r,o):i}onlyRedirectCheck(i,r,o){let a="";return Ut(i.only).pipe(Vt(s=>cs([this.permissionsService.hasPermission(s),this.rolesService.hasOnlyRoles(s)]).pipe(He(l=>{l.every(d=>d===!1)&&(a=s)}))),xn(s=>ho(i.redirectTo)?s.some(l=>l===!0):s.every(l=>l===!1),!1),Vt(s=>ho(i.redirectTo)?s?Q(!0):(this.handleRedirectOfFailedPermission(i,a,r,o),Q(!1)):(a&&this.handleRedirectOfFailedPermission(i,a,r,o),Q(!s)))).toPromise()}handleRedirectOfFailedPermission(i,r,o,a){this.isFailedPermissionPropertyOfRedirectTo(i,r)?this.redirectToAnotherRoute(i.redirectTo[r],o,a,r):ho(i.redirectTo)?this.redirectToAnotherRoute(i.redirectTo,o,a,r):this.redirectToAnotherRoute(i.redirectTo[mne],o,a,r)}isFailedPermissionPropertyOfRedirectTo(i,r){return!!i.redirectTo&&i.redirectTo[r]}checkOnlyPermissions(i,r,o){let a=I({},i);return Promise.all([this.permissionsService.hasPermission(a.only),this.rolesService.hasOnlyRoles(a.only)]).then(([s,l])=>s||l?!0:(a.redirectTo&&this.redirectToAnotherRoute(a.redirectTo,r,o),!1))}passingOnlyPermissionsValidation(i,r,o){return ho(i.redirectTo)||Hk(i.redirectTo)&&!this.isRedirectionWithParameters(i.redirectTo)?this.onlyRedirectCheck(i,r,o):this.checkOnlyPermissions(i,r,o)}};n.\u0275fac=function(r){return new(r||n)(pe(wu),pe(Ph),pe(Ae))},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})();var Bc=(()=>{let n=class n{static forRoot(i={}){return{ngModule:n,providers:[mj,Lx,dj,wu,cj,Ph,Uk,{provide:$k,useValue:i.permissionsIsolate},{provide:Wk,useValue:i.rolesIsolate},{provide:zk,useValue:i.configurationIsolate}]}}static forChild(i={}){return{ngModule:n,providers:[{provide:$k,useValue:i.permissionsIsolate},{provide:Wk,useValue:i.rolesIsolate},{provide:zk,useValue:i.configurationIsolate},Uk,wu,Ph,cj]}}};n.\u0275fac=function(r){return new(r||n)},n.\u0275mod=ee({type:n}),n.\u0275inj=J({});let t=n;return t})();function Hg(t){return t+.5|0}var jc=(t,n,e)=>Math.max(Math.min(t,e),n);function Bg(t){return jc(Hg(t*2.55),0,255)}function Hc(t){return jc(Hg(t*255),0,255)}function Al(t){return jc(Hg(t/2.55)/100,0,1)}function hj(t){return jc(Hg(t*100),0,100)}var da={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Yk=[..."0123456789ABCDEF"],hne=t=>Yk[t&15],pne=t=>Yk[(t&240)>>4]+Yk[t&15],Vx=t=>(t&240)>>4===(t&15),fne=t=>Vx(t.r)&&Vx(t.g)&&Vx(t.b)&&Vx(t.a);function gne(t){var n=t.length,e;return t[0]==="#"&&(n===4||n===5?e={r:255&da[t[1]]*17,g:255&da[t[2]]*17,b:255&da[t[3]]*17,a:n===5?da[t[4]]*17:255}:(n===7||n===9)&&(e={r:da[t[1]]<<4|da[t[2]],g:da[t[3]]<<4|da[t[4]],b:da[t[5]]<<4|da[t[6]],a:n===9?da[t[7]]<<4|da[t[8]]:255})),e}var _ne=(t,n)=>t<255?n(t):"";function bne(t){var n=fne(t)?hne:pne;return t?"#"+n(t.r)+n(t.g)+n(t.b)+_ne(t.a,n):void 0}var vne=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function _j(t,n,e){let i=n*Math.min(e,1-e),r=(o,a=(o+t/30)%12)=>e-i*Math.max(Math.min(a-3,9-a,1),-1);return[r(0),r(8),r(4)]}function yne(t,n,e){let i=(r,o=(r+t/60)%6)=>e-e*n*Math.max(Math.min(o,4-o,1),0);return[i(5),i(3),i(1)]}function xne(t,n,e){let i=_j(t,1,.5),r;for(n+e>1&&(r=1/(n+e),n*=r,e*=r),r=0;r<3;r++)i[r]*=1-n-e,i[r]+=n;return i}function Cne(t,n,e,i,r){return t===r?(n-e)/i+(n.5?d/(2-o-a):d/(o+a),l=Cne(e,i,r,d,o),l=l*60+.5),[l|0,c||0,s]}function Kk(t,n,e,i){return(Array.isArray(n)?t(n[0],n[1],n[2]):t(n,e,i)).map(Hc)}function Zk(t,n,e){return Kk(_j,t,n,e)}function wne(t,n,e){return Kk(xne,t,n,e)}function Dne(t,n,e){return Kk(yne,t,n,e)}function bj(t){return(t%360+360)%360}function Mne(t){let n=vne.exec(t),e=255,i;if(!n)return;n[5]!==i&&(e=n[6]?Bg(+n[5]):Hc(+n[5]));let r=bj(+n[2]),o=+n[3]/100,a=+n[4]/100;return n[1]==="hwb"?i=wne(r,o,a):n[1]==="hsv"?i=Dne(r,o,a):i=Zk(r,o,a),{r:i[0],g:i[1],b:i[2],a:e}}function Ene(t,n){var e=Qk(t);e[0]=bj(e[0]+n),e=Zk(e),t.r=e[0],t.g=e[1],t.b=e[2]}function Sne(t){if(!t)return;let n=Qk(t),e=n[0],i=hj(n[1]),r=hj(n[2]);return t.a<255?`hsla(${e}, ${i}%, ${r}%, ${Al(t.a)})`:`hsl(${e}, ${i}%, ${r}%)`}var pj={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},fj={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function kne(){let t={},n=Object.keys(fj),e=Object.keys(pj),i,r,o,a,s;for(i=0;i>16&255,o>>8&255,o&255]}return t}var Bx;function Tne(t){Bx||(Bx=kne(),Bx.transparent=[0,0,0,0]);let n=Bx[t.toLowerCase()];return n&&{r:n[0],g:n[1],b:n[2],a:n.length===4?n[3]:255}}var Ine=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function Ane(t){let n=Ine.exec(t),e=255,i,r,o;if(n){if(n[7]!==i){let a=+n[7];e=n[8]?Bg(a):jc(a*255,0,255)}return i=+n[1],r=+n[3],o=+n[5],i=255&(n[2]?Bg(i):jc(i,0,255)),r=255&(n[4]?Bg(r):jc(r,0,255)),o=255&(n[6]?Bg(o):jc(o,0,255)),{r:i,g:r,b:o,a:e}}}function One(t){return t&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${Al(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`)}var qk=t=>t<=.0031308?t*12.92:Math.pow(t,1/2.4)*1.055-.055,Nh=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Rne(t,n,e){let i=Nh(Al(t.r)),r=Nh(Al(t.g)),o=Nh(Al(t.b));return{r:Hc(qk(i+e*(Nh(Al(n.r))-i))),g:Hc(qk(r+e*(Nh(Al(n.g))-r))),b:Hc(qk(o+e*(Nh(Al(n.b))-o))),a:t.a+e*(n.a-t.a)}}function jx(t,n,e){if(t){let i=Qk(t);i[n]=Math.max(0,Math.min(i[n]+i[n]*e,n===0?360:1)),i=Zk(i),t.r=i[0],t.g=i[1],t.b=i[2]}}function vj(t,n){return t&&Object.assign(n||{},t)}function gj(t){var n={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(n={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(n.a=Hc(t[3]))):(n=vj(t,{r:0,g:0,b:0,a:1}),n.a=Hc(n.a)),n}function Pne(t){return t.charAt(0)==="r"?Ane(t):Mne(t)}var jg=class t{constructor(n){if(n instanceof t)return n;let e=typeof n,i;e==="object"?i=gj(n):e==="string"&&(i=gne(n)||Tne(n)||Pne(n)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var n=vj(this._rgb);return n&&(n.a=Al(n.a)),n}set rgb(n){this._rgb=gj(n)}rgbString(){return this._valid?One(this._rgb):void 0}hexString(){return this._valid?bne(this._rgb):void 0}hslString(){return this._valid?Sne(this._rgb):void 0}mix(n,e){if(n){let i=this.rgb,r=n.rgb,o,a=e===o?.5:e,s=2*a-1,l=i.a-r.a,c=((s*l===-1?s:(s+l)/(1+s*l))+1)/2;o=1-c,i.r=255&c*i.r+o*r.r+.5,i.g=255&c*i.g+o*r.g+.5,i.b=255&c*i.b+o*r.b+.5,i.a=a*i.a+(1-a)*r.a,this.rgb=i}return this}interpolate(n,e){return n&&(this._rgb=Rne(this._rgb,n._rgb,e)),this}clone(){return new t(this.rgb)}alpha(n){return this._rgb.a=Hc(n),this}clearer(n){let e=this._rgb;return e.a*=1-n,this}greyscale(){let n=this._rgb,e=Hg(n.r*.3+n.g*.59+n.b*.11);return n.r=n.g=n.b=e,this}opaquer(n){let e=this._rgb;return e.a*=1+n,this}negate(){let n=this._rgb;return n.r=255-n.r,n.g=255-n.g,n.b=255-n.b,this}lighten(n){return jx(this._rgb,2,n),this}darken(n){return jx(this._rgb,2,-n),this}saturate(n){return jx(this._rgb,1,n),this}desaturate(n){return jx(this._rgb,1,-n),this}rotate(n){return Ene(this._rgb,n),this}};function qs(){}var Ij=(()=>{let t=0;return()=>t++})();function Nt(t){return t==null}function Ri(t){if(Array.isArray&&Array.isArray(t))return!0;let n=Object.prototype.toString.call(t);return n.slice(0,7)==="[object"&&n.slice(-6)==="Array]"}function Bt(t){return t!==null&&Object.prototype.toString.call(t)==="[object Object]"}function Xi(t){return(typeof t=="number"||t instanceof Number)&&isFinite(+t)}function po(t,n){return Xi(t)?t:n}function ut(t,n){return typeof t>"u"?n:t}var Aj=(t,n)=>typeof t=="string"&&t.endsWith("%")?parseFloat(t)/100:+t/n,tT=(t,n)=>typeof t=="string"&&t.endsWith("%")?parseFloat(t)/100*n:+t;function ki(t,n,e){if(t&&typeof t.call=="function")return t.apply(e,n)}function ui(t,n,e,i){let r,o,a;if(Ri(t))if(o=t.length,i)for(r=o-1;r>=0;r--)n.call(e,t[r],r);else for(r=0;rt,x:t=>t.x,y:t=>t.y};function Lne(t){let n=t.split("."),e=[],i="";for(let r of n)i+=r,i.endsWith("\\")?i=i.slice(0,-1)+".":(e.push(i),i="");return e}function Vne(t){let n=Lne(t);return e=>{for(let i of n){if(i==="")break;e=e&&e[i]}return e}}function Pl(t,n){return(yj[n]||(yj[n]=Vne(n)))(t)}function Yx(t){return t.charAt(0).toUpperCase()+t.slice(1)}var Hh=t=>typeof t<"u",Ol=t=>typeof t=="function",iT=(t,n)=>{if(t.size!==n.size)return!1;for(let e of t)if(!n.has(e))return!1;return!0};function Rj(t){return t.type==="mouseup"||t.type==="click"||t.type==="contextmenu"}var Jt=Math.PI,Pi=2*Jt,Bne=Pi+Jt,Wx=Number.POSITIVE_INFINITY,jne=Jt/180,rn=Jt/2,Du=Jt/4,xj=Jt*2/3,Rl=Math.log10,ns=Math.sign;function zh(t,n,e){return Math.abs(t-n)r-o).pop(),n}function Hne(t){return typeof t=="symbol"||typeof t=="object"&&t!==null&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}function Su(t){return!Hne(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function Fj(t,n){let e=Math.round(t);return e-n<=t&&e+n>=t}function rT(t,n,e){let i,r,o;for(i=0,r=t.length;il&&c=Math.min(n,e)-i&&t<=Math.max(n,e)+i}function Kx(t,n,e){e=e||(a=>t[a]1;)o=r+i>>1,e(o)?r=o:i=o;return{lo:r,hi:i}}var Ws=(t,n,e,i)=>Kx(t,e,i?r=>{let o=t[r][n];return ot[r][n]Kx(t,e,i=>t[i][n]>=e);function Vj(t,n,e){let i=0,r=t.length;for(;ii&&t[r-1]>e;)r--;return i>0||r{let i="_onData"+Yx(e),r=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...o){let a=r.apply(this,o);return t._chartjs.listeners.forEach(s=>{typeof s[i]=="function"&&s[i](...o)}),a}})})}function sT(t,n){let e=t._chartjs;if(!e)return;let i=e.listeners,r=i.indexOf(n);r!==-1&&i.splice(r,1),!(i.length>0)&&(Bj.forEach(o=>{delete t[o]}),delete t._chartjs)}function lT(t){let n=new Set(t);return n.size===t.length?t:Array.from(n)}var cT=(function(){return typeof window>"u"?function(t){return t()}:window.requestAnimationFrame})();function dT(t,n){let e=[],i=!1;return function(...r){e=r,i||(i=!0,cT.call(window,()=>{i=!1,t.apply(n,e)}))}}function Hj(t,n){let e;return function(...i){return n?(clearTimeout(e),e=setTimeout(t,n,i)):t.apply(this,i),n}}var Zx=t=>t==="start"?"left":t==="end"?"right":"center",fr=(t,n,e)=>t==="start"?n:t==="end"?e:(n+e)/2,zj=(t,n,e,i)=>t===(i?"left":"right")?e:t==="center"?(n+e)/2:n;function uT(t,n,e){let i=n.length,r=0,o=i;if(t._sorted){let{iScale:a,vScale:s,_parsed:l}=t,c=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null,d=a.axis,{min:p,max:_,minDefined:b,maxDefined:y}=a.getUserBounds();if(b){if(r=Math.min(Ws(l,d,p).lo,e?i:Ws(n,d,a.getPixelForValue(p)).lo),c){let w=l.slice(0,r+1).reverse().findIndex(C=>!Nt(C[s.axis]));r-=Math.max(0,w)}r=On(r,0,i-1)}if(y){let w=Math.max(Ws(l,a.axis,_,!0).hi+1,e?0:Ws(n,d,a.getPixelForValue(_),!0).hi+1);if(c){let C=l.slice(w-1).findIndex(D=>!Nt(D[s.axis]));w+=Math.max(0,C)}o=On(w,r,i)-r}else o=i-r}return{start:r,count:o}}function mT(t){let{xScale:n,yScale:e,_scaleRanges:i}=t,r={xmin:n.min,xmax:n.max,ymin:e.min,ymax:e.max};if(!i)return t._scaleRanges=r,!0;let o=i.xmin!==n.min||i.xmax!==n.max||i.ymin!==e.min||i.ymax!==e.max;return Object.assign(i,r),o}var Hx=t=>t===0||t===1,Cj=(t,n,e)=>-(Math.pow(2,10*(t-=1))*Math.sin((t-n)*Pi/e)),wj=(t,n,e)=>Math.pow(2,-10*t)*Math.sin((t-n)*Pi/e)+1,Lh={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>-Math.cos(t*rn)+1,easeOutSine:t=>Math.sin(t*rn),easeInOutSine:t=>-.5*(Math.cos(Jt*t)-1),easeInExpo:t=>t===0?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>t===1?1:-Math.pow(2,-10*t)+1,easeInOutExpo:t=>Hx(t)?t:t<.5?.5*Math.pow(2,10*(t*2-1)):.5*(-Math.pow(2,-10*(t*2-1))+2),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>Hx(t)?t:Cj(t,.075,.3),easeOutElastic:t=>Hx(t)?t:wj(t,.075,.3),easeInOutElastic(t){return Hx(t)?t:t<.5?.5*Cj(t*2,.1125,.45):.5+.5*wj(t*2-1,.1125,.45)},easeInBack(t){return t*t*((1.70158+1)*t-1.70158)},easeOutBack(t){return(t-=1)*t*((1.70158+1)*t+1.70158)+1},easeInOutBack(t){let n=1.70158;return(t/=.5)<1?.5*(t*t*(((n*=1.525)+1)*t-n)):.5*((t-=2)*t*(((n*=1.525)+1)*t+n)+2)},easeInBounce:t=>1-Lh.easeOutBounce(1-t),easeOutBounce(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:t=>t<.5?Lh.easeInBounce(t*2)*.5:Lh.easeOutBounce(t*2-1)*.5+.5};function hT(t){if(t&&typeof t=="object"){let n=t.toString();return n==="[object CanvasPattern]"||n==="[object CanvasGradient]"}return!1}function pT(t){return hT(t)?t:new jg(t)}function Xk(t){return hT(t)?t:new jg(t).saturate(.5).darken(.1).hexString()}var Une=["x","y","borderWidth","radius","tension"],$ne=["color","borderColor","backgroundColor"];function Wne(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:n=>n!=="onProgress"&&n!=="onComplete"&&n!=="fn"}),t.set("animations",{colors:{type:"color",properties:$ne},numbers:{type:"number",properties:Une}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:n=>n|0}}}})}function Gne(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var Dj=new Map;function qne(t,n){n=n||{};let e=t+JSON.stringify(n),i=Dj.get(e);return i||(i=new Intl.NumberFormat(t,n),Dj.set(e,i)),i}function $h(t,n,e){return qne(n,e).format(t)}var Uj={values(t){return Ri(t)?t:""+t},numeric(t,n,e){if(t===0)return"0";let i=this.chart.options.locale,r,o=t;if(e.length>1){let c=Math.max(Math.abs(e[0].value),Math.abs(e[e.length-1].value));(c<1e-4||c>1e15)&&(r="scientific"),o=Yne(t,e)}let a=Rl(Math.abs(o)),s=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:r,minimumFractionDigits:s,maximumFractionDigits:s};return Object.assign(l,this.options.ticks.format),$h(t,i,l)},logarithmic(t,n,e){if(t===0)return"0";let i=e[n].significand||t/Math.pow(10,Math.floor(Rl(t)));return[1,2,3,5,10,15].includes(i)||n>.8*e.length?Uj.numeric.call(this,t,n,e):""}};function Yne(t,n){let e=n.length>3?n[2].value-n[1].value:n[1].value-n[0].value;return Math.abs(e)>=1&&t!==Math.floor(t)&&(e=t-Math.floor(t)),e}var Wg={formatters:Uj};function Qne(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(n,e)=>e.lineWidth,tickColor:(n,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Wg.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:n=>!n.startsWith("before")&&!n.startsWith("after")&&n!=="callback"&&n!=="parser",_indexable:n=>n!=="borderDash"&&n!=="tickBorderDash"&&n!=="dash"}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:n=>n!=="backdropPadding"&&n!=="callback",_indexable:n=>n!=="backdropPadding"})}var $c=Object.create(null),Xx=Object.create(null);function zg(t,n){if(!n)return t;let e=n.split(".");for(let i=0,r=e.length;ii.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(i,r)=>Xk(r.backgroundColor),this.hoverBorderColor=(i,r)=>Xk(r.borderColor),this.hoverColor=(i,r)=>Xk(r.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(n),this.apply(e)}set(n,e){return Jk(this,n,e)}get(n){return zg(this,n)}describe(n,e){return Jk(Xx,n,e)}override(n,e){return Jk($c,n,e)}route(n,e,i,r){let o=zg(this,n),a=zg(this,i),s="_"+e;Object.defineProperties(o,{[s]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){let l=this[s],c=a[r];return Bt(l)?Object.assign({},c,l):ut(l,c)},set(l){this[s]=l}}})}apply(n){n.forEach(e=>e(this))}},Ti=new eT({_scriptable:t=>!t.startsWith("on"),_indexable:t=>t!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[Wne,Gne,Qne]);function Kne(t){return!t||Nt(t.size)||Nt(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ug(t,n,e,i,r){let o=n[r];return o||(o=n[r]=t.measureText(r).width,e.push(r)),o>i&&(i=o),i}function $j(t,n,e,i){i=i||{};let r=i.data=i.data||{},o=i.garbageCollect=i.garbageCollect||[];i.font!==n&&(r=i.data={},o=i.garbageCollect=[],i.font=n),t.save(),t.font=n;let a=0,s=e.length,l,c,d,p,_;for(l=0;le.length){for(l=0;l0&&t.stroke()}}function Gs(t,n,e){return e=e||.5,!n||t&&t.x>n.left-e&&t.xn.top-e&&t.y0&&o.strokeColor!=="",l,c;for(t.save(),t.font=r.string,Zne(t,o),l=0;l+t||0;function eC(t,n){let e={},i=Bt(n),r=i?Object.keys(n):n,o=Bt(t)?i?a=>ut(t[a],t[n[a]]):a=>t[a]:()=>t;for(let a of r)e[a]=nre(o(a));return e}function _T(t){return eC(t,{top:"y",right:"x",bottom:"y",left:"x"})}function qc(t){return eC(t,["topLeft","topRight","bottomLeft","bottomRight"])}function gr(t){let n=_T(t);return n.width=n.left+n.right,n.height=n.top+n.bottom,n}function vn(t,n){t=t||{},n=n||Ti.font;let e=ut(t.size,n.size);typeof e=="string"&&(e=parseInt(e,10));let i=ut(t.style,n.style);i&&!(""+i).match(tre)&&(console.warn('Invalid font style specified: "'+i+'"'),i=void 0);let r={family:ut(t.family,n.family),lineHeight:ire(ut(t.lineHeight,n.lineHeight),e),size:e,style:i,weight:ut(t.weight,n.weight),string:""};return r.string=Kne(r),r}function Gh(t,n,e,i){let r=!0,o,a,s;for(o=0,a=t.length;oe&&s===0?0:s+l;return{min:a(i,-Math.abs(o)),max:a(r,o)}}function Fl(t,n){return Object.assign(Object.create(t),n)}function tC(t,n=[""],e,i,r=()=>t[0]){let o=e||t;typeof i>"u"&&(i=Kj("_fallback",t));let a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:i,_getTarget:r,override:s=>tC([s,...t],n,o,i)};return new Proxy(a,{deleteProperty(s,l){return delete s[l],delete s._keys,delete t[0][l],!0},get(s,l){return Yj(s,l,()=>ure(l,n,t,s))},getOwnPropertyDescriptor(s,l){return Reflect.getOwnPropertyDescriptor(s._scopes[0],l)},getPrototypeOf(){return Reflect.getPrototypeOf(t[0])},has(s,l){return Ej(s).includes(l)},ownKeys(s){return Ej(s)},set(s,l,c){let d=s._storage||(s._storage=r());return s[l]=d[l]=c,delete s._keys,!0}})}function Eu(t,n,e,i){let r={_cacheable:!1,_proxy:t,_context:n,_subProxy:e,_stack:new Set,_descriptors:bT(t,i),setContext:o=>Eu(t,o,e,i),override:o=>Eu(t.override(o),n,e,i)};return new Proxy(r,{deleteProperty(o,a){return delete o[a],delete t[a],!0},get(o,a,s){return Yj(o,a,()=>ore(o,a,s))},getOwnPropertyDescriptor(o,a){return o._descriptors.allKeys?Reflect.has(t,a)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,a)},getPrototypeOf(){return Reflect.getPrototypeOf(t)},has(o,a){return Reflect.has(t,a)},ownKeys(){return Reflect.ownKeys(t)},set(o,a,s){return t[a]=s,delete o[a],!0}})}function bT(t,n={scriptable:!0,indexable:!0}){let{_scriptable:e=n.scriptable,_indexable:i=n.indexable,_allKeys:r=n.allKeys}=t;return{allKeys:r,scriptable:e,indexable:i,isScriptable:Ol(e)?e:()=>e,isIndexable:Ol(i)?i:()=>i}}var rre=(t,n)=>t?t+Yx(n):n,vT=(t,n)=>Bt(n)&&t!=="adapters"&&(Object.getPrototypeOf(n)===null||n.constructor===Object);function Yj(t,n,e){if(Object.prototype.hasOwnProperty.call(t,n)||n==="constructor")return t[n];let i=e();return t[n]=i,i}function ore(t,n,e){let{_proxy:i,_context:r,_subProxy:o,_descriptors:a}=t,s=i[n];return Ol(s)&&a.isScriptable(n)&&(s=are(n,s,t,e)),Ri(s)&&s.length&&(s=sre(n,s,t,a.isIndexable)),vT(n,s)&&(s=Eu(s,r,o&&o[n],a)),s}function are(t,n,e,i){let{_proxy:r,_context:o,_subProxy:a,_stack:s}=e;if(s.has(t))throw new Error("Recursion detected: "+Array.from(s).join("->")+"->"+t);s.add(t);let l=n(o,a||i);return s.delete(t),vT(t,l)&&(l=yT(r._scopes,r,t,l)),l}function sre(t,n,e,i){let{_proxy:r,_context:o,_subProxy:a,_descriptors:s}=e;if(typeof o.index<"u"&&i(t))return n[o.index%n.length];if(Bt(n[0])){let l=n,c=r._scopes.filter(d=>d!==l);n=[];for(let d of l){let p=yT(c,r,t,d);n.push(Eu(p,o,a&&a[t],s))}}return n}function Qj(t,n,e){return Ol(t)?t(n,e):t}var lre=(t,n)=>t===!0?n:typeof t=="string"?Pl(n,t):void 0;function cre(t,n,e,i,r){for(let o of n){let a=lre(e,o);if(a){t.add(a);let s=Qj(a._fallback,e,r);if(typeof s<"u"&&s!==e&&s!==i)return s}else if(a===!1&&typeof i<"u"&&e!==i)return null}return!1}function yT(t,n,e,i){let r=n._rootScopes,o=Qj(n._fallback,e,i),a=[...t,...r],s=new Set;s.add(i);let l=Mj(s,a,e,o||e,i);return l===null||typeof o<"u"&&o!==e&&(l=Mj(s,a,o,l,i),l===null)?!1:tC(Array.from(s),[""],r,o,()=>dre(n,e,i))}function Mj(t,n,e,i,r){for(;e;)e=cre(t,n,e,i,r);return e}function dre(t,n,e){let i=t._getTarget();n in i||(i[n]={});let r=i[n];return Ri(r)&&Bt(e)?e:r||{}}function ure(t,n,e,i){let r;for(let o of n)if(r=Kj(rre(o,t),e),typeof r<"u")return vT(t,r)?yT(e,i,t,r):r}function Kj(t,n){for(let e of n){if(!e)continue;let i=e[t];if(typeof i<"u")return i}}function Ej(t){let n=t._keys;return n||(n=t._keys=mre(t._scopes)),n}function mre(t){let n=new Set;for(let e of t)for(let i of Object.keys(e).filter(r=>!r.startsWith("_")))n.add(i);return Array.from(n)}function xT(t,n,e,i){let{iScale:r}=t,{key:o="r"}=this._parsing,a=new Array(i),s,l,c,d;for(s=0,l=i;snt==="x"?"y":"x";function pre(t,n,e,i){let r=t.skip?n:t,o=n,a=e.skip?n:e,s=Gx(o,r),l=Gx(a,o),c=s/(s+l),d=l/(s+l);c=isNaN(c)?0:c,d=isNaN(d)?0:d;let p=i*c,_=i*d;return{previous:{x:o.x-p*(a.x-r.x),y:o.y-p*(a.y-r.y)},next:{x:o.x+_*(a.x-r.x),y:o.y+_*(a.y-r.y)}}}function fre(t,n,e){let i=t.length,r,o,a,s,l,c=Bh(t,0);for(let d=0;d!c.skip)),n.cubicInterpolationMode==="monotone")_re(t,r);else{let c=i?t[t.length-1]:t[0];for(o=0,a=t.length;ot.ownerDocument.defaultView.getComputedStyle(t,null);function vre(t,n){return rC(t).getPropertyValue(n)}var yre=["top","right","bottom","left"];function Mu(t,n,e){let i={};e=e?"-"+e:"";for(let r=0;r<4;r++){let o=yre[r];i[o]=parseFloat(t[n+"-"+o+e])||0}return i.width=i.left+i.right,i.height=i.top+i.bottom,i}var xre=(t,n,e)=>(t>0||n>0)&&(!e||!e.shadowRoot);function Cre(t,n){let e=t.touches,i=e&&e.length?e[0]:t,{offsetX:r,offsetY:o}=i,a=!1,s,l;if(xre(r,o,t.target))s=r,l=o;else{let c=n.getBoundingClientRect();s=i.clientX-c.left,l=i.clientY-c.top,a=!0}return{x:s,y:l,box:a}}function Yc(t,n){if("native"in t)return t;let{canvas:e,currentDevicePixelRatio:i}=n,r=rC(e),o=r.boxSizing==="border-box",a=Mu(r,"padding"),s=Mu(r,"border","width"),{x:l,y:c,box:d}=Cre(t,e),p=a.left+(d&&s.left),_=a.top+(d&&s.top),{width:b,height:y}=n;return o&&(b-=a.width+s.width,y-=a.height+s.height),{x:Math.round((l-p)/b*e.width/i),y:Math.round((c-_)/y*e.height/i)}}function wre(t,n,e){let i,r;if(n===void 0||e===void 0){let o=t&&nC(t);if(!o)n=t.clientWidth,e=t.clientHeight;else{let a=o.getBoundingClientRect(),s=rC(o),l=Mu(s,"border","width"),c=Mu(s,"padding");n=a.width-c.width-l.width,e=a.height-c.height-l.height,i=qx(s.maxWidth,o,"clientWidth"),r=qx(s.maxHeight,o,"clientHeight")}}return{width:n,height:e,maxWidth:i||Wx,maxHeight:r||Wx}}var Uc=t=>Math.round(t*10)/10;function Jj(t,n,e,i){let r=rC(t),o=Mu(r,"margin"),a=qx(r.maxWidth,t,"clientWidth")||Wx,s=qx(r.maxHeight,t,"clientHeight")||Wx,l=wre(t,n,e),{width:c,height:d}=l;if(r.boxSizing==="content-box"){let _=Mu(r,"border","width"),b=Mu(r,"padding");c-=b.width+_.width,d-=b.height+_.height}return c=Math.max(0,c-o.width),d=Math.max(0,i?c/i:d-o.height),c=Uc(Math.min(c,a,l.maxWidth)),d=Uc(Math.min(d,s,l.maxHeight)),c&&!d&&(d=Uc(c/2)),(n!==void 0||e!==void 0)&&i&&l.height&&d>l.height&&(d=l.height,c=Uc(Math.floor(d*i))),{width:c,height:d}}function CT(t,n,e){let i=n||1,r=Uc(t.height*i),o=Uc(t.width*i);t.height=Uc(t.height),t.width=Uc(t.width);let a=t.canvas;return a.style&&(e||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),t.currentDevicePixelRatio!==i||a.height!==r||a.width!==o?(t.currentDevicePixelRatio=i,a.height=r,a.width=o,t.ctx.setTransform(i,0,0,i,0,0),!0):!1}var e4=(function(){let t=!1;try{let n={get passive(){return t=!0,!1}};iC()&&(window.addEventListener("test",null,n),window.removeEventListener("test",null,n))}catch{}return t})();function wT(t,n){let e=vre(t,n),i=e&&e.match(/^(\d+)(\.\d+)?px$/);return i?+i[1]:void 0}function zc(t,n,e,i){return{x:t.x+e*(n.x-t.x),y:t.y+e*(n.y-t.y)}}function t4(t,n,e,i){return{x:t.x+e*(n.x-t.x),y:i==="middle"?e<.5?t.y:n.y:i==="after"?e<1?t.y:n.y:e>0?n.y:t.y}}function i4(t,n,e,i){let r={x:t.cp2x,y:t.cp2y},o={x:n.cp1x,y:n.cp1y},a=zc(t,r,e),s=zc(r,o,e),l=zc(o,n,e),c=zc(a,s,e),d=zc(s,l,e);return zc(c,d,e)}var Dre=function(t,n){return{x(e){return t+t+n-e},setWidth(e){n=e},textAlign(e){return e==="center"?e:e==="right"?"left":"right"},xPlus(e,i){return e-i},leftForLtr(e,i){return e-i}}},Mre=function(){return{x(t){return t},setWidth(t){},textAlign(t){return t},xPlus(t,n){return t+n},leftForLtr(t,n){return t}}};function ku(t,n,e){return t?Dre(n,e):Mre()}function DT(t,n){let e,i;(n==="ltr"||n==="rtl")&&(e=t.canvas.style,i=[e.getPropertyValue("direction"),e.getPropertyPriority("direction")],e.setProperty("direction",n,"important"),t.prevTextDirection=i)}function MT(t,n){n!==void 0&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",n[0],n[1]))}function n4(t){return t==="angle"?{between:Uh,compare:zne,normalize:pr}:{between:Ys,compare:(n,e)=>n-e,normalize:n=>n}}function Sj({start:t,end:n,count:e,loop:i,style:r}){return{start:t%e,end:n%e,loop:i&&(n-t+1)%e===0,style:r}}function Ere(t,n,e){let{property:i,start:r,end:o}=e,{between:a,normalize:s}=n4(i),l=n.length,{start:c,end:d,loop:p}=t,_,b;if(p){for(c+=l,d+=l,_=0,b=l;_l(r,W,D)&&s(r,W)!==0,K=()=>s(o,D)===0||l(o,W,D),oe=()=>w||Z(),Se=()=>!w||K();for(let ve=d,Be=d;ve<=p;++ve)F=n[ve%a],!F.skip&&(D=c(F[i]),D!==W&&(w=l(D,r,o),C===null&&oe()&&(C=s(D,r)===0?ve:Be),C!==null&&Se()&&(y.push(Sj({start:C,end:ve,loop:_,count:a,style:b})),C=null),Be=ve,W=D));return C!==null&&y.push(Sj({start:C,end:p,loop:_,count:a,style:b})),y}function ST(t,n){let e=[],i=t.segments;for(let r=0;rr&&t[o%n].skip;)o--;return o%=n,{start:r,end:o}}function kre(t,n,e,i){let r=t.length,o=[],a=n,s=t[n],l;for(l=n+1;l<=e;++l){let c=t[l%r];c.skip||c.stop?s.skip||(i=!1,o.push({start:n%r,end:(l-1)%r,loop:i}),n=a=c.stop?l:null):(a=l,s.skip&&(n=l)),s=c}return a!==null&&o.push({start:n%r,end:a%r,loop:i}),o}function r4(t,n){let e=t.points,i=t.options.spanGaps,r=e.length;if(!r)return[];let o=!!t._loop,{start:a,end:s}=Sre(e,r,o,i);if(i===!0)return kj(t,[{start:a,end:s,loop:o}],e,n);let l=ss({chart:n,initial:e.initial,numSteps:a,currentStep:Math.min(i-e.start,a)}))}_refresh(){this._request||(this._running=!0,this._request=cT.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(n=Date.now()){let e=0;this._charts.forEach((i,r)=>{if(!i.running||!i.items.length)return;let o=i.items,a=o.length-1,s=!1,l;for(;a>=0;--a)l=o[a],l._active?(l._total>i.duration&&(i.duration=l._total),l.tick(n),s=!0):(o[a]=o[o.length-1],o.pop());s&&(r.draw(),this._notify(r,i,n,"progress")),o.length||(i.running=!1,this._notify(r,i,n,"complete"),i.initial=!1),e+=o.length}),this._lastDate=n,e===0&&(this._running=!1)}_getAnims(n){let e=this._charts,i=e.get(n);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(n,i)),i}listen(n,e,i){this._getAnims(n).listeners[e].push(i)}add(n,e){!e||!e.length||this._getAnims(n).items.push(...e)}has(n){return this._getAnims(n).items.length>0}start(n){let e=this._charts.get(n);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce((i,r)=>Math.max(i,r._duration),0),this._refresh())}running(n){if(!this._running)return!1;let e=this._charts.get(n);return!(!e||!e.running||!e.items.length)}stop(n){let e=this._charts.get(n);if(!e||!e.items.length)return;let i=e.items,r=i.length-1;for(;r>=0;--r)i[r].cancel();e.items=[],this._notify(n,e,Date.now(),"complete")}remove(n){return this._charts.delete(n)}},Nl=new jT,o4="transparent",Ore={boolean(t,n,e){return e>.5?n:t},color(t,n,e){let i=pT(t||o4),r=i.valid&&pT(n||o4);return r&&r.valid?r.mix(i,e).hexString():n},number(t,n,e){return t+(n-t)*e}},HT=class{constructor(n,e,i,r){let o=e[i];r=Gh([n.to,r,o,n.from]);let a=Gh([n.from,o,r]);this._active=!0,this._fn=n.fn||Ore[n.type||typeof a],this._easing=Lh[n.easing]||Lh.linear,this._start=Math.floor(Date.now()+(n.delay||0)),this._duration=this._total=Math.floor(n.duration),this._loop=!!n.loop,this._target=e,this._prop=i,this._from=a,this._to=r,this._promises=void 0}active(){return this._active}update(n,e,i){if(this._active){this._notify(!1);let r=this._target[this._prop],o=i-this._start,a=this._duration-o;this._start=i,this._duration=Math.floor(Math.max(a,n.duration)),this._total+=o,this._loop=!!n.loop,this._to=Gh([n.to,e,r,n.from]),this._from=Gh([n.from,r,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(n){let e=n-this._start,i=this._duration,r=this._prop,o=this._from,a=this._loop,s=this._to,l;if(this._active=o!==s&&(a||e1?2-l:l,l=this._easing(Math.min(1,Math.max(0,l))),this._target[r]=this._fn(o,s,l)}wait(){let n=this._promises||(this._promises=[]);return new Promise((e,i)=>{n.push({res:e,rej:i})})}_notify(n){let e=n?"res":"rej",i=this._promises||[];for(let r=0;r{let o=n[r];if(!Bt(o))return;let a={};for(let s of e)a[s]=o[s];(Ri(o.properties)&&o.properties||[r]).forEach(s=>{(s===r||!i.has(s))&&i.set(s,a)})})}_animateOptions(n,e){let i=e.options,r=Pre(n,i);if(!r)return[];let o=this._createAnimations(r,i);return i.$shared&&Rre(n.options.$animations,i).then(()=>{n.options=i},()=>{}),o}_createAnimations(n,e){let i=this._properties,r=[],o=n.$animations||(n.$animations={}),a=Object.keys(e),s=Date.now(),l;for(l=a.length-1;l>=0;--l){let c=a[l];if(c.charAt(0)==="$")continue;if(c==="options"){r.push(...this._animateOptions(n,e));continue}let d=e[c],p=o[c],_=i.get(c);if(p)if(_&&p.active()){p.update(_,d,s);continue}else p.cancel();if(!_||!_.duration){n[c]=d;continue}o[c]=p=new HT(_,n,c,d),r.push(p)}return r}update(n,e){if(this._properties.size===0){Object.assign(n,e);return}let i=this._createAnimations(n,e);if(i.length)return Nl.add(this._chart,i),!0}};function Rre(t,n){let e=[],i=Object.keys(n);for(let r=0;r0||!e&&o<0)return r.index}return null}function c4(t,n){let{chart:e,_cachedMeta:i}=t,r=e._stacks||(e._stacks={}),{iScale:o,vScale:a,index:s}=i,l=o.axis,c=a.axis,d=Vre(o,a,i),p=n.length,_;for(let b=0;be[i].axis===n).shift()}function Hre(t,n){return Fl(t,{active:!1,dataset:void 0,datasetIndex:n,index:n,mode:"default",type:"dataset"})}function zre(t,n,e){return Fl(t,{active:!1,dataIndex:n,parsed:void 0,raw:void 0,element:e,index:n,mode:"default",type:"data"})}function Yg(t,n){let e=t.controller.index,i=t.vScale&&t.vScale.axis;if(i){n=n||t._parsed;for(let r of n){let o=r._stacks;if(!o||o[i]===void 0||o[i][e]===void 0)return;delete o[i][e],o[i]._visualValues!==void 0&&o[i]._visualValues[e]!==void 0&&delete o[i]._visualValues[e]}}}var AT=t=>t==="reset"||t==="none",d4=(t,n)=>n?t:Object.assign({},t),Ure=(t,n,e)=>t&&!n.hidden&&n._stacked&&{keys:oH(e,!0),values:null},Kc=(()=>{class t{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(e,i){this.chart=e,this._ctx=e.ctx,this.index=i,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){let e=this._cachedMeta;this.configure(),this.linkScales(),e._stacked=TT(e.vScale,e),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(e){this.index!==e&&Yg(this._cachedMeta),this.index=e}linkScales(){let e=this.chart,i=this._cachedMeta,r=this.getDataset(),o=(_,b,y,w)=>_==="x"?b:_==="r"?w:y,a=i.xAxisID=ut(r.xAxisID,IT(e,"x")),s=i.yAxisID=ut(r.yAxisID,IT(e,"y")),l=i.rAxisID=ut(r.rAxisID,IT(e,"r")),c=i.indexAxis,d=i.iAxisID=o(c,a,s,l),p=i.vAxisID=o(c,s,a,l);i.xScale=this.getScaleForId(a),i.yScale=this.getScaleForId(s),i.rScale=this.getScaleForId(l),i.iScale=this.getScaleForId(d),i.vScale=this.getScaleForId(p)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(e){return this.chart.scales[e]}_getOtherScale(e){let i=this._cachedMeta;return e===i.iScale?i.vScale:i.iScale}reset(){this._update("reset")}_destroy(){let e=this._cachedMeta;this._data&&sT(this._data,this),e._stacked&&Yg(e)}_dataCheck(){let e=this.getDataset(),i=e.data||(e.data=[]),r=this._data;if(Bt(i)){let o=this._cachedMeta;this._data=Lre(i,o)}else if(r!==i){if(r){sT(r,this);let o=this._cachedMeta;Yg(o),o._parsed=[]}i&&Object.isExtensible(i)&&jj(i,this),this._syncList=[],this._data=i}}addElements(){let e=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(e.dataset=new this.datasetElementType)}buildOrUpdateElements(e){let i=this._cachedMeta,r=this.getDataset(),o=!1;this._dataCheck();let a=i._stacked;i._stacked=TT(i.vScale,i),i.stack!==r.stack&&(o=!0,Yg(i),i.stack=r.stack),this._resyncElements(e),(o||a!==i._stacked)&&(c4(this,i._parsed),i._stacked=TT(i.vScale,i))}configure(){let e=this.chart.config,i=e.datasetScopeKeys(this._type),r=e.getOptionScopes(this.getDataset(),i,!0);this.options=e.createResolver(r,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(e,i){let{_cachedMeta:r,_data:o}=this,{iScale:a,_stacked:s}=r,l=a.axis,c=e===0&&i===o.length?!0:r._sorted,d=e>0&&r._parsed[e-1],p,_,b;if(this._parsing===!1)r._parsed=o,r._sorted=!0,b=o;else{Ri(o[e])?b=this.parseArrayData(r,o,e,i):Bt(o[e])?b=this.parseObjectData(r,o,e,i):b=this.parsePrimitiveData(r,o,e,i);let y=()=>_[l]===null||d&&_[l]C||_=0;--b)if(!w()){this.updateRangeFromParsed(d,e,y,c);break}}return d}getAllParsedValues(e){let i=this._cachedMeta._parsed,r=[],o,a,s;for(o=0,a=i.length;o=0&&ethis.getContext(r,o,i),C=d.resolveNamedOptions(b,y,w,_);return C.$shared&&(C.$shared=c,a[s]=Object.freeze(d4(C,c))),C}_resolveAnimations(e,i,r){let o=this.chart,a=this._cachedDataOpts,s=`animation-${i}`,l=a[s];if(l)return l;let c;if(o.options.animation!==!1){let p=this.chart.config,_=p.datasetAnimationScopeKeys(this._type,i),b=p.getOptionScopes(this.getDataset(),_);c=p.createResolver(b,this.getContext(e,r,i))}let d=new hC(o,c&&c.animations);return c&&c._cacheable&&(a[s]=Object.freeze(d)),d}getSharedOptions(e){if(e.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},e))}includeOptions(e,i){return!i||AT(e)||this.chart._animationsDisabled}_getSharedOptions(e,i){let r=this.resolveDataElementOptions(e,i),o=this._sharedOptions,a=this.getSharedOptions(r),s=this.includeOptions(i,a)||a!==o;return this.updateSharedOptions(a,i,r),{sharedOptions:a,includeOptions:s}}updateElement(e,i,r,o){AT(o)?Object.assign(e,r):this._resolveAnimations(i,o).update(e,r)}updateSharedOptions(e,i,r){e&&!AT(i)&&this._resolveAnimations(void 0,i).update(e,r)}_setStyle(e,i,r,o){e.active=o;let a=this.getStyle(i,o);this._resolveAnimations(i,r,o).update(e,{options:!o&&this.getSharedOptions(a)||a})}removeHoverStyle(e,i,r){this._setStyle(e,r,"active",!1)}setHoverStyle(e,i,r){this._setStyle(e,r,"active",!0)}_removeDatasetHoverStyle(){let e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!1)}_setDatasetHoverStyle(){let e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!0)}_resyncElements(e){let i=this._data,r=this._cachedMeta.data;for(let[l,c,d]of this._syncList)this[l](c,d);this._syncList=[];let o=r.length,a=i.length,s=Math.min(a,o);s&&this.parse(0,s),a>o?this._insertElements(o,a-o,e):a{for(d.length+=i,l=d.length-1;l>=s;l--)d[l]=d[l-i]};for(c(a),l=e;lr-o))}return t._cache.$bar}function Wre(t){let n=t.iScale,e=$re(n,t.type),i=n._length,r,o,a,s,l=()=>{a===32767||a===-32768||(Hh(s)&&(i=Math.min(i,Math.abs(a-s)||i)),s=a)};for(r=0,o=e.length;r0?r[t-1]:null,s=tMath.abs(s)&&(l=s,c=a),n[e.axis]=c,n._custom={barStart:l,barEnd:c,start:r,end:o,min:a,max:s}}function aH(t,n,e,i){return Ri(t)?Yre(t,n,e,i):n[e.axis]=e.parse(t,i),n}function u4(t,n,e,i){let r=t.iScale,o=t.vScale,a=r.getLabels(),s=r===o,l=[],c,d,p,_;for(c=e,d=e+i;c=e?1:-1)}function Kre(t){let n,e,i,r,o;return t.horizontal?(n=t.base>t.x,e="left",i="right"):(n=t.base{class t extends Kc{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(e,i,r,o){return u4(e,i,r,o)}parseArrayData(e,i,r,o){return u4(e,i,r,o)}parseObjectData(e,i,r,o){let{iScale:a,vScale:s}=e,{xAxisKey:l="x",yAxisKey:c="y"}=this._parsing,d=a.axis==="x"?l:c,p=s.axis==="x"?l:c,_=[],b,y,w,C;for(b=r,y=r+o;bp.controller.options.grouped),a=r.options.stacked,s=[],l=this._cachedMeta.controller.getParsed(i),c=l&&l[r.axis],d=p=>{let _=p._parsed.find(y=>y[r.axis]===c),b=_&&_[p.vScale.axis];if(Nt(b)||isNaN(b))return!0};for(let p of o)if(!(i!==void 0&&d(p))&&((a===!1||s.indexOf(p.stack)===-1||a===void 0&&p.stack===void 0)&&s.push(p.stack),p.index===e))break;return s.length||s.push(void 0),s}_getStackCount(e){return this._getStacks(void 0,e).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){let e=this.chart.scales,i=this.chart.options.indexAxis;return Object.keys(e).filter(r=>e[r].axis===i).shift()}_getAxis(){let e={},i=this.getFirstScaleIdForIndexAxis();for(let r of this.chart.data.datasets)e[ut(this.chart.options.indexAxis==="x"?r.xAxisID:r.yAxisID,i)]=!0;return Object.keys(e)}_getStackIndex(e,i,r){let o=this._getStacks(e,r),a=i!==void 0?o.indexOf(i):-1;return a===-1?o.length-1:a}_getRuler(){let e=this.options,i=this._cachedMeta,r=i.iScale,o=[],a,s;for(a=0,s=i.data.length;a{class t extends Kc{static id="bubble";static defaults={datasetElementType:!1,dataElementType:"point",animations:{numbers:{type:"number",properties:["x","y","borderWidth","radius"]}}};static overrides={scales:{x:{type:"linear"},y:{type:"linear"}}};initialize(){this.enableOptionSharing=!0,super.initialize()}parsePrimitiveData(e,i,r,o){let a=super.parsePrimitiveData(e,i,r,o);for(let s=0;s=0;--r)i=Math.max(i,e[r].size(this.resolveDataElementOptions(r))/2);return i>0&&i}getLabelAndValue(e){let i=this._cachedMeta,r=this.chart.data.labels||[],{xScale:o,yScale:a}=i,s=this.getParsed(e),l=o.getLabelForValue(s.x),c=a.getLabelForValue(s.y),d=s._custom;return{label:r[e]||"",value:"("+l+", "+c+(d?", "+d:"")+")"}}update(e){let i=this._cachedMeta.data;this.updateElements(i,0,i.length,e)}updateElements(e,i,r,o){let a=o==="reset",{iScale:s,vScale:l}=this._cachedMeta,{sharedOptions:c,includeOptions:d}=this._getSharedOptions(i,o),p=s.axis,_=l.axis;for(let b=i;bUh(W,s,l,!0)?1:Math.max(Z,Z*e,K,K*e),y=(W,Z,K)=>Uh(W,s,l,!0)?-1:Math.min(Z,Z*e,K,K*e),w=b(0,c,p),C=b(rn,d,_),D=y(Jt,c,p),F=y(Jt+rn,d,_);i=(w-D)/2,r=(C-F)/2,o=-(w+D)/2,a=-(C+F)/2}return{ratioX:i,ratioY:r,offsetX:o,offsetY:a}}var sI=(()=>{class t extends Kc{static id="doughnut";static defaults={datasetElementType:!1,dataElementType:"arc",animation:{animateRotate:!0,animateScale:!1},animations:{numbers:{type:"number",properties:["circumference","endAngle","innerRadius","outerRadius","startAngle","x","y","offset","borderWidth","spacing"]}},cutout:"50%",rotation:0,circumference:360,radius:"100%",spacing:0,indexAxis:"r"};static descriptors={_scriptable:e=>e!=="spacing",_indexable:e=>e!=="spacing"&&!e.startsWith("borderDash")&&!e.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(e){let i=e.data,{labels:{pointStyle:r,textAlign:o,color:a,useBorderRadius:s,borderRadius:l}}=e.legend.options;return i.labels.length&&i.datasets.length?i.labels.map((c,d)=>{let _=e.getDatasetMeta(0).controller.getStyle(d);return{text:c,fillStyle:_.backgroundColor,fontColor:a,hidden:!e.getDataVisibility(d),lineDash:_.borderDash,lineDashOffset:_.borderDashOffset,lineJoin:_.borderJoinStyle,lineWidth:_.borderWidth,strokeStyle:_.borderColor,textAlign:o,pointStyle:r,borderRadius:s&&(l||_.borderRadius),index:d}}):[]}},onClick(e,i,r){r.chart.toggleDataVisibility(i.index),r.chart.update()}}}};constructor(e,i){super(e,i),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(e,i){let r=this.getDataset().data,o=this._cachedMeta;if(this._parsing===!1)o._parsed=r;else{let a=c=>+r[c];if(Bt(r[e])){let{key:c="value"}=this._parsing;a=d=>+Pl(r[d],c)}let s,l;for(s=e,l=e+i;s0&&!isNaN(e)?Pi*(Math.abs(e)/i):0}getLabelAndValue(e){let i=this._cachedMeta,r=this.chart,o=r.data.labels||[],a=$h(i._parsed[e],r.options.locale);return{label:o[e]||"",value:a}}getMaxBorderWidth(e){let i=0,r=this.chart,o,a,s,l,c;if(!e){for(o=0,a=r.data.datasets.length;o{class t extends Kc{static id="line";static defaults={datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1};static overrides={scales:{_index_:{type:"category"},_value_:{type:"linear"}}};initialize(){this.enableOptionSharing=!0,this.supportsDecimation=!0,super.initialize()}update(e){let i=this._cachedMeta,{dataset:r,data:o=[],_dataset:a}=i,s=this.chart._animationsDisabled,{start:l,count:c}=uT(i,o,s);this._drawStart=l,this._drawCount=c,mT(i)&&(l=0,c=o.length),r._chart=this.chart,r._datasetIndex=this.index,r._decimated=!!a._decimated,r.points=o;let d=this.resolveDatasetElementOptions(e);this.options.showLine||(d.borderWidth=0),d.segment=this.options.segment,this.updateElement(r,void 0,{animated:!s,options:d},e),this.updateElements(o,l,c,e)}updateElements(e,i,r,o){let a=o==="reset",{iScale:s,vScale:l,_stacked:c,_dataset:d}=this._cachedMeta,{sharedOptions:p,includeOptions:_}=this._getSharedOptions(i,o),b=s.axis,y=l.axis,{spanGaps:w,segment:C}=this.options,D=Su(w)?w:Number.POSITIVE_INFINITY,F=this.chart._animationsDisabled||a||o==="none",W=i+r,Z=e.length,K=i>0&&this.getParsed(i-1);for(let oe=0;oe=W){ve.skip=!0;continue}let Be=this.getParsed(oe),wt=Nt(Be[y]),Ct=ve[b]=s.getPixelForValue(Be[b],oe),Ht=ve[y]=a||wt?l.getBasePixel():l.getPixelForValue(c?this.applyStack(l,Be,c):Be[y],oe);ve.skip=isNaN(Ct)||isNaN(Ht)||wt,ve.stop=oe>0&&Math.abs(Be[b]-K[b])>D,C&&(ve.parsed=Be,ve.raw=d.data[oe]),_&&(ve.options=p||this.resolveDataElementOptions(oe,Se.active?"active":o)),F||this.updateElement(Se,oe,ve,o),K=Be}}getMaxOverflow(){let e=this._cachedMeta,i=e.dataset,r=i.options&&i.options.borderWidth||0,o=e.data||[];if(!o.length)return r;let a=o[0].size(this.resolveDataElementOptions(0)),s=o[o.length-1].size(this.resolveDataElementOptions(o.length-1));return Math.max(r,a,s)/2}draw(){let e=this._cachedMeta;e.dataset.updateControlPoints(this.chart.chartArea,e.iScale.axis),super.draw()}}return t})(),sH=(()=>{class t extends Kc{static id="polarArea";static defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(e){let i=e.data;if(i.labels.length&&i.datasets.length){let{labels:{pointStyle:r,color:o}}=e.legend.options;return i.labels.map((a,s)=>{let c=e.getDatasetMeta(0).controller.getStyle(s);return{text:a,fillStyle:c.backgroundColor,strokeStyle:c.borderColor,fontColor:o,lineWidth:c.borderWidth,pointStyle:r,hidden:!e.getDataVisibility(s),index:s}})}return[]}},onClick(e,i,r){r.chart.toggleDataVisibility(i.index),r.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(e,i){super(e,i),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(e){let i=this._cachedMeta,r=this.chart,o=r.data.labels||[],a=$h(i._parsed[e].r,r.options.locale);return{label:o[e]||"",value:a}}parseObjectData(e,i,r,o){return xT.bind(this)(e,i,r,o)}update(e){let i=this._cachedMeta.data;this._updateRadius(),this.updateElements(i,0,i.length,e)}getMinMax(){let e=this._cachedMeta,i={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return e.data.forEach((r,o)=>{let a=this.getParsed(o).r;!isNaN(a)&&this.chart.getDataVisibility(o)&&(ai.max&&(i.max=a))}),i}_updateRadius(){let e=this.chart,i=e.chartArea,r=e.options,o=Math.min(i.right-i.left,i.bottom-i.top),a=Math.max(o/2,0),s=Math.max(r.cutoutPercentage?a/100*r.cutoutPercentage:1,0),l=(a-s)/e.getVisibleDatasetCount();this.outerRadius=a-l*this.index,this.innerRadius=this.outerRadius-l}updateElements(e,i,r,o){let a=o==="reset",s=this.chart,c=s.options.animation,d=this._cachedMeta.rScale,p=d.xCenter,_=d.yCenter,b=d.getIndexAngle(0)-.5*Jt,y=b,w,C=360/this.countVisibleElements();for(w=0;w{!isNaN(this.getParsed(o).r)&&this.chart.getDataVisibility(o)&&i++}),i}_computeAngle(e,i,r){return this.chart.getDataVisibility(e)?ua(this.resolveDataElementOptions(e,i).angle||r):0}}return t})(),roe=(()=>{class t extends sI{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}}return t})(),ooe=(()=>{class t extends Kc{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(e){let i=this._cachedMeta.vScale,r=this.getParsed(e);return{label:i.getLabels()[e],value:""+i.getLabelForValue(r[i.axis])}}parseObjectData(e,i,r,o){return xT.bind(this)(e,i,r,o)}update(e){let i=this._cachedMeta,r=i.dataset,o=i.data||[],a=i.iScale.getLabels();if(r.points=o,e!=="resize"){let s=this.resolveDatasetElementOptions(e);this.options.showLine||(s.borderWidth=0);let l={_loop:!0,_fullLoop:a.length===o.length,options:s};this.updateElement(r,void 0,l,e)}this.updateElements(o,0,o.length,e)}updateElements(e,i,r,o){let a=this._cachedMeta.rScale,s=o==="reset";for(let l=i;l{class t extends Kc{static id="scatter";static defaults={datasetElementType:!1,dataElementType:"point",showLine:!1,fill:!1};static overrides={interaction:{mode:"point"},scales:{x:{type:"linear"},y:{type:"linear"}}};getLabelAndValue(e){let i=this._cachedMeta,r=this.chart.data.labels||[],{xScale:o,yScale:a}=i,s=this.getParsed(e),l=o.getLabelForValue(s.x),c=a.getLabelForValue(s.y);return{label:r[e]||"",value:"("+l+", "+c+")"}}update(e){let i=this._cachedMeta,{data:r=[]}=i,o=this.chart._animationsDisabled,{start:a,count:s}=uT(i,r,o);if(this._drawStart=a,this._drawCount=s,mT(i)&&(a=0,s=r.length),this.options.showLine){this.datasetElementType||this.addElements();let{dataset:l,_dataset:c}=i;l._chart=this.chart,l._datasetIndex=this.index,l._decimated=!!c._decimated,l.points=r;let d=this.resolveDatasetElementOptions(e);d.segment=this.options.segment,this.updateElement(l,void 0,{animated:!o,options:d},e)}else this.datasetElementType&&(delete i.dataset,this.datasetElementType=!1);this.updateElements(r,a,s,e)}addElements(){let{showLine:e}=this.options;!this.datasetElementType&&e&&(this.datasetElementType=this.chart.registry.getElement("line")),super.addElements()}updateElements(e,i,r,o){let a=o==="reset",{iScale:s,vScale:l,_stacked:c,_dataset:d}=this._cachedMeta,p=this.resolveDataElementOptions(i,o),_=this.getSharedOptions(p),b=this.includeOptions(o,_),y=s.axis,w=l.axis,{spanGaps:C,segment:D}=this.options,F=Su(C)?C:Number.POSITIVE_INFINITY,W=this.chart._animationsDisabled||a||o==="none",Z=i>0&&this.getParsed(i-1);for(let K=i;K0&&Math.abs(Se[y]-Z[y])>F,D&&(ve.parsed=Se,ve.raw=d.data[K]),b&&(ve.options=_||this.resolveDataElementOptions(K,oe.active?"active":o)),W||this.updateElement(oe,K,ve,o),Z=Se}this.updateSharedOptions(_,o,p)}getMaxOverflow(){let e=this._cachedMeta,i=e.data||[];if(!this.options.showLine){let l=0;for(let c=i.length-1;c>=0;--c)l=Math.max(l,i[c].size(this.resolveDataElementOptions(c))/2);return l>0&&l}let r=e.dataset,o=r.options&&r.options.borderWidth||0;if(!i.length)return o;let a=i[0].size(this.resolveDataElementOptions(0)),s=i[i.length-1].size(this.resolveDataElementOptions(i.length-1));return Math.max(o,a,s)/2}}return t})(),soe=Object.freeze({__proto__:null,BarController:eoe,BubbleController:toe,DoughnutController:sI,LineController:noe,PieController:roe,PolarAreaController:sH,RadarController:ooe,ScatterController:aoe});function Tu(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}var zT=class t{static override(n){Object.assign(t.prototype,n)}options;constructor(n){this.options=n||{}}init(){}formats(){return Tu()}parse(){return Tu()}format(){return Tu()}add(){return Tu()}diff(){return Tu()}startOf(){return Tu()}endOf(){return Tu()}},loe={_date:zT};function coe(t,n,e,i){let{controller:r,data:o,_sorted:a}=t,s=r._cachedMeta.iScale,l=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null;if(s&&n===s.axis&&n!=="r"&&a&&o.length){let c=s._reversePixels?Lj:Ws;if(i){if(r._sharedOptions){let d=o[0],p=typeof d.getRange=="function"&&d.getRange(n);if(p){let _=c(o,n,e-p),b=c(o,n,e+p);return{lo:_.lo,hi:b.hi}}}}else{let d=c(o,n,e);if(l){let{vScale:p}=r._cachedMeta,{_parsed:_}=t,b=_.slice(0,d.lo+1).reverse().findIndex(w=>!Nt(w[p.axis]));d.lo-=Math.max(0,b);let y=_.slice(d.hi).findIndex(w=>!Nt(w[p.axis]));d.hi+=Math.max(0,y)}return d}}return{lo:0,hi:o.length-1}}function r_(t,n,e,i,r){let o=t.getSortedVisibleDatasetMetas(),a=e[n];for(let s=0,l=o.length;s{l[a]&&l[a](n[e],r)&&(o.push({element:l,datasetIndex:c,index:d}),s=s||l.inRange(n.x,n.y,r))}),i&&!s?[]:o}var hoe={evaluateInteractionItems:r_,modes:{index(t,n,e,i){let r=Yc(n,t),o=e.axis||"x",a=e.includeInvisible||!1,s=e.intersect?RT(t,r,o,i,a):PT(t,r,o,!1,i,a),l=[];return s.length?(t.getSortedVisibleDatasetMetas().forEach(c=>{let d=s[0].index,p=c.data[d];p&&!p.skip&&l.push({element:p,datasetIndex:c.index,index:d})}),l):[]},dataset(t,n,e,i){let r=Yc(n,t),o=e.axis||"xy",a=e.includeInvisible||!1,s=e.intersect?RT(t,r,o,i,a):PT(t,r,o,!1,i,a);if(s.length>0){let l=s[0].datasetIndex,c=t.getDatasetMeta(l).data;s=[];for(let d=0;de.pos===n)}function f4(t,n){return t.filter(e=>lH.indexOf(e.pos)===-1&&e.box.axis===n)}function Kg(t,n){return t.sort((e,i)=>{let r=n?i:e,o=n?e:i;return r.weight===o.weight?r.index-o.index:r.weight-o.weight})}function poe(t){let n=[],e,i,r,o,a,s;for(e=0,i=(t||[]).length;ec.box.fullSize),!0),i=Kg(Qg(n,"left"),!0),r=Kg(Qg(n,"right")),o=Kg(Qg(n,"top"),!0),a=Kg(Qg(n,"bottom")),s=f4(n,"x"),l=f4(n,"y");return{fullSize:e,leftAndTop:i.concat(o),rightAndBottom:r.concat(l).concat(a).concat(s),chartArea:Qg(n,"chartArea"),vertical:i.concat(r).concat(l),horizontal:o.concat(a).concat(s)}}function g4(t,n,e,i){return Math.max(t[e],n[e])+Math.max(t[i],n[i])}function cH(t,n){t.top=Math.max(t.top,n.top),t.left=Math.max(t.left,n.left),t.bottom=Math.max(t.bottom,n.bottom),t.right=Math.max(t.right,n.right)}function boe(t,n,e,i){let{pos:r,box:o}=e,a=t.maxPadding;if(!Bt(r)){e.size&&(t[r]-=e.size);let p=i[e.stack]||{size:0,count:1};p.size=Math.max(p.size,e.horizontal?o.height:o.width),e.size=p.size/p.count,t[r]+=e.size}o.getPadding&&cH(a,o.getPadding());let s=Math.max(0,n.outerWidth-g4(a,t,"left","right")),l=Math.max(0,n.outerHeight-g4(a,t,"top","bottom")),c=s!==t.w,d=l!==t.h;return t.w=s,t.h=l,e.horizontal?{same:c,other:d}:{same:d,other:c}}function voe(t){let n=t.maxPadding;function e(i){let r=Math.max(n[i]-t[i],0);return t[i]+=r,r}t.y+=e("top"),t.x+=e("left"),e("right"),e("bottom")}function yoe(t,n){let e=n.maxPadding;function i(r){let o={left:0,top:0,right:0,bottom:0};return r.forEach(a=>{o[a]=Math.max(n[a],e[a])}),o}return i(t?["left","right"]:["top","bottom"])}function Jg(t,n,e,i){let r=[],o,a,s,l,c,d;for(o=0,a=t.length,c=0;o{typeof w.beforeLayout=="function"&&w.beforeLayout()});let d=l.reduce((w,C)=>C.box.options&&C.box.options.display===!1?w:w+1,0)||1,p=Object.freeze({outerWidth:n,outerHeight:e,padding:r,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/d,hBoxMaxHeight:a/2}),_=Object.assign({},r);cH(_,gr(i));let b=Object.assign({maxPadding:_,w:o,h:a,x:r.left,y:r.top},r),y=goe(l.concat(c),p);Jg(s.fullSize,b,p,y),Jg(l,b,p,y),Jg(c,b,p,y)&&Jg(l,b,p,y),voe(b),_4(s.leftAndTop,b,p,y),b.x+=b.w,b.y+=b.h,_4(s.rightAndBottom,b,p,y),t.chartArea={left:b.left,top:b.top,right:b.left+b.w,bottom:b.top+b.h,height:b.h,width:b.w},ui(s.chartArea,w=>{let C=w.box;Object.assign(C,t.chartArea),C.update(b.w,b.h,{left:0,top:0,right:0,bottom:0})})}},pC=class{acquireContext(n,e){}releaseContext(n){return!1}addEventListener(n,e,i){}removeEventListener(n,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(n,e,i,r){return e=Math.max(0,e||n.width),i=i||n.height,{width:e,height:Math.max(0,r?Math.floor(e/r):i)}}isAttached(n){return!0}updateConfig(n){}},UT=class extends pC{acquireContext(n){return n&&n.getContext&&n.getContext("2d")||null}updateConfig(n){n.options.animation=!1}},uC="$chartjs",xoe={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},b4=t=>t===null||t==="";function Coe(t,n){let e=t.style,i=t.getAttribute("height"),r=t.getAttribute("width");if(t[uC]={initial:{height:i,width:r,style:{display:e.display,height:e.height,width:e.width}}},e.display=e.display||"block",e.boxSizing=e.boxSizing||"border-box",b4(r)){let o=wT(t,"width");o!==void 0&&(t.width=o)}if(b4(i))if(t.style.height==="")t.height=t.width/(n||2);else{let o=wT(t,"height");o!==void 0&&(t.height=o)}return t}var dH=e4?{passive:!0}:!1;function woe(t,n,e){t&&t.addEventListener(n,e,dH)}function Doe(t,n,e){t&&t.canvas&&t.canvas.removeEventListener(n,e,dH)}function Moe(t,n){let e=xoe[t.type]||t.type,{x:i,y:r}=Yc(t,n);return{type:e,chart:n,native:t,x:i!==void 0?i:null,y:r!==void 0?r:null}}function fC(t,n){for(let e of t)if(e===n||e.contains(n))return!0}function Eoe(t,n,e){let i=t.canvas,r=new MutationObserver(o=>{let a=!1;for(let s of o)a=a||fC(s.addedNodes,i),a=a&&!fC(s.removedNodes,i);a&&e()});return r.observe(document,{childList:!0,subtree:!0}),r}function Soe(t,n,e){let i=t.canvas,r=new MutationObserver(o=>{let a=!1;for(let s of o)a=a||fC(s.removedNodes,i),a=a&&!fC(s.addedNodes,i);a&&e()});return r.observe(document,{childList:!0,subtree:!0}),r}var t_=new Map,v4=0;function uH(){let t=window.devicePixelRatio;t!==v4&&(v4=t,t_.forEach((n,e)=>{e.currentDevicePixelRatio!==t&&n()}))}function koe(t,n){t_.size||window.addEventListener("resize",uH),t_.set(t,n)}function Toe(t){t_.delete(t),t_.size||window.removeEventListener("resize",uH)}function Ioe(t,n,e){let i=t.canvas,r=i&&nC(i);if(!r)return;let o=dT((s,l)=>{let c=r.clientWidth;e(s,l),c{let l=s[0],c=l.contentRect.width,d=l.contentRect.height;c===0&&d===0||o(c,d)});return a.observe(r),koe(t,o),a}function FT(t,n,e){e&&e.disconnect(),n==="resize"&&Toe(t)}function Aoe(t,n,e){let i=t.canvas,r=dT(o=>{t.ctx!==null&&e(Moe(o,t))},t);return woe(i,n,r),r}var $T=class extends pC{acquireContext(n,e){let i=n&&n.getContext&&n.getContext("2d");return i&&i.canvas===n?(Coe(n,e),i):null}releaseContext(n){let e=n.canvas;if(!e[uC])return!1;let i=e[uC].initial;["height","width"].forEach(o=>{let a=i[o];Nt(a)?e.removeAttribute(o):e.setAttribute(o,a)});let r=i.style||{};return Object.keys(r).forEach(o=>{e.style[o]=r[o]}),e.width=e.width,delete e[uC],!0}addEventListener(n,e,i){this.removeEventListener(n,e);let r=n.$proxies||(n.$proxies={}),a={attach:Eoe,detach:Soe,resize:Ioe}[e]||Aoe;r[e]=a(n,e,i)}removeEventListener(n,e){let i=n.$proxies||(n.$proxies={}),r=i[e];if(!r)return;({attach:FT,detach:FT,resize:FT}[e]||Doe)(n,e,r),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(n,e,i,r){return Jj(n,e,i,r)}isAttached(n){let e=n&&nC(n);return!!(e&&e.isConnected)}};function Ooe(t){return!iC()||typeof OffscreenCanvas<"u"&&t instanceof OffscreenCanvas?UT:$T}var rs=class{static defaults={};static defaultRoutes=void 0;x;y;active=!1;options;$animations;tooltipPosition(n){let{x:e,y:i}=this.getProps(["x","y"],n);return{x:e,y:i}}hasValue(){return Su(this.x)&&Su(this.y)}getProps(n,e){let i=this.$animations;if(!e||!i)return this;let r={};return n.forEach(o=>{r[o]=i[o]&&i[o].active()?i[o]._to:this[o]}),r}};function Roe(t,n){let e=t.options.ticks,i=Poe(t),r=Math.min(e.maxTicksLimit||i,i),o=e.major.enabled?Noe(n):[],a=o.length,s=o[0],l=o[a-1],c=[];if(a>r)return Loe(n,c,o,a/r),c;let d=Foe(o,n,r);if(a>0){let p,_,b=a>1?Math.round((l-s)/(a-1)):null;for(aC(n,c,d,Nt(b)?0:s-b,s),p=0,_=a-1;p<_;p++)aC(n,c,d,o[p],o[p+1]);return aC(n,c,d,l,Nt(b)?n.length:l+b),c}return aC(n,c,d),c}function Poe(t){let n=t.options.offset,e=t._tickSize(),i=t._length/e+(n?0:1),r=t._maxLength/e;return Math.floor(Math.min(i,r))}function Foe(t,n,e){let i=Voe(t),r=n.length/e;if(!i)return Math.max(r,1);let o=Pj(i);for(let a=0,s=o.length-1;ar)return l}return Math.max(r,1)}function Noe(t){let n=[],e,i;for(e=0,i=t.length;et==="left"?"right":t==="right"?"left":t,y4=(t,n,e)=>n==="top"||n==="left"?t[n]+e:t[n]-e,x4=(t,n)=>Math.min(n||t,t);function C4(t,n){let e=[],i=t.length/n,r=t.length,o=0;for(;oa+s)))return l}function Hoe(t,n){ui(t,e=>{let i=e.gc,r=i.length/2,o;if(r>n){for(o=0;oi?i:e,i=r&&e>i?e:i,{min:po(e,po(i,e)),max:po(i,po(e,i))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){let n=this.chart.data;return this.options.labels||(this.isHorizontal()?n.xLabels:n.yLabels)||n.labels||[]}getLabelItems(n=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(n))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){ki(this.options.beforeUpdate,[this])}update(n,e,i){let{beginAtZero:r,grace:o,ticks:a}=this.options,s=a.sampleSize;this.beforeUpdate(),this.maxWidth=n,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=qj(this,o,r),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();let l=s=o||i<=1||!this.isHorizontal()){this.labelRotation=r;return}let d=this._getLabelSizes(),p=d.widest.width,_=d.highest.height,b=On(this.chart.width-p,0,this.maxWidth);s=n.offset?this.maxWidth/i:b/(i-1),p+6>s&&(s=b/(i-(n.offset?.5:1)),l=this.maxHeight-Zg(n.grid)-e.padding-w4(n.title,this.chart.options.font),c=Math.sqrt(p*p+_*_),a=Qx(Math.min(Math.asin(On((d.highest.height+6)/s,-1,1)),Math.asin(On(l/c,-1,1))-Math.asin(On(_/c,-1,1)))),a=Math.max(r,Math.min(o,a))),this.labelRotation=a}afterCalculateLabelRotation(){ki(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){ki(this.options.beforeFit,[this])}fit(){let n={width:0,height:0},{chart:e,options:{ticks:i,title:r,grid:o}}=this,a=this._isVisible(),s=this.isHorizontal();if(a){let l=w4(r,e.options.font);if(s?(n.width=this.maxWidth,n.height=Zg(o)+l):(n.height=this.maxHeight,n.width=Zg(o)+l),i.display&&this.ticks.length){let{first:c,last:d,widest:p,highest:_}=this._getLabelSizes(),b=i.padding*2,y=ua(this.labelRotation),w=Math.cos(y),C=Math.sin(y);if(s){let D=i.mirror?0:C*p.width+w*_.height;n.height=Math.min(this.maxHeight,n.height+D+b)}else{let D=i.mirror?0:w*p.width+C*_.height;n.width=Math.min(this.maxWidth,n.width+D+b)}this._calculatePadding(c,d,C,w)}}this._handleMargins(),s?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=n.height):(this.width=n.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(n,e,i,r){let{ticks:{align:o,padding:a},position:s}=this.options,l=this.labelRotation!==0,c=s!=="top"&&this.axis==="x";if(this.isHorizontal()){let d=this.getPixelForTick(0)-this.left,p=this.right-this.getPixelForTick(this.ticks.length-1),_=0,b=0;l?c?(_=r*n.width,b=i*e.height):(_=i*n.height,b=r*e.width):o==="start"?b=e.width:o==="end"?_=n.width:o!=="inner"&&(_=n.width/2,b=e.width/2),this.paddingLeft=Math.max((_-d+a)*this.width/(this.width-d),0),this.paddingRight=Math.max((b-p+a)*this.width/(this.width-p),0)}else{let d=e.height/2,p=n.height/2;o==="start"?(d=0,p=n.height):o==="end"&&(d=e.height,p=0),this.paddingTop=d+a,this.paddingBottom=p+a}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){ki(this.options.afterFit,[this])}isHorizontal(){let{axis:n,position:e}=this.options;return e==="top"||e==="bottom"||n==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(n){this.beforeTickToLabelConversion(),this.generateTickLabels(n);let e,i;for(e=0,i=n.length;e({width:a[Be]||0,height:s[Be]||0});return{first:ve(0),last:ve(e-1),widest:ve(oe),highest:ve(Se),widths:a,heights:s}}getLabelForValue(n){return n}getPixelForValue(n,e){return NaN}getValueForPixel(n){}getPixelForTick(n){let e=this.ticks;return n<0||n>e.length-1?null:this.getPixelForValue(e[n].value)}getPixelForDecimal(n){this._reversePixels&&(n=1-n);let e=this._startPixel+n*this._length;return Nj(this._alignToPixels?Wc(this.chart,e,0):e)}getDecimalForPixel(n){let e=(n-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){let{min:n,max:e}=this;return n<0&&e<0?e:n>0&&e>0?n:0}getContext(n){let e=this.ticks||[];if(n>=0&&ns*r?s/i:l/r:l*r0}_computeGridLineItems(n){let e=this.axis,i=this.chart,r=this.options,{grid:o,position:a,border:s}=r,l=o.offset,c=this.isHorizontal(),p=this.ticks.length+(l?1:0),_=Zg(o),b=[],y=s.setContext(this.getContext()),w=y.display?y.width:0,C=w/2,D=function(Hi){return Wc(i,Hi,w)},F,W,Z,K,oe,Se,ve,Be,wt,Ct,Ht,Jn;if(a==="top")F=D(this.bottom),Se=this.bottom-_,Be=F-C,Ct=D(n.top)+C,Jn=n.bottom;else if(a==="bottom")F=D(this.top),Ct=n.top,Jn=D(n.bottom)-C,Se=F+C,Be=this.top+_;else if(a==="left")F=D(this.right),oe=this.right-_,ve=F-C,wt=D(n.left)+C,Ht=n.right;else if(a==="right")F=D(this.left),wt=n.left,Ht=D(n.right)-C,oe=F+C,ve=this.left+_;else if(e==="x"){if(a==="center")F=D((n.top+n.bottom)/2+.5);else if(Bt(a)){let Hi=Object.keys(a)[0],an=a[Hi];F=D(this.chart.scales[Hi].getPixelForValue(an))}Ct=n.top,Jn=n.bottom,Se=F+C,Be=Se+_}else if(e==="y"){if(a==="center")F=D((n.left+n.right)/2);else if(Bt(a)){let Hi=Object.keys(a)[0],an=a[Hi];F=D(this.chart.scales[Hi].getPixelForValue(an))}oe=F-C,ve=oe-_,wt=n.left,Ht=n.right}let vo=ut(r.ticks.maxTicksLimit,p),bi=Math.max(1,Math.ceil(p/vo));for(W=0;W0&&(ld-=sd/2);break}T_={left:ld,top:bp,width:sd+Bu.width,height:_p+Bu.height,color:bi.backdropColor}}C.push({label:Z,font:Be,textOffset:Ht,options:{rotation:w,color:an,strokeColor:Ia,strokeWidth:br,textAlign:Vu,textBaseline:Jn,translation:[K,oe],backdrop:T_}})}return C}_getXAxisLabelAlignment(){let{position:n,ticks:e}=this.options;if(-ua(this.labelRotation))return n==="top"?"left":"right";let r="center";return e.align==="start"?r="left":e.align==="end"?r="right":e.align==="inner"&&(r="inner"),r}_getYAxisLabelAlignment(n){let{position:e,ticks:{crossAlign:i,mirror:r,padding:o}}=this.options,a=this._getLabelSizes(),s=n+o,l=a.widest.width,c,d;return e==="left"?r?(d=this.right+o,i==="near"?c="left":i==="center"?(c="center",d+=l/2):(c="right",d+=l)):(d=this.right-s,i==="near"?c="right":i==="center"?(c="center",d-=l/2):(c="left",d=this.left)):e==="right"?r?(d=this.left+o,i==="near"?c="right":i==="center"?(c="center",d-=l/2):(c="left",d-=l)):(d=this.left+s,i==="near"?c="left":i==="center"?(c="center",d+=l/2):(c="right",d=this.right)):c="right",{textAlign:c,x:d}}_computeLabelArea(){if(this.options.ticks.mirror)return;let n=this.chart,e=this.options.position;if(e==="left"||e==="right")return{top:0,left:this.left,bottom:n.height,right:this.right};if(e==="top"||e==="bottom")return{top:this.top,left:0,bottom:this.bottom,right:n.width}}drawBackground(){let{ctx:n,options:{backgroundColor:e},left:i,top:r,width:o,height:a}=this;e&&(n.save(),n.fillStyle=e,n.fillRect(i,r,o,a),n.restore())}getLineWidthForValue(n){let e=this.options.grid;if(!this._isVisible()||!e.display)return 0;let r=this.ticks.findIndex(o=>o.value===n);return r>=0?e.setContext(this.getContext(r)).lineWidth:0}drawGrid(n){let e=this.options.grid,i=this.ctx,r=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(n)),o,a,s=(l,c,d)=>{!d.width||!d.color||(i.save(),i.lineWidth=d.width,i.strokeStyle=d.color,i.setLineDash(d.borderDash||[]),i.lineDashOffset=d.borderDashOffset,i.beginPath(),i.moveTo(l.x,l.y),i.lineTo(c.x,c.y),i.stroke(),i.restore())};if(e.display)for(o=0,a=r.length;o{this.draw(o)}}]:[{z:i,draw:o=>{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:r,draw:()=>{this.drawBorder()}},{z:e,draw:o=>{this.drawLabels(o)}}]}getMatchingVisibleMetas(n){let e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",r=[],o,a;for(o=0,a=e.length;o{let i=e.split("."),r=i.pop(),o=[t].concat(i).join("."),a=n[e].split("."),s=a.pop(),l=a.join(".");Ti.route(o,r,l,s)})}function Yoe(t){return"id"in t&&"defaults"in t}var WT=class{constructor(){this.controllers=new Yh(Kc,"datasets",!0),this.elements=new Yh(rs,"elements"),this.plugins=new Yh(Object,"plugins"),this.scales=new Yh(Au,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...n){this._each("register",n)}remove(...n){this._each("unregister",n)}addControllers(...n){this._each("register",n,this.controllers)}addElements(...n){this._each("register",n,this.elements)}addPlugins(...n){this._each("register",n,this.plugins)}addScales(...n){this._each("register",n,this.scales)}getController(n){return this._get(n,this.controllers,"controller")}getElement(n){return this._get(n,this.elements,"element")}getPlugin(n){return this._get(n,this.plugins,"plugin")}getScale(n){return this._get(n,this.scales,"scale")}removeControllers(...n){this._each("unregister",n,this.controllers)}removeElements(...n){this._each("unregister",n,this.elements)}removePlugins(...n){this._each("unregister",n,this.plugins)}removeScales(...n){this._each("unregister",n,this.scales)}_each(n,e,i){[...e].forEach(r=>{let o=i||this._getRegistryForType(r);i||o.isForType(r)||o===this.plugins&&r.id?this._exec(n,o,r):ui(r,a=>{let s=i||this._getRegistryForType(a);this._exec(n,s,a)})})}_exec(n,e,i){let r=Yx(n);ki(i["before"+r],[],i),e[n](i),ki(i["after"+r],[],i)}_getRegistryForType(n){for(let e=0;eo.filter(s=>!a.some(l=>s.plugin.id===l.plugin.id));this._notify(r(e,i),n,"stop"),this._notify(r(i,e),n,"start")}};function Qoe(t){let n={},e=[],i=Object.keys(Ks.plugins.items);for(let o=0;o1&&D4(t[0].toLowerCase());if(i)return i}throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function M4(t,n,e){if(e[n+"AxisID"]===t)return{axis:n}}function iae(t,n){if(n.data&&n.data.datasets){let e=n.data.datasets.filter(i=>i.xAxisID===t||i.yAxisID===t);if(e.length)return M4(t,"x",e[0])||M4(t,"y",e[0])}return{}}function nae(t,n){let e=$c[t.type]||{scales:{}},i=n.scales||{},r=qT(t.type,n),o=Object.create(null);return Object.keys(i).forEach(a=>{let s=i[a];if(!Bt(s))return console.error(`Invalid scale configuration for scale: ${a}`);if(s._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${a}`);let l=YT(a,s,iae(a,t),Ti.scales[s.type]),c=eae(l,r),d=e.scales||{};o[a]=jh(Object.create(null),[{axis:l},s,d[l],d[c]])}),t.data.datasets.forEach(a=>{let s=a.type||t.type,l=a.indexAxis||qT(s,n),d=($c[s]||{}).scales||{};Object.keys(d).forEach(p=>{let _=Joe(p,l),b=a[_+"AxisID"]||_;o[b]=o[b]||Object.create(null),jh(o[b],[{axis:_},i[b],d[p]])})}),Object.keys(o).forEach(a=>{let s=o[a];jh(s,[Ti.scales[s.type],Ti.scale])}),o}function mH(t){let n=t.options||(t.options={});n.plugins=ut(n.plugins,{}),n.scales=nae(t,n)}function hH(t){return t=t||{},t.datasets=t.datasets||[],t.labels=t.labels||[],t}function rae(t){return t=t||{},t.data=hH(t.data),mH(t),t}var E4=new Map,pH=new Set;function sC(t,n){let e=E4.get(t);return e||(e=n(),E4.set(t,e),pH.add(e)),e}var Xg=(t,n,e)=>{let i=Pl(n,e);i!==void 0&&t.add(i)},QT=class{constructor(n){this._config=rae(n),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(n){this._config.type=n}get data(){return this._config.data}set data(n){this._config.data=hH(n)}get options(){return this._config.options}set options(n){this._config.options=n}get plugins(){return this._config.plugins}update(){let n=this._config;this.clearCache(),mH(n)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(n){return sC(n,()=>[[`datasets.${n}`,""]])}datasetAnimationScopeKeys(n,e){return sC(`${n}.transition.${e}`,()=>[[`datasets.${n}.transitions.${e}`,`transitions.${e}`],[`datasets.${n}`,""]])}datasetElementScopeKeys(n,e){return sC(`${n}-${e}`,()=>[[`datasets.${n}.elements.${e}`,`datasets.${n}`,`elements.${e}`,""]])}pluginScopeKeys(n){let e=n.id,i=this.type;return sC(`${i}-plugin-${e}`,()=>[[`plugins.${e}`,...n.additionalOptionScopes||[]]])}_cachedScopes(n,e){let i=this._scopeCache,r=i.get(n);return(!r||e)&&(r=new Map,i.set(n,r)),r}getOptionScopes(n,e,i){let{options:r,type:o}=this,a=this._cachedScopes(n,i),s=a.get(e);if(s)return s;let l=new Set;e.forEach(d=>{n&&(l.add(n),d.forEach(p=>Xg(l,n,p))),d.forEach(p=>Xg(l,r,p)),d.forEach(p=>Xg(l,$c[o]||{},p)),d.forEach(p=>Xg(l,Ti,p)),d.forEach(p=>Xg(l,Xx,p))});let c=Array.from(l);return c.length===0&&c.push(Object.create(null)),pH.has(e)&&a.set(e,c),c}chartOptionScopes(){let{options:n,type:e}=this;return[n,$c[e]||{},Ti.datasets[e]||{},{type:e},Ti,Xx]}resolveNamedOptions(n,e,i,r=[""]){let o={$shared:!0},{resolver:a,subPrefixes:s}=S4(this._resolverCache,n,r),l=a;if(aae(a,e)){o.$shared=!1,i=Ol(i)?i():i;let c=this.createResolver(n,i,s);l=Eu(a,i,c)}for(let c of e)o[c]=l[c];return o}createResolver(n,e,i=[""],r){let{resolver:o}=S4(this._resolverCache,n,i);return Bt(e)?Eu(o,e,void 0,r):o}};function S4(t,n,e){let i=t.get(n);i||(i=new Map,t.set(n,i));let r=e.join(),o=i.get(r);return o||(o={resolver:tC(n,e),subPrefixes:e.filter(s=>!s.toLowerCase().includes("hover"))},i.set(r,o)),o}var oae=t=>Bt(t)&&Object.getOwnPropertyNames(t).some(n=>Ol(t[n]));function aae(t,n){let{isScriptable:e,isIndexable:i}=bT(t);for(let r of n){let o=e(r),a=i(r),s=(a||o)&&t[r];if(o&&(Ol(s)||oae(s))||a&&Ri(s))return!0}return!1}var sae="4.5.1",lae=["top","bottom","left","right","chartArea"];function k4(t,n){return t==="top"||t==="bottom"||lae.indexOf(t)===-1&&n==="x"}function T4(t,n){return function(e,i){return e[t]===i[t]?e[n]-i[n]:e[t]-i[t]}}function I4(t){let n=t.chart,e=n.options.animation;n.notifyPlugins("afterRender"),ki(e&&e.onComplete,[t],n)}function cae(t){let n=t.chart,e=n.options.animation;ki(e&&e.onProgress,[t],n)}function fH(t){return iC()&&typeof t=="string"?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}var mC={},A4=t=>{let n=fH(t);return Object.values(mC).filter(e=>e.canvas===n).pop()};function dae(t,n,e){let i=Object.keys(t);for(let r of i){let o=+r;if(o>=n){let a=t[r];delete t[r],(e>0||o>n)&&(t[o+e]=a)}}}function uae(t,n,e,i){return!e||t.type==="mouseout"?null:i?n:t}var vC=(()=>{class t{static defaults=Ti;static instances=mC;static overrides=$c;static registry=Ks;static version=sae;static getChart=A4;static register(...e){Ks.add(...e),O4()}static unregister(...e){Ks.remove(...e),O4()}constructor(e,i){let r=this.config=new QT(i),o=fH(e),a=A4(o);if(a)throw new Error("Canvas is already in use. Chart with ID '"+a.id+"' must be destroyed before the canvas with ID '"+a.canvas.id+"' can be reused.");let s=r.createResolver(r.chartOptionScopes(),this.getContext());this.platform=new(r.platform||Ooe(o)),this.platform.updateConfig(r);let l=this.platform.acquireContext(o,s.aspectRatio),c=l&&l.canvas,d=c&&c.height,p=c&&c.width;if(this.id=Ij(),this.ctx=l,this.canvas=c,this.width=p,this.height=d,this._options=s,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new GT,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=Hj(_=>this.update(_),s.resizeDelay||0),this._dataChanges=[],mC[this.id]=this,!l||!c){console.error("Failed to create chart: can't acquire context from the given item");return}Nl.listen(this,"complete",I4),Nl.listen(this,"progress",cae),this._initialize(),this.attached&&this.update()}get aspectRatio(){let{options:{aspectRatio:e,maintainAspectRatio:i},width:r,height:o,_aspectRatio:a}=this;return Nt(e)?i&&a?a:o?r/o:null:e}get data(){return this.config.data}set data(e){this.config.data=e}get options(){return this._options}set options(e){this.config.options=e}get registry(){return Ks}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():CT(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return fT(this.canvas,this.ctx),this}stop(){return Nl.stop(this),this}resize(e,i){Nl.running(this)?this._resizeBeforeDraw={width:e,height:i}:this._resize(e,i)}_resize(e,i){let r=this.options,o=this.canvas,a=r.maintainAspectRatio&&this.aspectRatio,s=this.platform.getMaximumSize(o,e,i,a),l=r.devicePixelRatio||this.platform.getDevicePixelRatio(),c=this.width?"resize":"attach";this.width=s.width,this.height=s.height,this._aspectRatio=this.aspectRatio,CT(this,l,!0)&&(this.notifyPlugins("resize",{size:s}),ki(r.onResize,[this,s],this),this.attached&&this._doResize(c)&&this.render())}ensureScalesHaveIDs(){let i=this.options.scales||{};ui(i,(r,o)=>{r.id=o})}buildOrUpdateScales(){let e=this.options,i=e.scales,r=this.scales,o=Object.keys(r).reduce((s,l)=>(s[l]=!1,s),{}),a=[];i&&(a=a.concat(Object.keys(i).map(s=>{let l=i[s],c=YT(s,l),d=c==="r",p=c==="x";return{options:l,dposition:d?"chartArea":p?"bottom":"left",dtype:d?"radialLinear":p?"category":"linear"}}))),ui(a,s=>{let l=s.options,c=l.id,d=YT(c,l),p=ut(l.type,s.dtype);(l.position===void 0||k4(l.position,d)!==k4(s.dposition))&&(l.position=s.dposition),o[c]=!0;let _=null;if(c in r&&r[c].type===p)_=r[c];else{let b=Ks.getScale(p);_=new b({id:c,type:p,ctx:this.ctx,chart:this}),r[_.id]=_}_.init(l,e)}),ui(o,(s,l)=>{s||delete r[l]}),ui(r,s=>{Pr.configure(this,s,s.options),Pr.addBox(this,s)})}_updateMetasets(){let e=this._metasets,i=this.data.datasets.length,r=e.length;if(e.sort((o,a)=>o.index-a.index),r>i){for(let o=i;oi.length&&delete this._stacks,e.forEach((r,o)=>{i.filter(a=>a===r._dataset).length===0&&this._destroyDatasetMeta(o)})}buildOrUpdateControllers(){let e=[],i=this.data.datasets,r,o;for(this._removeUnreferencedMetasets(),r=0,o=i.length;r{this.getDatasetMeta(i).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(e){let i=this.config;i.update();let r=this._options=i.createResolver(i.chartOptionScopes(),this.getContext()),o=this._animationsDisabled=!r.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:e,cancelable:!0})===!1)return;let a=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let s=0;for(let d=0,p=this.data.datasets.length;d{d.reset()}),this._updateDatasets(e),this.notifyPlugins("afterUpdate",{mode:e}),this._layers.sort(T4("z","_idx"));let{_active:l,_lastEvent:c}=this;c?this._eventHandler(c,!0):l.length&&this._updateHoverStyles(l,l,!0),this.render()}_updateScales(){ui(this.scales,e=>{Pr.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let e=this.options,i=new Set(Object.keys(this._listeners)),r=new Set(e.events);(!iT(i,r)||!!this._responsiveListeners!==e.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){let{_hiddenIndices:e}=this,i=this._getUniformDataChanges()||[];for(let{method:r,start:o,count:a}of i){let s=r==="_removeElements"?-a:a;dae(e,o,s)}}_getUniformDataChanges(){let e=this._dataChanges;if(!e||!e.length)return;this._dataChanges=[];let i=this.data.datasets.length,r=a=>new Set(e.filter(s=>s[0]===a).map((s,l)=>l+","+s.splice(1).join(","))),o=r(0);for(let a=1;aa.split(",")).map(a=>({method:a[1],start:+a[2],count:+a[3]}))}_updateLayout(e){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;Pr.update(this,this.width,this.height,e);let i=this.chartArea,r=i.width<=0||i.height<=0;this._layers=[],ui(this.boxes,o=>{r&&o.position==="chartArea"||(o.configure&&o.configure(),this._layers.push(...o._layers()))},this),this._layers.forEach((o,a)=>{o._idx=a}),this.notifyPlugins("afterLayout")}_updateDatasets(e){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:e,cancelable:!0})!==!1){for(let i=0,r=this.data.datasets.length;i=0;--i)this._drawDataset(e[i]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(e){let i=this.ctx,r={meta:e,index:e.index,cancelable:!0},o=kT(this,e);this.notifyPlugins("beforeDatasetDraw",r)!==!1&&(o&&Gg(i,o),e.controller.draw(),o&&qg(i),r.cancelable=!1,this.notifyPlugins("afterDatasetDraw",r))}isPointInArea(e){return Gs(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,i,r,o){let a=hoe.modes[i];return typeof a=="function"?a(this,e,r,o):[]}getDatasetMeta(e){let i=this.data.datasets[e],r=this._metasets,o=r.filter(a=>a&&a._dataset===i).pop();return o||(o={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:i&&i.order||0,index:e,_dataset:i,_parsed:[],_sorted:!1},r.push(o)),o}getContext(){return this.$context||(this.$context=Fl(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(e){let i=this.data.datasets[e];if(!i)return!1;let r=this.getDatasetMeta(e);return typeof r.hidden=="boolean"?!r.hidden:!i.hidden}setDatasetVisibility(e,i){let r=this.getDatasetMeta(e);r.hidden=!i}toggleDataVisibility(e){this._hiddenIndices[e]=!this._hiddenIndices[e]}getDataVisibility(e){return!this._hiddenIndices[e]}_updateVisibility(e,i,r){let o=r?"show":"hide",a=this.getDatasetMeta(e),s=a.controller._resolveAnimations(void 0,o);Hh(i)?(a.data[i].hidden=!r,this.update()):(this.setDatasetVisibility(e,r),s.update(a,{visible:r}),this.update(l=>l.datasetIndex===e?o:void 0))}hide(e,i){this._updateVisibility(e,i,!1)}show(e,i){this._updateVisibility(e,i,!0)}_destroyDatasetMeta(e){let i=this._metasets[e];i&&i.controller&&i.controller._destroy(),delete this._metasets[e]}_stop(){let e,i;for(this.stop(),Nl.remove(this),e=0,i=this.data.datasets.length;e{i.addEventListener(this,a,s),e[a]=s},o=(a,s,l)=>{a.offsetX=s,a.offsetY=l,this._eventHandler(a)};ui(this.options.events,a=>r(a,o))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});let e=this._responsiveListeners,i=this.platform,r=(c,d)=>{i.addEventListener(this,c,d),e[c]=d},o=(c,d)=>{e[c]&&(i.removeEventListener(this,c,d),delete e[c])},a=(c,d)=>{this.canvas&&this.resize(c,d)},s,l=()=>{o("attach",l),this.attached=!0,this.resize(),r("resize",a),r("detach",s)};s=()=>{this.attached=!1,o("resize",a),this._stop(),this._resize(0,0),r("attach",l)},i.isAttached(this.canvas)?l():s()}unbindEvents(){ui(this._listeners,(e,i)=>{this.platform.removeEventListener(this,i,e)}),this._listeners={},ui(this._responsiveListeners,(e,i)=>{this.platform.removeEventListener(this,i,e)}),this._responsiveListeners=void 0}updateHoverStyle(e,i,r){let o=r?"set":"remove",a,s,l,c;for(i==="dataset"&&(a=this.getDatasetMeta(e[0].datasetIndex),a.controller["_"+o+"DatasetHoverStyle"]()),l=0,c=e.length;l{let l=this.getDatasetMeta(a);if(!l)throw new Error("No dataset found at index "+a);return{datasetIndex:a,element:l.data[s],index:s}});!$g(r,i)&&(this._active=r,this._lastEvent=null,this._updateHoverStyles(r,i))}notifyPlugins(e,i,r){return this._plugins.notify(this,e,i,r)}isPluginEnabled(e){return this._plugins._cache.filter(i=>i.plugin.id===e).length===1}_updateHoverStyles(e,i,r){let o=this.options.hover,a=(c,d)=>c.filter(p=>!d.some(_=>p.datasetIndex===_.datasetIndex&&p.index===_.index)),s=a(i,e),l=r?e:a(e,i);s.length&&this.updateHoverStyle(s,o.mode,!1),l.length&&o.mode&&this.updateHoverStyle(l,o.mode,!0)}_eventHandler(e,i){let r={event:e,replay:i,cancelable:!0,inChartArea:this.isPointInArea(e)},o=s=>(s.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins("beforeEvent",r,o)===!1)return;let a=this._handleEvent(e,i,r.inChartArea);return r.cancelable=!1,this.notifyPlugins("afterEvent",r,o),(a||r.changed)&&this.render(),this}_handleEvent(e,i,r){let{_active:o=[],options:a}=this,s=i,l=this._getActiveElements(e,o,r,s),c=Rj(e),d=uae(e,this._lastEvent,r,c);r&&(this._lastEvent=null,ki(a.onHover,[e,l,this],this),c&&ki(a.onClick,[e,l,this],this));let p=!$g(l,o);return(p||i)&&(this._active=l,this._updateHoverStyles(l,o,i)),this._lastEvent=d,p}_getActiveElements(e,i,r,o){if(e.type==="mouseout")return[];if(!r)return i;let a=this.options.hover;return this.getElementsAtEventForMode(e,a.mode,a,o)}}return t})();function O4(){return ui(vC.instances,t=>t._plugins.invalidate())}function mae(t,n,e){let{startAngle:i,x:r,y:o,outerRadius:a,innerRadius:s,options:l}=n,{borderWidth:c,borderJoinStyle:d}=l,p=Math.min(c/a,pr(i-e));if(t.beginPath(),t.arc(r,o,a-c/2,i+p/2,e-p/2),s>0){let _=Math.min(c/s,pr(i-e));t.arc(r,o,s+c/2,e-_/2,i+_/2,!0)}else{let _=Math.min(c/2,a*pr(i-e));if(d==="round")t.arc(r,o,_,e-Jt/2,i+Jt/2,!0);else if(d==="bevel"){let b=2*_*_,y=-b*Math.cos(e+Jt/2)+r,w=-b*Math.sin(e+Jt/2)+o,C=b*Math.cos(i+Jt/2)+r,D=b*Math.sin(i+Jt/2)+o;t.lineTo(y,w),t.lineTo(C,D)}}t.closePath(),t.moveTo(0,0),t.rect(0,0,t.canvas.width,t.canvas.height),t.clip("evenodd")}function hae(t,n,e){let{startAngle:i,pixelMargin:r,x:o,y:a,outerRadius:s,innerRadius:l}=n,c=r/s;t.beginPath(),t.arc(o,a,s,i-c,e+c),l>r?(c=r/l,t.arc(o,a,l,e+c,i-c,!0)):t.arc(o,a,r,e+rn,i-rn),t.closePath(),t.clip()}function pae(t){return eC(t,["outerStart","outerEnd","innerStart","innerEnd"])}function fae(t,n,e,i){let r=pae(t.options.borderRadius),o=(e-n)/2,a=Math.min(o,i*n/2),s=l=>{let c=(e-Math.min(o,l))*i/2;return On(l,0,Math.min(o,c))};return{outerStart:s(r.outerStart),outerEnd:s(r.outerEnd),innerStart:On(r.innerStart,0,a),innerEnd:On(r.innerEnd,0,a)}}function qh(t,n,e,i){return{x:e+t*Math.cos(n),y:i+t*Math.sin(n)}}function gC(t,n,e,i,r,o){let{x:a,y:s,startAngle:l,pixelMargin:c,innerRadius:d}=n,p=Math.max(n.outerRadius+i+e-c,0),_=d>0?d+i+e+c:0,b=0,y=r-l;if(i){let bi=d>0?d-i:0,Hi=p>0?p-i:0,an=(bi+Hi)/2,Ia=an!==0?y*an/(an+i):y;b=(y-Ia)/2}let w=Math.max(.001,y*p-e/Jt)/p,C=(y-w)/2,D=l+C+b,F=r-C-b,{outerStart:W,outerEnd:Z,innerStart:K,innerEnd:oe}=fae(n,_,p,F-D),Se=p-W,ve=p-Z,Be=D+W/Se,wt=F-Z/ve,Ct=_+K,Ht=_+oe,Jn=D+K/Ct,vo=F-oe/Ht;if(t.beginPath(),o){let bi=(Be+wt)/2;if(t.arc(a,s,p,Be,bi),t.arc(a,s,p,bi,wt),Z>0){let br=qh(ve,wt,a,s);t.arc(br.x,br.y,Z,wt,F+rn)}let Hi=qh(Ht,F,a,s);if(t.lineTo(Hi.x,Hi.y),oe>0){let br=qh(Ht,vo,a,s);t.arc(br.x,br.y,oe,F+rn,vo+Math.PI)}let an=(F-oe/_+(D+K/_))/2;if(t.arc(a,s,_,F-oe/_,an,!0),t.arc(a,s,_,an,D+K/_,!0),K>0){let br=qh(Ct,Jn,a,s);t.arc(br.x,br.y,K,Jn+Math.PI,D-rn)}let Ia=qh(Se,D,a,s);if(t.lineTo(Ia.x,Ia.y),W>0){let br=qh(Se,Be,a,s);t.arc(br.x,br.y,W,D-rn,Be)}}else{t.moveTo(a,s);let bi=Math.cos(Be)*p+a,Hi=Math.sin(Be)*p+s;t.lineTo(bi,Hi);let an=Math.cos(wt)*p+a,Ia=Math.sin(wt)*p+s;t.lineTo(an,Ia)}t.closePath()}function gae(t,n,e,i,r){let{fullCircles:o,startAngle:a,circumference:s}=n,l=n.endAngle;if(o){gC(t,n,e,i,l,r);for(let c=0;c=Jt&&b===0&&d!=="miter"&&mae(t,n,w),o||(gC(t,n,e,i,w,r),t.stroke())}var KT=class extends rs{static id="arc";static defaults={borderAlign:"center",borderColor:"#fff",borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1};static defaultRoutes={backgroundColor:"backgroundColor"};static descriptors={_scriptable:!0,_indexable:n=>n!=="borderDash"};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(n){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,n&&Object.assign(this,n)}inRange(n,e,i){let r=this.getProps(["x","y"],i),{angle:o,distance:a}=aT(r,{x:n,y:e}),{startAngle:s,endAngle:l,innerRadius:c,outerRadius:d,circumference:p}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),_=(this.options.spacing+this.options.borderWidth)/2,b=ut(p,l-s),y=Uh(o,s,l)&&s!==l,w=b>=Pi||y,C=Ys(a,c+_,d+_);return w&&C}getCenterPoint(n){let{x:e,y:i,startAngle:r,endAngle:o,innerRadius:a,outerRadius:s}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],n),{offset:l,spacing:c}=this.options,d=(r+o)/2,p=(a+s+c+l)/2;return{x:e+Math.cos(d)*p,y:i+Math.sin(d)*p}}tooltipPosition(n){return this.getCenterPoint(n)}draw(n){let{options:e,circumference:i}=this,r=(e.offset||0)/4,o=(e.spacing||0)/2,a=e.circular;if(this.pixelMargin=e.borderAlign==="inner"?.33:0,this.fullCircles=i>Pi?Math.floor(i/Pi):0,i===0||this.innerRadius<0||this.outerRadius<0)return;n.save();let s=(this.startAngle+this.endAngle)/2;n.translate(Math.cos(s)*r,Math.sin(s)*r);let l=1-Math.sin(Math.min(Jt,i||0)),c=r*l;n.fillStyle=e.backgroundColor,n.strokeStyle=e.borderColor,gae(n,this,c,o,a),_ae(n,this,c,o,a),n.restore()}};function gH(t,n,e=n){t.lineCap=ut(e.borderCapStyle,n.borderCapStyle),t.setLineDash(ut(e.borderDash,n.borderDash)),t.lineDashOffset=ut(e.borderDashOffset,n.borderDashOffset),t.lineJoin=ut(e.borderJoinStyle,n.borderJoinStyle),t.lineWidth=ut(e.borderWidth,n.borderWidth),t.strokeStyle=ut(e.borderColor,n.borderColor)}function bae(t,n,e){t.lineTo(e.x,e.y)}function vae(t){return t.stepped?Wj:t.tension||t.cubicInterpolationMode==="monotone"?Gj:bae}function _H(t,n,e={}){let i=t.length,{start:r=0,end:o=i-1}=e,{start:a,end:s}=n,l=Math.max(r,a),c=Math.min(o,s),d=rs&&o>s;return{count:i,start:l,loop:n.loop,ilen:c(a+(c?s-Z:Z))%o,W=()=>{w!==C&&(t.lineTo(d,C),t.lineTo(d,w),t.lineTo(d,D))};for(l&&(b=r[F(0)],t.moveTo(b.x,b.y)),_=0;_<=s;++_){if(b=r[F(_)],b.skip)continue;let Z=b.x,K=b.y,oe=Z|0;oe===y?(KC&&(C=K),d=(p*d+Z)/++p):(W(),t.lineTo(Z,K),y=oe,p=0,w=C=K),D=K}W()}function ZT(t){let n=t.options,e=n.borderDash&&n.borderDash.length;return!t._decimated&&!t._loop&&!n.tension&&n.cubicInterpolationMode!=="monotone"&&!n.stepped&&!e?xae:yae}function Cae(t){return t.stepped?t4:t.tension||t.cubicInterpolationMode==="monotone"?i4:zc}function wae(t,n,e,i){let r=n._path;r||(r=n._path=new Path2D,n.path(r,e,i)&&r.closePath()),gH(t,n.options),t.stroke(r)}function Dae(t,n,e,i){let{segments:r,options:o}=n,a=ZT(n);for(let s of r)gH(t,o,s.style),t.beginPath(),a(t,n,s,{start:e,end:e+i-1})&&t.closePath(),t.stroke()}var Mae=typeof Path2D=="function";function Eae(t,n,e,i){Mae&&!n.options.segment?wae(t,n,e,i):Dae(t,n,e,i)}var yC=(()=>{class t extends rs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:e=>e!=="borderDash"&&e!=="fill"};constructor(e){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,e&&Object.assign(this,e)}updateControlPoints(e,i){let r=this.options;if((r.tension||r.cubicInterpolationMode==="monotone")&&!r.stepped&&!this._pointsUpdated){let o=r.spanGaps?this._loop:this._fullLoop;Xj(this._points,r,e,o,i),this._pointsUpdated=!0}}set points(e){this._points=e,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=r4(this,this.options.segment))}first(){let e=this.segments,i=this.points;return e.length&&i[e[0].start]}last(){let e=this.segments,i=this.points,r=e.length;return r&&i[e[r-1].end]}interpolate(e,i){let r=this.options,o=e[i],a=this.points,s=ST(this,{property:i,start:o,end:o});if(!s.length)return;let l=[],c=Cae(r),d,p;for(d=0,p=s.length;d{class t extends rs{static id="point";parsed;skip;stop;static defaults={borderWidth:1,hitRadius:1,hoverBorderWidth:1,hoverRadius:4,pointStyle:"circle",radius:3,rotation:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};constructor(e){super(),this.options=void 0,this.parsed=void 0,this.skip=void 0,this.stop=void 0,e&&Object.assign(this,e)}inRange(e,i,r){let o=this.options,{x:a,y:s}=this.getProps(["x","y"],r);return Math.pow(e-a,2)+Math.pow(i-s,2)t.replace("rgb(","rgba(").replace(")",", 0.5)"));function vH(t){return JT[t%JT.length]}function yH(t){return P4[t%P4.length]}function Pae(t,n){return t.borderColor=vH(n),t.backgroundColor=yH(n),++n}function Fae(t,n){return t.backgroundColor=t.data.map(()=>vH(n++)),n}function Nae(t,n){return t.backgroundColor=t.data.map(()=>yH(n++)),n}function Lae(t){let n=0;return(e,i)=>{let r=t.getDatasetMeta(i).controller;r instanceof sI?n=Fae(e,n):r instanceof sH?n=Nae(e,n):r&&(n=Pae(e,n))}}function F4(t){let n;for(n in t)if(t[n].borderColor||t[n].backgroundColor)return!0;return!1}function Vae(t){return t&&(t.borderColor||t.backgroundColor)}function Bae(){return Ti.borderColor!=="rgba(0,0,0,0.1)"||Ti.backgroundColor!=="rgba(0,0,0,0.1)"}var jae={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,n,e){if(!e.enabled)return;let{data:{datasets:i},options:r}=t.config,{elements:o}=r,a=F4(i)||Vae(r)||o&&F4(o)||Bae();if(!e.forceOverride&&a)return;let s=Lae(t);i.forEach(s)}};function Hae(t,n,e,i,r){let o=r.samples||i;if(o>=e)return t.slice(n,n+e);let a=[],s=(e-2)/(o-2),l=0,c=n+e-1,d=n,p,_,b,y,w;for(a[l++]=t[d],p=0;pb&&(b=y,_=t[F],w=F);a[l++]=_,d=w}return a[l++]=t[c],a}function zae(t,n,e,i){let r=0,o=0,a,s,l,c,d,p,_,b,y,w,C=[],D=n+e-1,F=t[n].x,Z=t[D].x-F;for(a=n;aw&&(w=c,_=a),r=(o*r+s.x)/++o;else{let oe=a-1;if(!Nt(p)&&!Nt(_)){let Se=Math.min(p,_),ve=Math.max(p,_);Se!==b&&Se!==oe&&C.push(Me(I({},t[Se]),{x:r})),ve!==b&&ve!==oe&&C.push(Me(I({},t[ve]),{x:r}))}a>0&&oe!==b&&C.push(t[oe]),C.push(s),d=K,o=0,y=w=c,p=_=b=a}}return C}function xH(t){if(t._decimated){let n=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:n})}}function N4(t){t.data.datasets.forEach(n=>{xH(n)})}function Uae(t,n){let e=n.length,i=0,r,{iScale:o}=t,{min:a,max:s,minDefined:l,maxDefined:c}=o.getUserBounds();return l&&(i=On(Ws(n,o.axis,a).lo,0,e-1)),c?r=On(Ws(n,o.axis,s).hi+1,i,e)-i:r=e-i,{start:i,count:r}}var $ae={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,n,e)=>{if(!e.enabled){N4(t);return}let i=t.width;t.data.datasets.forEach((r,o)=>{let{_data:a,indexAxis:s}=r,l=t.getDatasetMeta(o),c=a||r.data;if(Gh([s,t.options.indexAxis])==="y"||!l.controller.supportsDecimation)return;let d=t.scales[l.xAxisID];if(d.type!=="linear"&&d.type!=="time"||t.options.parsing)return;let{start:p,count:_}=Uae(l,c),b=e.threshold||4*i;if(_<=b){xH(r);return}Nt(a)&&(r._data=c,delete r.data,Object.defineProperty(r,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(w){this._data=w}}));let y;switch(e.algorithm){case"lttb":y=Hae(c,p,_,i,e);break;case"min-max":y=zae(c,p,_,i);break;default:throw new Error(`Unsupported decimation algorithm '${e.algorithm}'`)}r._decimated=y})},destroy(t){N4(t)}};function Wae(t,n,e){let i=t.segments,r=t.points,o=n.points,a=[];for(let s of i){let{start:l,end:c}=s;c=xC(l,c,r);let d=eI(e,r[l],r[c],s.loop);if(!n.segments){a.push({source:s,target:d,start:r[l],end:r[c]});continue}let p=ST(n,d);for(let _ of p){let b=eI(e,o[_.start],o[_.end],_.loop),y=ET(s,r,b);for(let w of y)a.push({source:w,target:_,start:{[e]:L4(d,b,"start",Math.max)},end:{[e]:L4(d,b,"end",Math.min)}})}}return a}function eI(t,n,e,i){if(i)return;let r=n[t],o=e[t];return t==="angle"&&(r=pr(r),o=pr(o)),{property:t,start:r,end:o}}function Gae(t,n){let{x:e=null,y:i=null}=t||{},r=n.points,o=[];return n.segments.forEach(({start:a,end:s})=>{s=xC(a,s,r);let l=r[a],c=r[s];i!==null?(o.push({x:l.x,y:i}),o.push({x:c.x,y:i})):e!==null&&(o.push({x:e,y:l.y}),o.push({x:e,y:c.y}))}),o}function xC(t,n,e){for(;n>t;n--){let i=e[n];if(!isNaN(i.x)&&!isNaN(i.y))break}return n}function L4(t,n,e,i){return t&&n?i(t[e],n[e]):t?t[e]:n?n[e]:0}function CH(t,n){let e=[],i=!1;return Ri(t)?(i=!0,e=t):e=Gae(t,n),e.length?new yC({points:e,options:{tension:0},_loop:i,_fullLoop:i}):null}function V4(t){return t&&t.fill!==!1}function qae(t,n,e){let r=t[n].fill,o=[n],a;if(!e)return r;for(;r!==!1&&o.indexOf(r)===-1;){if(!Xi(r))return r;if(a=t[r],!a)return!1;if(a.visible)return r;o.push(r),r=a.fill}return!1}function Yae(t,n,e){let i=Xae(t);if(Bt(i))return isNaN(i.value)?!1:i;let r=parseFloat(i);return Xi(r)&&Math.floor(r)===r?Qae(i[0],n,r,e):["origin","start","end","stack","shape"].indexOf(i)>=0&&i}function Qae(t,n,e,i){return(t==="-"||t==="+")&&(e=n+e),e===n||e<0||e>=i?!1:e}function Kae(t,n){let e=null;return t==="start"?e=n.bottom:t==="end"?e=n.top:Bt(t)?e=n.getPixelForValue(t.value):n.getBasePixel&&(e=n.getBasePixel()),e}function Zae(t,n,e){let i;return t==="start"?i=e:t==="end"?i=n.options.reverse?n.min:n.max:Bt(t)?i=t.value:i=n.getBaseValue(),i}function Xae(t){let n=t.options,e=n.fill,i=ut(e&&e.target,e);return i===void 0&&(i=!!n.backgroundColor),i===!1||i===null?!1:i===!0?"origin":i}function Jae(t){let{scale:n,index:e,line:i}=t,r=[],o=i.segments,a=i.points,s=ese(n,e);s.push(CH({x:null,y:n.bottom},i));for(let l=0;l=0;--a){let s=r[a].$filler;s&&(s.line.updateControlPoints(o,s.axis),i&&s.fill&&VT(t.ctx,s,o))}},beforeDatasetsDraw(t,n,e){if(e.drawTime!=="beforeDatasetsDraw")return;let i=t.getSortedVisibleDatasetMetas();for(let r=i.length-1;r>=0;--r){let o=i[r].$filler;V4(o)&&VT(t.ctx,o,t.chartArea)}},beforeDatasetDraw(t,n,e){let i=n.meta.$filler;!V4(i)||e.drawTime!=="beforeDatasetDraw"||VT(t.ctx,i,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}},z4=(t,n)=>{let{boxHeight:e=n,boxWidth:i=n}=t;return t.usePointStyle&&(e=Math.min(e,n),i=t.pointStyleWidth||Math.min(i,n)),{boxWidth:i,boxHeight:e,itemHeight:Math.max(n,e)}},use=(t,n)=>t!==null&&n!==null&&t.datasetIndex===n.datasetIndex&&t.index===n.index,bC=class extends rs{constructor(n){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=n.chart,this.options=n.options,this.ctx=n.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(n,e,i){this.maxWidth=n,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){let n=this.options.labels||{},e=ki(n.generateLabels,[this.chart],this)||[];n.filter&&(e=e.filter(i=>n.filter(i,this.chart.data))),n.sort&&(e=e.sort((i,r)=>n.sort(i,r,this.chart.data))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){let{options:n,ctx:e}=this;if(!n.display){this.width=this.height=0;return}let i=n.labels,r=vn(i.font),o=r.size,a=this._computeTitleHeight(),{boxWidth:s,itemHeight:l}=z4(i,o),c,d;e.font=r.string,this.isHorizontal()?(c=this.maxWidth,d=this._fitRows(a,o,s,l)+10):(d=this.maxHeight,c=this._fitCols(a,r,s,l)+10),this.width=Math.min(c,n.maxWidth||this.maxWidth),this.height=Math.min(d,n.maxHeight||this.maxHeight)}_fitRows(n,e,i,r){let{ctx:o,maxWidth:a,options:{labels:{padding:s}}}=this,l=this.legendHitBoxes=[],c=this.lineWidths=[0],d=r+s,p=n;o.textAlign="left",o.textBaseline="middle";let _=-1,b=-d;return this.legendItems.forEach((y,w)=>{let C=i+e/2+o.measureText(y.text).width;(w===0||c[c.length-1]+C+2*s>a)&&(p+=d,c[c.length-(w>0?0:1)]=0,b+=d,_++),l[w]={left:0,top:b,row:_,width:C,height:r},c[c.length-1]+=C+s}),p}_fitCols(n,e,i,r){let{ctx:o,maxHeight:a,options:{labels:{padding:s}}}=this,l=this.legendHitBoxes=[],c=this.columnSizes=[],d=a-n,p=s,_=0,b=0,y=0,w=0;return this.legendItems.forEach((C,D)=>{let{itemWidth:F,itemHeight:W}=mse(i,e,o,C,r);D>0&&b+W+2*s>d&&(p+=_+s,c.push({width:_,height:b}),y+=_+s,w++,_=b=0),l[D]={left:y,top:b,col:w,width:F,height:W},_=Math.max(_,F),b+=W+s}),p+=_,c.push({width:_,height:b}),p}adjustHitBoxes(){if(!this.options.display)return;let n=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:r},rtl:o}}=this,a=ku(o,this.left,this.width);if(this.isHorizontal()){let s=0,l=fr(i,this.left+r,this.right-this.lineWidths[s]);for(let c of e)s!==c.row&&(s=c.row,l=fr(i,this.left+r,this.right-this.lineWidths[s])),c.top+=this.top+n+r,c.left=a.leftForLtr(a.x(l),c.width),l+=c.width+r}else{let s=0,l=fr(i,this.top+n+r,this.bottom-this.columnSizes[s].height);for(let c of e)c.col!==s&&(s=c.col,l=fr(i,this.top+n+r,this.bottom-this.columnSizes[s].height)),c.top=l,c.left+=this.left+r,c.left=a.leftForLtr(a.x(c.left),c.width),l+=c.height+r}}isHorizontal(){return this.options.position==="top"||this.options.position==="bottom"}draw(){if(this.options.display){let n=this.ctx;Gg(n,this),this._draw(),qg(n)}}_draw(){let{options:n,columnSizes:e,lineWidths:i,ctx:r}=this,{align:o,labels:a}=n,s=Ti.color,l=ku(n.rtl,this.left,this.width),c=vn(a.font),{padding:d}=a,p=c.size,_=p/2,b;this.drawTitle(),r.textAlign=l.textAlign("left"),r.textBaseline="middle",r.lineWidth=.5,r.font=c.string;let{boxWidth:y,boxHeight:w,itemHeight:C}=z4(a,p),D=function(oe,Se,ve){if(isNaN(y)||y<=0||isNaN(w)||w<0)return;r.save();let Be=ut(ve.lineWidth,1);if(r.fillStyle=ut(ve.fillStyle,s),r.lineCap=ut(ve.lineCap,"butt"),r.lineDashOffset=ut(ve.lineDashOffset,0),r.lineJoin=ut(ve.lineJoin,"miter"),r.lineWidth=Be,r.strokeStyle=ut(ve.strokeStyle,s),r.setLineDash(ut(ve.lineDash,[])),a.usePointStyle){let wt={radius:w*Math.SQRT2/2,pointStyle:ve.pointStyle,rotation:ve.rotation,borderWidth:Be},Ct=l.xPlus(oe,y/2),Ht=Se+_;gT(r,wt,Ct,Ht,a.pointStyleWidth&&y)}else{let wt=Se+Math.max((p-w)/2,0),Ct=l.leftForLtr(oe,y),Ht=qc(ve.borderRadius);r.beginPath(),Object.values(Ht).some(Jn=>Jn!==0)?Wh(r,{x:Ct,y:wt,w:y,h:w,radius:Ht}):r.rect(Ct,wt,y,w),r.fill(),Be!==0&&r.stroke()}r.restore()},F=function(oe,Se,ve){Gc(r,ve.text,oe,Se+C/2,c,{strikethrough:ve.hidden,textAlign:l.textAlign(ve.textAlign)})},W=this.isHorizontal(),Z=this._computeTitleHeight();W?b={x:fr(o,this.left+d,this.right-i[0]),y:this.top+d+Z,line:0}:b={x:this.left+d,y:fr(o,this.top+Z+d,this.bottom-e[0].height),line:0},DT(this.ctx,n.textDirection);let K=C+d;this.legendItems.forEach((oe,Se)=>{r.strokeStyle=oe.fontColor,r.fillStyle=oe.fontColor;let ve=r.measureText(oe.text).width,Be=l.textAlign(oe.textAlign||(oe.textAlign=a.textAlign)),wt=y+_+ve,Ct=b.x,Ht=b.y;l.setWidth(this.width),W?Se>0&&Ct+wt+d>this.right&&(Ht=b.y+=K,b.line++,Ct=b.x=fr(o,this.left+d,this.right-i[b.line])):Se>0&&Ht+K>this.bottom&&(Ct=b.x=Ct+e[b.line].width+d,b.line++,Ht=b.y=fr(o,this.top+Z+d,this.bottom-e[b.line].height));let Jn=l.x(Ct);if(D(Jn,Ht,oe),Ct=zj(Be,Ct+y+_,W?Ct+wt:this.right,n.rtl),F(l.x(Ct),Ht,oe),W)b.x+=wt+d;else if(typeof oe.text!="string"){let vo=c.lineHeight;b.y+=wH(oe,vo)+d}else b.y+=K}),MT(this.ctx,n.textDirection)}drawTitle(){let n=this.options,e=n.title,i=vn(e.font),r=gr(e.padding);if(!e.display)return;let o=ku(n.rtl,this.left,this.width),a=this.ctx,s=e.position,l=i.size/2,c=r.top+l,d,p=this.left,_=this.width;if(this.isHorizontal())_=Math.max(...this.lineWidths),d=this.top+c,p=fr(n.align,p,this.right-_);else{let y=this.columnSizes.reduce((w,C)=>Math.max(w,C.height),0);d=c+fr(n.align,this.top,this.bottom-y-n.labels.padding-this._computeTitleHeight())}let b=fr(s,p,p+_);a.textAlign=o.textAlign(Zx(s)),a.textBaseline="middle",a.strokeStyle=e.color,a.fillStyle=e.color,a.font=i.string,Gc(a,e.text,b,d,i)}_computeTitleHeight(){let n=this.options.title,e=vn(n.font),i=gr(n.padding);return n.display?e.lineHeight+i.height:0}_getLegendItemAt(n,e){let i,r,o;if(Ys(n,this.left,this.right)&&Ys(e,this.top,this.bottom)){for(o=this.legendHitBoxes,i=0;io.length>a.length?o:a)),n+e.size/2+i.measureText(r).width}function pse(t,n,e){let i=t;return typeof n.text!="string"&&(i=wH(n,e)),i}function wH(t,n){let e=t.text?t.text.length:0;return n*e}function fse(t,n){return!!((t==="mousemove"||t==="mouseout")&&(n.onHover||n.onLeave)||n.onClick&&(t==="click"||t==="mouseup"))}var gse={id:"legend",_element:bC,start(t,n,e){let i=t.legend=new bC({ctx:t.ctx,options:e,chart:t});Pr.configure(t,i,e),Pr.addBox(t,i)},stop(t){Pr.removeBox(t,t.legend),delete t.legend},beforeUpdate(t,n,e){let i=t.legend;Pr.configure(t,i,e),i.options=e},afterUpdate(t){let n=t.legend;n.buildLabels(),n.adjustHitBoxes()},afterEvent(t,n){n.replay||t.legend.handleEvent(n.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(t,n,e){let i=n.datasetIndex,r=e.chart;r.isDatasetVisible(i)?(r.hide(i),n.hidden=!0):(r.show(i),n.hidden=!1)},onHover:null,onLeave:null,labels:{color:t=>t.chart.options.color,boxWidth:40,padding:10,generateLabels(t){let n=t.data.datasets,{labels:{usePointStyle:e,pointStyle:i,textAlign:r,color:o,useBorderRadius:a,borderRadius:s}}=t.legend.options;return t._getSortedDatasetMetas().map(l=>{let c=l.controller.getStyle(e?0:void 0),d=gr(c.borderWidth);return{text:n[l.index].label,fillStyle:c.backgroundColor,fontColor:o,hidden:!l.visible,lineCap:c.borderCapStyle,lineDash:c.borderDash,lineDashOffset:c.borderDashOffset,lineJoin:c.borderJoinStyle,lineWidth:(d.width+d.height)/4,strokeStyle:c.borderColor,pointStyle:i||c.pointStyle,rotation:c.rotation,textAlign:r||c.textAlign,borderRadius:a&&(s||c.borderRadius),datasetIndex:l.index}},this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}},i_=class extends rs{constructor(n){super(),this.chart=n.chart,this.options=n.options,this.ctx=n.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(n,e){let i=this.options;if(this.left=0,this.top=0,!i.display){this.width=this.height=this.right=this.bottom=0;return}this.width=this.right=n,this.height=this.bottom=e;let r=Ri(i.text)?i.text.length:1;this._padding=gr(i.padding);let o=r*vn(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){let n=this.options.position;return n==="top"||n==="bottom"}_drawArgs(n){let{top:e,left:i,bottom:r,right:o,options:a}=this,s=a.align,l=0,c,d,p;return this.isHorizontal()?(d=fr(s,i,o),p=e+n,c=o-i):(a.position==="left"?(d=i+n,p=fr(s,r,e),l=Jt*-.5):(d=o-n,p=fr(s,e,r),l=Jt*.5),c=r-e),{titleX:d,titleY:p,maxWidth:c,rotation:l}}draw(){let n=this.ctx,e=this.options;if(!e.display)return;let i=vn(e.font),o=i.lineHeight/2+this._padding.top,{titleX:a,titleY:s,maxWidth:l,rotation:c}=this._drawArgs(o);Gc(n,e.text,0,0,i,{color:e.color,maxWidth:l,rotation:c,textAlign:Zx(e.align),textBaseline:"middle",translation:[a,s]})}};function _se(t,n){let e=new i_({ctx:t.ctx,options:n,chart:t});Pr.configure(t,e,n),Pr.addBox(t,e),t.titleBlock=e}var bse={id:"title",_element:i_,start(t,n,e){_se(t,e)},stop(t){let n=t.titleBlock;Pr.removeBox(t,n),delete t.titleBlock},beforeUpdate(t,n,e){let i=t.titleBlock;Pr.configure(t,i,e),i.options=e},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}},lC=new WeakMap,vse={id:"subtitle",start(t,n,e){let i=new i_({ctx:t.ctx,options:e,chart:t});Pr.configure(t,i,e),Pr.addBox(t,i),lC.set(t,i)},stop(t){Pr.removeBox(t,lC.get(t)),lC.delete(t)},beforeUpdate(t,n,e){let i=lC.get(t);Pr.configure(t,i,e),i.options=e},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}},e_={average(t){if(!t.length)return!1;let n,e,i=new Set,r=0,o=0;for(n=0,e=t.length;ns+l)/i.size,y:r/o}},nearest(t,n){if(!t.length)return!1;let e=n.x,i=n.y,r=Number.POSITIVE_INFINITY,o,a,s;for(o=0,a=t.length;o-1?t.split(` +`):t}function yse(t,n){let{element:e,datasetIndex:i,index:r}=n,o=t.getDatasetMeta(i).controller,{label:a,value:s}=o.getLabelAndValue(r);return{chart:t,label:a,parsed:o.getParsed(r),raw:t.data.datasets[i].data[r],formattedValue:s,dataset:o.getDataset(),dataIndex:r,datasetIndex:i,element:e}}function U4(t,n){let e=t.chart.ctx,{body:i,footer:r,title:o}=t,{boxWidth:a,boxHeight:s}=n,l=vn(n.bodyFont),c=vn(n.titleFont),d=vn(n.footerFont),p=o.length,_=r.length,b=i.length,y=gr(n.padding),w=y.height,C=0,D=i.reduce((Z,K)=>Z+K.before.length+K.lines.length+K.after.length,0);if(D+=t.beforeBody.length+t.afterBody.length,p&&(w+=p*c.lineHeight+(p-1)*n.titleSpacing+n.titleMarginBottom),D){let Z=n.displayColors?Math.max(s,l.lineHeight):l.lineHeight;w+=b*Z+(D-b)*l.lineHeight+(D-1)*n.bodySpacing}_&&(w+=n.footerMarginTop+_*d.lineHeight+(_-1)*n.footerSpacing);let F=0,W=function(Z){C=Math.max(C,e.measureText(Z).width+F)};return e.save(),e.font=c.string,ui(t.title,W),e.font=l.string,ui(t.beforeBody.concat(t.afterBody),W),F=n.displayColors?a+2+n.boxPadding:0,ui(i,Z=>{ui(Z.before,W),ui(Z.lines,W),ui(Z.after,W)}),F=0,e.font=d.string,ui(t.footer,W),e.restore(),C+=y.width,{width:C,height:w}}function xse(t,n){let{y:e,height:i}=n;return et.height-i/2?"bottom":"center"}function Cse(t,n,e,i){let{x:r,width:o}=i,a=e.caretSize+e.caretPadding;if(t==="left"&&r+o+a>n.width||t==="right"&&r-o-a<0)return!0}function wse(t,n,e,i){let{x:r,width:o}=e,{width:a,chartArea:{left:s,right:l}}=t,c="center";return i==="center"?c=r<=(s+l)/2?"left":"right":r<=o/2?c="left":r>=a-o/2&&(c="right"),Cse(c,t,n,e)&&(c="center"),c}function $4(t,n,e){let i=e.yAlign||n.yAlign||xse(t,e);return{xAlign:e.xAlign||n.xAlign||wse(t,n,e,i),yAlign:i}}function Dse(t,n){let{x:e,width:i}=t;return n==="right"?e-=i:n==="center"&&(e-=i/2),e}function Mse(t,n,e){let{y:i,height:r}=t;return n==="top"?i+=e:n==="bottom"?i-=r+e:i-=r/2,i}function W4(t,n,e,i){let{caretSize:r,caretPadding:o,cornerRadius:a}=t,{xAlign:s,yAlign:l}=e,c=r+o,{topLeft:d,topRight:p,bottomLeft:_,bottomRight:b}=qc(a),y=Dse(n,s),w=Mse(n,l,c);return l==="center"?s==="left"?y+=c:s==="right"&&(y-=c):s==="left"?y-=Math.max(d,_)+r:s==="right"&&(y+=Math.max(p,b)+r),{x:On(y,0,i.width-n.width),y:On(w,0,i.height-n.height)}}function cC(t,n,e){let i=gr(e.padding);return n==="center"?t.x+t.width/2:n==="right"?t.x+t.width-i.right:t.x+i.left}function G4(t){return Qs([],Ll(t))}function Ese(t,n,e){return Fl(t,{tooltip:n,tooltipItems:e,type:"tooltip"})}function q4(t,n){let e=n&&n.dataset&&n.dataset.tooltip&&n.dataset.tooltip.callbacks;return e?t.override(e):t}var DH={beforeTitle:qs,title(t){if(t.length>0){let n=t[0],e=n.chart.data.labels,i=e?e.length:0;if(this&&this.options&&this.options.mode==="dataset")return n.dataset.label||"";if(n.label)return n.label;if(i>0&&n.dataIndex"u"?DH[n].call(e,i):r}var Y4=(()=>{class t extends rs{static positioners=e_;constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){let e=this._cachedAnimations;if(e)return e;let i=this.chart,r=this.options.setContext(this.getContext()),o=r.enabled&&i.options.animation&&r.animations,a=new hC(this.chart,o);return o._cacheable&&(this._cachedAnimations=Object.freeze(a)),a}getContext(){return this.$context||(this.$context=Ese(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,i){let{callbacks:r}=i,o=fo(r,"beforeTitle",this,e),a=fo(r,"title",this,e),s=fo(r,"afterTitle",this,e),l=[];return l=Qs(l,Ll(o)),l=Qs(l,Ll(a)),l=Qs(l,Ll(s)),l}getBeforeBody(e,i){return G4(fo(i.callbacks,"beforeBody",this,e))}getBody(e,i){let{callbacks:r}=i,o=[];return ui(e,a=>{let s={before:[],lines:[],after:[]},l=q4(r,a);Qs(s.before,Ll(fo(l,"beforeLabel",this,a))),Qs(s.lines,fo(l,"label",this,a)),Qs(s.after,Ll(fo(l,"afterLabel",this,a))),o.push(s)}),o}getAfterBody(e,i){return G4(fo(i.callbacks,"afterBody",this,e))}getFooter(e,i){let{callbacks:r}=i,o=fo(r,"beforeFooter",this,e),a=fo(r,"footer",this,e),s=fo(r,"afterFooter",this,e),l=[];return l=Qs(l,Ll(o)),l=Qs(l,Ll(a)),l=Qs(l,Ll(s)),l}_createItems(e){let i=this._active,r=this.chart.data,o=[],a=[],s=[],l=[],c,d;for(c=0,d=i.length;ce.filter(p,_,b,r))),e.itemSort&&(l=l.sort((p,_)=>e.itemSort(p,_,r))),ui(l,p=>{let _=q4(e.callbacks,p);o.push(fo(_,"labelColor",this,p)),a.push(fo(_,"labelPointStyle",this,p)),s.push(fo(_,"labelTextColor",this,p))}),this.labelColors=o,this.labelPointStyles=a,this.labelTextColors=s,this.dataPoints=l,l}update(e,i){let r=this.options.setContext(this.getContext()),o=this._active,a,s=[];if(!o.length)this.opacity!==0&&(a={opacity:0});else{let l=e_[r.position].call(this,o,this._eventPosition);s=this._createItems(r),this.title=this.getTitle(s,r),this.beforeBody=this.getBeforeBody(s,r),this.body=this.getBody(s,r),this.afterBody=this.getAfterBody(s,r),this.footer=this.getFooter(s,r);let c=this._size=U4(this,r),d=Object.assign({},l,c),p=$4(this.chart,r,d),_=W4(r,d,p,this.chart);this.xAlign=p.xAlign,this.yAlign=p.yAlign,a={opacity:1,x:_.x,y:_.y,width:c.width,height:c.height,caretX:l.x,caretY:l.y}}this._tooltipItems=s,this.$context=void 0,a&&this._resolveAnimations().update(this,a),e&&r.external&&r.external.call(this,{chart:this.chart,tooltip:this,replay:i})}drawCaret(e,i,r,o){let a=this.getCaretPosition(e,r,o);i.lineTo(a.x1,a.y1),i.lineTo(a.x2,a.y2),i.lineTo(a.x3,a.y3)}getCaretPosition(e,i,r){let{xAlign:o,yAlign:a}=this,{caretSize:s,cornerRadius:l}=r,{topLeft:c,topRight:d,bottomLeft:p,bottomRight:_}=qc(l),{x:b,y}=e,{width:w,height:C}=i,D,F,W,Z,K,oe;return a==="center"?(K=y+C/2,o==="left"?(D=b,F=D-s,Z=K+s,oe=K-s):(D=b+w,F=D+s,Z=K-s,oe=K+s),W=D):(o==="left"?F=b+Math.max(c,p)+s:o==="right"?F=b+w-Math.max(d,_)-s:F=this.caretX,a==="top"?(Z=y,K=Z-s,D=F-s,W=F+s):(Z=y+C,K=Z+s,D=F+s,W=F-s),oe=Z),{x1:D,x2:F,x3:W,y1:Z,y2:K,y3:oe}}drawTitle(e,i,r){let o=this.title,a=o.length,s,l,c;if(a){let d=ku(r.rtl,this.x,this.width);for(e.x=cC(this,r.titleAlign,r),i.textAlign=d.textAlign(r.titleAlign),i.textBaseline="middle",s=vn(r.titleFont),l=r.titleSpacing,i.fillStyle=r.titleColor,i.font=s.string,c=0;cW!==0)?(e.beginPath(),e.fillStyle=a.multiKeyBackground,Wh(e,{x:C,y:w,w:d,h:c,radius:F}),e.fill(),e.stroke(),e.fillStyle=s.backgroundColor,e.beginPath(),Wh(e,{x:D,y:w+1,w:d-2,h:c-2,radius:F}),e.fill()):(e.fillStyle=a.multiKeyBackground,e.fillRect(C,w,d,c),e.strokeRect(C,w,d,c),e.fillStyle=s.backgroundColor,e.fillRect(D,w+1,d-2,c-2))}e.fillStyle=this.labelTextColors[r]}drawBody(e,i,r){let{body:o}=this,{bodySpacing:a,bodyAlign:s,displayColors:l,boxHeight:c,boxWidth:d,boxPadding:p}=r,_=vn(r.bodyFont),b=_.lineHeight,y=0,w=ku(r.rtl,this.x,this.width),C=function(Be){i.fillText(Be,w.x(e.x+y),e.y+b/2),e.y+=b+a},D=w.textAlign(s),F,W,Z,K,oe,Se,ve;for(i.textAlign=s,i.textBaseline="middle",i.font=_.string,e.x=cC(this,D,r),i.fillStyle=r.bodyColor,ui(this.beforeBody,C),y=l&&D!=="right"?s==="center"?d/2+p:d+2+p:0,K=0,Se=o.length;K0&&i.stroke()}_updateAnimationTarget(e){let i=this.chart,r=this.$animations,o=r&&r.x,a=r&&r.y;if(o||a){let s=e_[e.position].call(this,this._active,this._eventPosition);if(!s)return;let l=this._size=U4(this,e),c=Object.assign({},s,this._size),d=$4(i,e,c),p=W4(e,c,d,i);(o._to!==p.x||a._to!==p.y)&&(this.xAlign=d.xAlign,this.yAlign=d.yAlign,this.width=l.width,this.height=l.height,this.caretX=s.x,this.caretY=s.y,this._resolveAnimations().update(this,p))}}_willRender(){return!!this.opacity}draw(e){let i=this.options.setContext(this.getContext()),r=this.opacity;if(!r)return;this._updateAnimationTarget(i);let o={width:this.width,height:this.height},a={x:this.x,y:this.y};r=Math.abs(r)<.001?0:r;let s=gr(i.padding),l=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;i.enabled&&l&&(e.save(),e.globalAlpha=r,this.drawBackground(a,e,o,i),DT(e,i.textDirection),a.y+=s.top,this.drawTitle(a,e,i),this.drawBody(a,e,i),this.drawFooter(a,e,i),MT(e,i.textDirection),e.restore())}getActiveElements(){return this._active||[]}setActiveElements(e,i){let r=this._active,o=e.map(({datasetIndex:l,index:c})=>{let d=this.chart.getDatasetMeta(l);if(!d)throw new Error("Cannot find a dataset at index "+l);return{datasetIndex:l,element:d.data[c],index:c}}),a=!$g(r,o),s=this._positionChanged(o,i);(a||s)&&(this._active=o,this._eventPosition=i,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(e,i,r=!0){if(i&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;let o=this.options,a=this._active||[],s=this._getActiveElements(e,a,i,r),l=this._positionChanged(s,e),c=i||!$g(s,a)||l;return c&&(this._active=s,(o.enabled||o.external)&&(this._eventPosition={x:e.x,y:e.y},this.update(!0,i))),c}_getActiveElements(e,i,r,o){let a=this.options;if(e.type==="mouseout")return[];if(!o)return i.filter(l=>this.chart.data.datasets[l.datasetIndex]&&this.chart.getDatasetMeta(l.datasetIndex).controller.getParsed(l.index)!==void 0);let s=this.chart.getElementsAtEventForMode(e,a.mode,a,r);return a.reverse&&s.reverse(),s}_positionChanged(e,i){let{caretX:r,caretY:o,options:a}=this,s=e_[a.position].call(this,e,i);return s!==!1&&(r!==s.x||o!==s.y)}}return t})(),Sse={id:"tooltip",_element:Y4,positioners:e_,afterInit(t,n,e){e&&(t.tooltip=new Y4({chart:t,options:e}))},beforeUpdate(t,n,e){t.tooltip&&t.tooltip.initialize(e)},reset(t,n,e){t.tooltip&&t.tooltip.initialize(e)},afterDraw(t){let n=t.tooltip;if(n&&n._willRender()){let e={tooltip:n};if(t.notifyPlugins("beforeTooltipDraw",Me(I({},e),{cancelable:!0}))===!1)return;n.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",e)}},afterEvent(t,n){if(t.tooltip){let e=n.replay;t.tooltip.handleEvent(n.event,e,n.inChartArea)&&(n.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,n)=>n.bodyFont.size,boxWidth:(t,n)=>n.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:DH},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>t!=="filter"&&t!=="itemSort"&&t!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},kse=Object.freeze({__proto__:null,Colors:jae,Decimation:$ae,Filler:dse,Legend:gse,SubTitle:vse,Title:bse,Tooltip:Sse}),Tse=(t,n,e,i)=>(typeof n=="string"?(e=t.push(n)-1,i.unshift({index:e,label:n})):isNaN(n)&&(e=null),e);function Ise(t,n,e,i){let r=t.indexOf(n);if(r===-1)return Tse(t,n,e,i);let o=t.lastIndexOf(n);return r!==o?e:r}var Ase=(t,n)=>t===null?null:On(Math.round(t),0,n);function Q4(t){let n=this.getLabels();return t>=0&&t{class t extends Au{static id="category";static defaults={ticks:{callback:Q4}};constructor(e){super(e),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(e){let i=this._addedLabels;if(i.length){let r=this.getLabels();for(let{index:o,label:a}of i)r[o]===a&&r.splice(o,1);this._addedLabels=[]}super.init(e)}parse(e,i){if(Nt(e))return null;let r=this.getLabels();return i=isFinite(i)&&r[i]===e?i:Ise(r,e,ut(i,e),this._addedLabels),Ase(i,r.length-1)}determineDataLimits(){let{minDefined:e,maxDefined:i}=this.getUserBounds(),{min:r,max:o}=this.getMinMax(!0);this.options.bounds==="ticks"&&(e||(r=0),i||(o=this.getLabels().length-1)),this.min=r,this.max=o}buildTicks(){let e=this.min,i=this.max,r=this.options.offset,o=[],a=this.getLabels();a=e===0&&i===a.length-1?a:a.slice(e,i+1),this._valueRange=Math.max(a.length-(r?0:1),1),this._startValue=this.min-(r?.5:0);for(let s=e;s<=i;s++)o.push({value:s});return o}getLabelForValue(e){return Q4.call(this,e)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(e){return typeof e!="number"&&(e=this.parse(e)),e===null?NaN:this.getPixelForDecimal((e-this._startValue)/this._valueRange)}getPixelForTick(e){let i=this.ticks;return e<0||e>i.length-1?null:this.getPixelForValue(i[e].value)}getValueForPixel(e){return Math.round(this._startValue+this.getDecimalForPixel(e)*this._valueRange)}getBasePixel(){return this.bottom}}return t})();function Rse(t,n){let e=[],{bounds:r,step:o,min:a,max:s,precision:l,count:c,maxTicks:d,maxDigits:p,includeBounds:_}=t,b=o||1,y=d-1,{min:w,max:C}=n,D=!Nt(a),F=!Nt(s),W=!Nt(c),Z=(C-w)/(p+1),K=nT((C-w)/y/b)*b,oe,Se,ve,Be;if(K<1e-14&&!D&&!F)return[{value:w},{value:C}];Be=Math.ceil(C/K)-Math.floor(w/K),Be>y&&(K=nT(Be*K/y/b)*b),Nt(l)||(oe=Math.pow(10,l),K=Math.ceil(K*oe)/oe),r==="ticks"?(Se=Math.floor(w/K)*K,ve=Math.ceil(C/K)*K):(Se=w,ve=C),D&&F&&o&&Fj((s-a)/o,K/1e3)?(Be=Math.round(Math.min((s-a)/K,d)),K=(s-a)/Be,Se=a,ve=s):W?(Se=D?a:Se,ve=F?s:ve,Be=c-1,K=(ve-Se)/Be):(Be=(ve-Se)/K,zh(Be,Math.round(Be),K/1e3)?Be=Math.round(Be):Be=Math.ceil(Be));let wt=Math.max(oT(K),oT(Se));oe=Math.pow(10,Nt(l)?wt:l),Se=Math.round(Se*oe)/oe,ve=Math.round(ve*oe)/oe;let Ct=0;for(D&&(_&&Se!==a?(e.push({value:a}),Ses)break;e.push({value:Ht})}return F&&_&&ve!==s?e.length&&zh(e[e.length-1].value,s,K4(s,Z,t))?e[e.length-1].value=s:e.push({value:s}):(!F||ve===s)&&e.push({value:ve}),e}function K4(t,n,{horizontal:e,minRotation:i}){let r=ua(i),o=(e?Math.sin(r):Math.cos(r))||.001,a=.75*n*(""+t).length;return Math.min(n/o,a)}var Qh=class extends Au{constructor(n){super(n),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(n,e){return Nt(n)||(typeof n=="number"||n instanceof Number)&&!isFinite(+n)?null:+n}handleTickRangeOptions(){let{beginAtZero:n}=this.options,{minDefined:e,maxDefined:i}=this.getUserBounds(),{min:r,max:o}=this,a=l=>r=e?r:l,s=l=>o=i?o:l;if(n){let l=ns(r),c=ns(o);l<0&&c<0?s(0):l>0&&c>0&&a(0)}if(r===o){let l=o===0?1:Math.abs(o*.05);s(o+l),n||a(r-l)}this.min=r,this.max=o}getTickLimit(){let n=this.options.ticks,{maxTicksLimit:e,stepSize:i}=n,r;return i?(r=Math.ceil(this.max/i)-Math.floor(this.min/i)+1,r>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${i} would result generating up to ${r} ticks. Limiting to 1000.`),r=1e3)):(r=this.computeTickLimit(),e=e||11),e&&(r=Math.min(e,r)),r}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){let n=this.options,e=n.ticks,i=this.getTickLimit();i=Math.max(2,i);let r={maxTicks:i,bounds:n.bounds,min:n.min,max:n.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:e.includeBounds!==!1},o=this._range||this,a=Rse(r,o);return n.bounds==="ticks"&&rT(a,this,"value"),n.reverse?(a.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),a}configure(){let n=this.ticks,e=this.min,i=this.max;if(super.configure(),this.options.offset&&n.length){let r=(i-e)/Math.max(n.length-1,1)/2;e-=r,i+=r}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(n){return $h(n,this.chart.options.locale,this.options.ticks.format)}},tI=class extends Qh{static id="linear";static defaults={ticks:{callback:Wg.formatters.numeric}};determineDataLimits(){let{min:n,max:e}=this.getMinMax(!0);this.min=Xi(n)?n:0,this.max=Xi(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){let n=this.isHorizontal(),e=n?this.width:this.height,i=ua(this.options.ticks.minRotation),r=(n?Math.sin(i):Math.cos(i))||.001,o=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,o.lineHeight/r))}getPixelForValue(n){return n===null?NaN:this.getPixelForDecimal((n-this._startValue)/this._valueRange)}getValueForPixel(n){return this._startValue+this.getDecimalForPixel(n)*this._valueRange}},n_=t=>Math.floor(Rl(t)),Iu=(t,n)=>Math.pow(10,n_(t)+n);function Z4(t){return t/Math.pow(10,n_(t))===1}function X4(t,n,e){let i=Math.pow(10,e),r=Math.floor(t/i);return Math.ceil(n/i)-r}function Pse(t,n){let e=n-t,i=n_(e);for(;X4(t,n,i)>10;)i++;for(;X4(t,n,i)<10;)i--;return Math.min(i,n_(t))}function Fse(t,{min:n,max:e}){n=po(t.min,n);let i=[],r=n_(n),o=Pse(n,e),a=o<0?Math.pow(10,Math.abs(o)):1,s=Math.pow(10,o),l=r>o?Math.pow(10,r):0,c=Math.round((n-l)*a)/a,d=Math.floor((n-l)/s/10)*s*10,p=Math.floor((c-d)/Math.pow(10,o)),_=po(t.min,Math.round((l+d+p*Math.pow(10,o))*a)/a);for(;_=10?p=p<15?15:20:p++,p>=20&&(o++,p=2,a=o>=0?1:a),_=Math.round((l+d+p*Math.pow(10,o))*a)/a;let b=po(t.max,_);return i.push({value:b,major:Z4(b),significand:p}),i}var iI=class extends Au{static id="logarithmic";static defaults={ticks:{callback:Wg.formatters.logarithmic,major:{enabled:!0}}};constructor(n){super(n),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(n,e){let i=Qh.prototype.parse.apply(this,[n,e]);if(i===0){this._zero=!0;return}return Xi(i)&&i>0?i:null}determineDataLimits(){let{min:n,max:e}=this.getMinMax(!0);this.min=Xi(n)?Math.max(0,n):null,this.max=Xi(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!Xi(this._userMin)&&(this.min=n===Iu(this.min,0)?Iu(this.min,-1):Iu(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){let{minDefined:n,maxDefined:e}=this.getUserBounds(),i=this.min,r=this.max,o=s=>i=n?i:s,a=s=>r=e?r:s;i===r&&(i<=0?(o(1),a(10)):(o(Iu(i,-1)),a(Iu(r,1)))),i<=0&&o(Iu(r,-1)),r<=0&&a(Iu(i,1)),this.min=i,this.max=r}buildTicks(){let n=this.options,e={min:this._userMin,max:this._userMax},i=Fse(e,this);return n.bounds==="ticks"&&rT(i,this,"value"),n.reverse?(i.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),i}getLabelForValue(n){return n===void 0?"0":$h(n,this.chart.options.locale,this.options.ticks.format)}configure(){let n=this.min;super.configure(),this._startValue=Rl(n),this._valueRange=Rl(this.max)-Rl(n)}getPixelForValue(n){return(n===void 0||n===0)&&(n=this.min),n===null||isNaN(n)?NaN:this.getPixelForDecimal(n===this.min?0:(Rl(n)-this._startValue)/this._valueRange)}getValueForPixel(n){let e=this.getDecimalForPixel(n);return Math.pow(10,this._startValue+e*this._valueRange)}};function nI(t){let n=t.ticks;if(n.display&&t.display){let e=gr(n.backdropPadding);return ut(n.font&&n.font.size,Ti.font.size)+e.height}return 0}function Nse(t,n,e){return e=Ri(e)?e:[e],{w:$j(t,n.string,e),h:e.length*n.lineHeight}}function J4(t,n,e,i,r){return t===i||t===r?{start:n-e/2,end:n+e/2}:tr?{start:n-e,end:n}:{start:n,end:n+e}}function Lse(t){let n={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},e=Object.assign({},n),i=[],r=[],o=t._pointLabels.length,a=t.options.pointLabels,s=a.centerPointLabels?Jt/o:0;for(let l=0;ln.r&&(s=(i.end-n.r)/o,t.r=Math.max(t.r,n.r+s)),r.startn.b&&(l=(r.end-n.b)/a,t.b=Math.max(t.b,n.b+l))}function Bse(t,n,e){let i=t.drawingArea,{extra:r,additionalAngle:o,padding:a,size:s}=e,l=t.getPointPosition(n,i+r+a,o),c=Math.round(Qx(pr(l.angle+rn))),d=$se(l.y,s.h,c),p=zse(c),_=Use(l.x,s.w,p);return{visible:!0,x:l.x,y:d,textAlign:p,left:_,top:d,right:_+s.w,bottom:d+s.h}}function jse(t,n){if(!n)return!0;let{left:e,top:i,right:r,bottom:o}=t;return!(Gs({x:e,y:i},n)||Gs({x:e,y:o},n)||Gs({x:r,y:i},n)||Gs({x:r,y:o},n))}function Hse(t,n,e){let i=[],r=t._pointLabels.length,o=t.options,{centerPointLabels:a,display:s}=o.pointLabels,l={extra:nI(o)/2,additionalAngle:a?Jt/r:0},c;for(let d=0;d270||e<90)&&(t-=n),t}function Wse(t,n,e){let{left:i,top:r,right:o,bottom:a}=e,{backdropColor:s}=n;if(!Nt(s)){let l=qc(n.borderRadius),c=gr(n.backdropPadding);t.fillStyle=s;let d=i-c.left,p=r-c.top,_=o-i+c.width,b=a-r+c.height;Object.values(l).some(y=>y!==0)?(t.beginPath(),Wh(t,{x:d,y:p,w:_,h:b,radius:l}),t.fill()):t.fillRect(d,p,_,b)}}function Gse(t,n){let{ctx:e,options:{pointLabels:i}}=t;for(let r=n-1;r>=0;r--){let o=t._pointLabelItems[r];if(!o.visible)continue;let a=i.setContext(t.getPointLabelContext(r));Wse(e,a,o);let s=vn(a.font),{x:l,y:c,textAlign:d}=o;Gc(e,t._pointLabels[r],l,c+s.lineHeight/2,s,{color:a.color,textAlign:d,textBaseline:"middle"})}}function MH(t,n,e,i){let{ctx:r}=t;if(e)r.arc(t.xCenter,t.yCenter,n,0,Pi);else{let o=t.getPointPosition(0,n);r.moveTo(o.x,o.y);for(let a=1;a{let r=ki(this.options.pointLabels.callback,[e,i],this);return r||r===0?r:""}).filter((e,i)=>this.chart.getDataVisibility(i))}fit(){let n=this.options;n.display&&n.pointLabels.display?Lse(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(n,e,i,r){this.xCenter+=Math.floor((n-e)/2),this.yCenter+=Math.floor((i-r)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(n,e,i,r))}getIndexAngle(n){let e=Pi/(this._pointLabels.length||1),i=this.options.startAngle||0;return pr(n*e+ua(i))}getDistanceFromCenterForValue(n){if(Nt(n))return NaN;let e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-n)*e:(n-this.min)*e}getValueForDistanceFromCenter(n){if(Nt(n))return NaN;let e=n/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(n){let e=this._pointLabels||[];if(n>=0&&n{if(p!==0||p===0&&this.min<0){l=this.getDistanceFromCenterForValue(d.value);let _=this.getContext(p),b=r.setContext(_),y=o.setContext(_);qse(this,b,l,a,y)}}),i.display){for(n.save(),s=a-1;s>=0;s--){let d=i.setContext(this.getPointLabelContext(s)),{color:p,lineWidth:_}=d;!_||!p||(n.lineWidth=_,n.strokeStyle=p,n.setLineDash(d.borderDash),n.lineDashOffset=d.borderDashOffset,l=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),c=this.getPointPosition(s,l),n.beginPath(),n.moveTo(this.xCenter,this.yCenter),n.lineTo(c.x,c.y),n.stroke())}n.restore()}}drawBorder(){}drawLabels(){let n=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;let r=this.getIndexAngle(0),o,a;n.save(),n.translate(this.xCenter,this.yCenter),n.rotate(r),n.textAlign="center",n.textBaseline="middle",this.ticks.forEach((s,l)=>{if(l===0&&this.min>=0&&!e.reverse)return;let c=i.setContext(this.getContext(l)),d=vn(c.font);if(o=this.getDistanceFromCenterForValue(this.ticks[l].value),c.showLabelBackdrop){n.font=d.string,a=n.measureText(s.label).width,n.fillStyle=c.backdropColor;let p=gr(c.backdropPadding);n.fillRect(-a/2-p.left,-o-d.size/2-p.top,a+p.width,d.size+p.height)}Gc(n,s.label,0,-o,d,{color:c.color,strokeColor:c.textStrokeColor,strokeWidth:c.textStrokeWidth})}),n.restore()}drawTitle(){}},CC={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},go=Object.keys(CC);function eH(t,n){return t-n}function tH(t,n){if(Nt(n))return null;let e=t._adapter,{parser:i,round:r,isoWeekday:o}=t._parseOpts,a=n;return typeof i=="function"&&(a=i(a)),Xi(a)||(a=typeof i=="string"?e.parse(a,i):e.parse(a)),a===null?null:(r&&(a=r==="week"&&(Su(o)||o===!0)?e.startOf(a,"isoWeek",o):e.startOf(a,r)),+a)}function iH(t,n,e,i){let r=go.length;for(let o=go.indexOf(t);o=go.indexOf(e);o--){let a=go[o];if(CC[a].common&&t._adapter.diff(r,i,a)>=n-1)return a}return go[e?go.indexOf(e):0]}function Kse(t){for(let n=go.indexOf(t)+1,e=go.length;n=n?e[i]:e[r];t[o]=!0}}function Zse(t,n,e,i){let r=t._adapter,o=+r.startOf(n[0].value,i),a=n[n.length-1].value,s,l;for(s=o;s<=a;s=+r.add(s,1,i))l=e[s],l>=0&&(n[l].major=!0);return n}function rH(t,n,e){let i=[],r={},o=n.length,a,s;for(a=0;a{class t extends Au{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(e){super(e),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(e,i={}){let r=e.time||(e.time={}),o=this._adapter=new loe._date(e.adapters.date);o.init(i),jh(r.displayFormats,o.formats()),this._parseOpts={parser:r.parser,round:r.round,isoWeekday:r.isoWeekday},super.init(e),this._normalized=i.normalized}parse(e,i){return e===void 0?null:tH(this,e)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){let e=this.options,i=this._adapter,r=e.time.unit||"day",{min:o,max:a,minDefined:s,maxDefined:l}=this.getUserBounds();function c(d){!s&&!isNaN(d.min)&&(o=Math.min(o,d.min)),!l&&!isNaN(d.max)&&(a=Math.max(a,d.max))}(!s||!l)&&(c(this._getLabelBounds()),(e.bounds!=="ticks"||e.ticks.source!=="labels")&&c(this.getMinMax(!1))),o=Xi(o)&&!isNaN(o)?o:+i.startOf(Date.now(),r),a=Xi(a)&&!isNaN(a)?a:+i.endOf(Date.now(),r)+1,this.min=Math.min(o,a-1),this.max=Math.max(o+1,a)}_getLabelBounds(){let e=this.getLabelTimestamps(),i=Number.POSITIVE_INFINITY,r=Number.NEGATIVE_INFINITY;return e.length&&(i=e[0],r=e[e.length-1]),{min:i,max:r}}buildTicks(){let e=this.options,i=e.time,r=e.ticks,o=r.source==="labels"?this.getLabelTimestamps():this._generate();e.bounds==="ticks"&&o.length&&(this.min=this._userMin||o[0],this.max=this._userMax||o[o.length-1]);let a=this.min,s=this.max,l=Vj(o,a,s);return this._unit=i.unit||(r.autoSkip?iH(i.minUnit,this.min,this.max,this._getLabelCapacity(a)):Qse(this,l.length,i.minUnit,this.min,this.max)),this._majorUnit=!r.major.enabled||this._unit==="year"?void 0:Kse(this._unit),this.initOffsets(o),e.reverse&&l.reverse(),rH(this,l,this._majorUnit)}afterAutoSkip(){this.options.offsetAfterAutoskip&&this.initOffsets(this.ticks.map(e=>+e.value))}initOffsets(e=[]){let i=0,r=0,o,a;this.options.offset&&e.length&&(o=this.getDecimalForValue(e[0]),e.length===1?i=1-o:i=(this.getDecimalForValue(e[1])-o)/2,a=this.getDecimalForValue(e[e.length-1]),e.length===1?r=a:r=(a-this.getDecimalForValue(e[e.length-2]))/2);let s=e.length<3?.5:.25;i=On(i,0,s),r=On(r,0,s),this._offsets={start:i,end:r,factor:1/(i+1+r)}}_generate(){let e=this._adapter,i=this.min,r=this.max,o=this.options,a=o.time,s=a.unit||iH(a.minUnit,i,r,this._getLabelCapacity(i)),l=ut(o.ticks.stepSize,1),c=s==="week"?a.isoWeekday:!1,d=Su(c)||c===!0,p={},_=i,b,y;if(d&&(_=+e.startOf(_,"isoWeek",c)),_=+e.startOf(_,d?"day":s),e.diff(r,i,s)>1e5*l)throw new Error(i+" and "+r+" are too far apart with stepSize of "+l+" "+s);let w=o.ticks.source==="data"&&this.getDataTimestamps();for(b=_,y=0;b+C)}getLabelForValue(e){let i=this._adapter,r=this.options.time;return r.tooltipFormat?i.format(e,r.tooltipFormat):i.format(e,r.displayFormats.datetime)}format(e,i){let o=this.options.time.displayFormats,a=this._unit,s=i||o[a];return this._adapter.format(e,s)}_tickFormatFunction(e,i,r,o){let a=this.options,s=a.ticks.callback;if(s)return ki(s,[e,i,r],this);let l=a.time.displayFormats,c=this._unit,d=this._majorUnit,p=c&&l[c],_=d&&l[d],b=r[i],y=d&&_&&b&&b.major;return this._adapter.format(e,o||(y?_:p))}generateTickLabels(e){let i,r,o;for(i=0,r=e.length;i0?l:1}getDataTimestamps(){let e=this._cache.data||[],i,r;if(e.length)return e;let o=this.getMatchingVisibleMetas();if(this._normalized&&o.length)return this._cache.data=o[0].controller.getAllParsedValues(this);for(i=0,r=o.length;i=t[i].pos&&n<=t[r].pos&&({lo:i,hi:r}=Ws(t,"pos",n)),{pos:o,time:s}=t[i],{pos:a,time:l}=t[r]):(n>=t[i].time&&n<=t[r].time&&({lo:i,hi:r}=Ws(t,"time",n)),{time:o,pos:s}=t[i],{time:a,pos:l}=t[r]);let c=a-o;return c?s+(l-s)*(n-o)/c:s}var aI=class extends oI{static id="timeseries";static defaults=oI.defaults;constructor(n){super(n),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){let n=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(n);this._minPos=dC(e,this.min),this._tableRange=dC(e,this.max)-this._minPos,super.initOffsets(n)}buildLookupTable(n){let{min:e,max:i}=this,r=[],o=[],a,s,l,c,d;for(a=0,s=n.length;a=e&&c<=i&&r.push(c);if(r.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(a=0,s=r.length;ar-o)}_getTimestampsForTable(){let n=this._cache.all||[];if(n.length)return n;let e=this.getDataTimestamps(),i=this.getLabelTimestamps();return e.length&&i.length?n=this.normalize(e.concat(i)):n=e.length?e:i,n=this._cache.all=n,n}getDecimalForValue(n){return(dC(this._table,n)-this._minPos)/this._tableRange}getValueForPixel(n){let e=this._offsets,i=this.getDecimalForPixel(n)/e.factor-e.end;return dC(this._table,i*this._tableRange+this._minPos,!0)}},Xse=Object.freeze({__proto__:null,CategoryScale:Ose,LinearScale:tI,LogarithmicScale:iI,RadialLinearScale:rI,TimeScale:oI,TimeSeriesScale:aI}),EH=[soe,Rae,kse,Xse];var Jse=typeof global=="object"&&global&&global.Object===Object&&global,wC=Jse;var ele=typeof self=="object"&&self&&self.Object===Object&&self,tle=wC||ele||Function("return this")(),ma=tle;var ile=ma.Symbol,Kh=ile;var SH=Object.prototype,nle=SH.hasOwnProperty,rle=SH.toString,o_=Kh?Kh.toStringTag:void 0;function ole(t){var n=nle.call(t,o_),e=t[o_];try{t[o_]=void 0;var i=!0}catch{}var r=rle.call(t);return i&&(n?t[o_]=e:delete t[o_]),r}var kH=ole;var ale=Object.prototype,sle=ale.toString;function lle(t){return sle.call(t)}var TH=lle;var cle="[object Null]",dle="[object Undefined]",IH=Kh?Kh.toStringTag:void 0;function ule(t){return t==null?t===void 0?dle:cle:IH&&IH in Object(t)?kH(t):TH(t)}var Zc=ule;function mle(t){return t!=null&&typeof t=="object"}var Zs=mle;var hle=Array.isArray,a_=hle;function ple(t){var n=typeof t;return t!=null&&(n=="object"||n=="function")}var _o=ple;function fle(t){return t}var DC=fle;var gle="[object AsyncFunction]",_le="[object Function]",ble="[object GeneratorFunction]",vle="[object Proxy]";function yle(t){if(!_o(t))return!1;var n=Zc(t);return n==_le||n==ble||n==gle||n==vle}var Zh=yle;var xle=ma["__core-js_shared__"],MC=xle;var AH=(function(){var t=/[^.]+$/.exec(MC&&MC.keys&&MC.keys.IE_PROTO||"");return t?"Symbol(src)_1."+t:""})();function Cle(t){return!!AH&&AH in t}var OH=Cle;var wle=Function.prototype,Dle=wle.toString;function Mle(t){if(t!=null){try{return Dle.call(t)}catch{}try{return t+""}catch{}}return""}var RH=Mle;var Ele=/[\\^$.*+?()[\]{}|]/g,Sle=/^\[object .+?Constructor\]$/,kle=Function.prototype,Tle=Object.prototype,Ile=kle.toString,Ale=Tle.hasOwnProperty,Ole=RegExp("^"+Ile.call(Ale).replace(Ele,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function Rle(t){if(!_o(t)||OH(t))return!1;var n=Zh(t)?Ole:Sle;return n.test(RH(t))}var PH=Rle;function Ple(t,n){return t?.[n]}var FH=Ple;function Fle(t,n){var e=FH(t,n);return PH(e)?e:void 0}var Xh=Fle;var NH=Object.create,Nle=(function(){function t(){}return function(n){if(!_o(n))return{};if(NH)return NH(n);t.prototype=n;var e=new t;return t.prototype=void 0,e}})(),LH=Nle;function Lle(t,n,e){switch(e.length){case 0:return t.call(n);case 1:return t.call(n,e[0]);case 2:return t.call(n,e[0],e[1]);case 3:return t.call(n,e[0],e[1],e[2])}return t.apply(n,e)}var VH=Lle;function Vle(t,n){var e=-1,i=t.length;for(n||(n=Array(i));++e0){if(++n>=Ble)return arguments[0]}else n=0;return t.apply(void 0,arguments)}}var jH=zle;function Ule(t){return function(){return t}}var HH=Ule;var $le=(function(){try{var t=Xh(Object,"defineProperty");return t({},"",{}),t}catch{}})(),Jh=$le;var Wle=Jh?function(t,n){return Jh(t,"toString",{configurable:!0,enumerable:!1,value:HH(n),writable:!0})}:DC,zH=Wle;var Gle=jH(zH),UH=Gle;var qle=9007199254740991,Yle=/^(?:0|[1-9]\d*)$/;function Qle(t,n){var e=typeof t;return n=n??qle,!!n&&(e=="number"||e!="symbol"&&Yle.test(t))&&t>-1&&t%1==0&&t-1&&t%1==0&&t<=rce}var SC=oce;function ace(t){return t!=null&&SC(t.length)&&!Zh(t)}var tp=ace;function sce(t,n,e){if(!_o(e))return!1;var i=typeof n;return(i=="number"?tp(e)&&EC(n,e.length):i=="string"&&n in e)?Xc(e[n],t):!1}var QH=sce;function lce(t){return YH(function(n,e){var i=-1,r=e.length,o=r>1?e[r-1]:void 0,a=r>2?e[2]:void 0;for(o=t.length>3&&typeof o=="function"?(r--,o):void 0,a&&QH(e[0],e[1],a)&&(o=r<3?void 0:o,r=1),n=Object(n);++i-1}var v5=wde;function Dde(t,n){var e=this.__data__,i=Jc(e,t);return i<0?(++this.size,e.push([t,n])):e[i][1]=n,this}var y5=Dde;function np(t){var n=-1,e=t==null?0:t.length;for(this.clear();++n{let n=class n{constructor(){this.colorschemesOptions=new rt(void 0)}setColorschemesOptions(i){this.pColorschemesOptions=i,this.colorschemesOptions.next(i)}getColorschemesOptions(){return this.pColorschemesOptions}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})(),ez=(()=>{let n=class n{constructor(i,r,o,a){this.zone=r,this.themeService=o,this.type="bar",this.plugins=[],this.chartClick=new U,this.chartHover=new U,this.subs=[],this.themeOverrides={},a?.registerables&&vC.register(...a.registerables),a?.defaults&&Ti.set(a.defaults),this.ctx=i.nativeElement.getContext("2d"),this.subs.push(this.themeService.colorschemesOptions.pipe(Nn()).subscribe(s=>this.themeChanged(s)))}ngOnChanges(i){let r=["type"],o=Object.getOwnPropertyNames(i);if(o.some(a=>r.includes(a))||o.every(a=>i[a].isFirstChange()))this.render();else{let a=this.getChartConfiguration();this.chart&&(Object.assign(this.chart.config.data,a.data),this.chart.config.plugins&&Object.assign(this.chart.config.plugins,a.plugins),this.chart.config.options&&Object.assign(this.chart.config.options,a.options)),this.update()}}ngOnDestroy(){this.chart&&(this.chart.destroy(),this.chart=void 0),this.subs.forEach(i=>i.unsubscribe())}render(){return this.chart&&this.chart.destroy(),this.zone.runOutsideAngular(()=>this.chart=new vC(this.ctx,this.getChartConfiguration()))}update(i){this.chart&&this.zone.runOutsideAngular(()=>this.chart?.update(i))}hideDataset(i,r){this.chart&&(this.chart.getDatasetMeta(i).hidden=r,this.update())}isDatasetHidden(i){return this.chart?.getDatasetMeta(i)?.hidden}toBase64Image(){return this.chart?.toBase64Image()}themeChanged(i){this.themeOverrides=i,this.chart&&(this.chart.config.options&&Object.assign(this.chart.config.options,this.getChartOptions()),this.update())}getChartOptions(){return PC({onHover:(i,r)=>{!this.chartHover.observed&&!this.chartHover.observers?.length||this.zone.run(()=>this.chartHover.emit({event:i,active:r}))},onClick:(i,r)=>{!this.chartClick.observed&&!this.chartClick.observers?.length||this.zone.run(()=>this.chartClick.emit({event:i,active:r}))}},this.themeOverrides,this.options,{plugins:{legend:{display:this.legend}}})}getChartConfiguration(){return{type:this.type,data:this.getChartData(),options:this.getChartOptions(),plugins:this.plugins}}getChartData(){return this.data?this.data:{labels:this.labels||[],datasets:this.datasets||[]}}};n.\u0275fac=function(r){return new(r||n)(be(Y),be(ae),be(aue),be(Z5,8))},n.\u0275dir=P({type:n,selectors:[["canvas","baseChart",""]],inputs:{type:"type",legend:"legend",data:"data",options:"options",plugins:"plugins",labels:"labels",datasets:"datasets"},outputs:{chartClick:"chartClick",chartHover:"chartHover"},exportAs:["base-chart"],features:[Oe]});let t=n;return t})();var Ji={production:!0,baseUrl:"",useHash:!1,apiUrl:"https://app-talent-api-dev.azurewebsites.net/api/v1",identityServerUrl:"https://app-talent-ids-dev.azurewebsites.net",clientId:"TalentManagement",scope:"openid profile email roles app.api.talentmanagement.read app.api.talentmanagement.write",allowAnonymousAccess:!0,aiEnabled:!0};var hI=document.baseURI.endsWith("/")?document.baseURI.slice(0,-1):document.baseURI,tz={issuer:Ji.identityServerUrl,redirectUri:hI+"/callback",postLogoutRedirectUri:hI,clientId:Ji.clientId,scope:Ji.scope,responseType:"code",showDebugInformation:!Ji.production,useSilentRefresh:!0,silentRefreshRedirectUri:hI+"/silent-refresh.html",silentRefreshTimeout:5e3,timeoutFactor:.75,sessionChecksEnabled:!0,clearHashAfterLogin:!0,strictDiscoveryDocumentValidation:!1,skipIssuerCheck:!Ji.production,requireHttps:Ji.production,requestAccessToken:!0,dummyClientSecret:void 0,customQueryParams:{}};var jt=(()=>{let n=class n{constructor(){this.oauthService=u(Km),this.router=u(Ae),this.isAuthenticatedSubject=new rt(!1),this.isAuthenticated$=this.isAuthenticatedSubject.asObservable(),this.userInfoSubject=new rt(null),this.userInfo$=this.userInfoSubject.asObservable(),this.permissionsChangeSubject=new z,this.permissionsChange$=this.permissionsChangeSubject.asObservable(),this.configureOAuth()}configureOAuth(){this.oauthService.configure(tz),this.oauthService.events.pipe(ce(i=>i.type==="token_received")).subscribe(()=>{this.handleSuccessfulLogin()}),this.oauthService.events.pipe(ce(i=>i.type==="token_error"||i.type==="token_refresh_error")).subscribe(()=>{console.error("Token error occurred")}),this.oauthService.setupAutomaticSilentRefresh()}initAuth(){return yn(this,null,function*(){try{return yield this.oauthService.loadDiscoveryDocument(),yield this.oauthService.tryLogin(),this.oauthService.hasValidAccessToken()?(yield this.handleSuccessfulLogin(),!0):!1}catch(i){return console.error("Error during authentication initialization:",i),!1}})}login(i){i?this.oauthService.initCodeFlow(i):this.oauthService.initCodeFlow()}logout(){this.oauthService.logOut(),this.isAuthenticatedSubject.next(!1),this.userInfoSubject.next(null),this.permissionsChangeSubject.next()}handleSuccessfulLogin(){return yn(this,null,function*(){try{let i=this.oauthService.getIdentityClaims();this.userInfoSubject.next(i),this.isAuthenticatedSubject.next(!0),this.permissionsChangeSubject.next()}catch(i){console.error("Error loading user info:",i)}})}isAuthenticated(){return this.oauthService.hasValidAccessToken()}getAccessToken(){return this.oauthService.getAccessToken()}getUserInfo(){return this.userInfoSubject.value}getUserRoles(){let i=this.oauthService.getIdentityClaims();if(!i)return[];let r=i.role;return Array.isArray(r)?r:typeof r=="string"?[r]:[]}hasRole(i){return this.getUserRoles().includes(i)}isEmployee(){return this.hasRole("Employee")}isManager(){return this.hasRole("Manager")}isHRAdmin(){return this.hasRole("HRAdmin")}hasAnyRole(i){let r=this.getUserRoles();return i.some(o=>r.includes(o))}getUserDisplayName(){let i=this.getUserInfo();return i?.name||i?.email||"User"}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var pI=(t,n)=>{let e=u(jt);return Ji.allowAnonymousAccess||e.isAuthenticated()?!0:(e.login(n?.url),!1)};var u_=t=>{let n=u(jt),e=u(Ae);return n.isAuthenticated()?n.isManager()||n.isHRAdmin()?!0:(e.navigate(["/403"]),!1):(n.login(),!1)},m_=t=>{let n=u(jt),e=u(Ae);return n.isAuthenticated()?n.isHRAdmin()?!0:(e.navigate(["/403"]),!1):(n.login(),!1)};var bo=(()=>{let n=class n{constructor(){this.menu$=new rt([])}getAll(){return this.menu$.asObservable()}change(){return this.menu$.pipe(ec())}set(i){return this.menu$.next(i),this.menu$.asObservable()}add(i){let r=this.menu$.value;r.push(i),this.menu$.next(r)}reset(){this.menu$.next([])}buildRoute(i){let r="";return i.forEach(o=>{o&&o.trim()&&(r+="/"+o.replace(/^\/+|\/+$/g,""))}),r}getItemName(i){return this.getLevel(i)[i.length-1]}isLeafItem(i){let r=i.route===void 0,o=i.children===void 0,a=!o&&i.children?.length===0;return r||o||a}deepClone(i){return JSON.parse(JSON.stringify(i))}isJsonObjEqual(i,r){return JSON.stringify(i)===JSON.stringify(r)}isRouteEqual(i,r){return r=this.deepClone(r),r=r.filter(o=>o!==""),this.isJsonObjEqual(i,r)}getLevel(i){let r=[];return this.menu$.value.forEach(o=>{let a=[{item:o,parentNamePathList:[],realRouteArr:[]}];for(;a.length>0;){let s=[];for(let l of a){let c=l.item,d=this.deepClone(l.parentNamePathList).concat(c.name),p=this.deepClone(l.realRouteArr).concat(c.route);if(this.isRouteEqual(i,p)){r=d;break}if(!this.isLeafItem(c)){let _=c.children?.map(b=>({item:b,parentNamePathList:d,realRouteArr:p}));s=s.concat(_)}}a=s}}),r}addNamespace(i,r){i.forEach(o=>{o.name=`${r}.${o.name}`,o.children&&o.children.length>0&&this.addNamespace(o.children,o.name)})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var FC;function sue(){if(FC===void 0&&(FC=null,typeof window<"u")){let t=window;t.trustedTypes!==void 0&&(FC=t.trustedTypes.createPolicy("angular#components",{createHTML:n=>n}))}return FC}function h_(t){return sue()?.createHTML(t)||t}function iz(t){return Error(`Unable to find icon with the name "${t}"`)}function lue(){return Error("Could not find HttpClient for use with Angular Material icons. Please add provideHttpClient() to your providers.")}function nz(t){return Error(`The URL provided to MatIconRegistry was not trusted as a resource URL via Angular's DomSanitizer. Attempted URL was "${t}".`)}function rz(t){return Error(`The literal provided to MatIconRegistry was not trusted as safe HTML by Angular's DomSanitizer. Attempted literal was "${t}".`)}var Bl=class{url;svgText;options;svgElement;constructor(n,e,i){this.url=n,this.svgText=e,this.options=i}},az=(()=>{class t{_httpClient;_sanitizer;_errorHandler;_document;_svgIconConfigs=new Map;_iconSetConfigs=new Map;_cachedIconsByUrl=new Map;_inProgressUrlFetches=new Map;_fontCssClassesByAlias=new Map;_resolvers=[];_defaultFontSetClass=["material-icons","mat-ligature-font"];constructor(e,i,r,o){this._httpClient=e,this._sanitizer=i,this._errorHandler=o,this._document=r}addSvgIcon(e,i,r){return this.addSvgIconInNamespace("",e,i,r)}addSvgIconLiteral(e,i,r){return this.addSvgIconLiteralInNamespace("",e,i,r)}addSvgIconInNamespace(e,i,r,o){return this._addSvgIconConfig(e,i,new Bl(r,null,o))}addSvgIconResolver(e){return this._resolvers.push(e),this}addSvgIconLiteralInNamespace(e,i,r,o){let a=this._sanitizer.sanitize(Bn.HTML,r);if(!a)throw rz(r);let s=h_(a);return this._addSvgIconConfig(e,i,new Bl("",s,o))}addSvgIconSet(e,i){return this.addSvgIconSetInNamespace("",e,i)}addSvgIconSetLiteral(e,i){return this.addSvgIconSetLiteralInNamespace("",e,i)}addSvgIconSetInNamespace(e,i,r){return this._addSvgIconSetConfig(e,new Bl(i,null,r))}addSvgIconSetLiteralInNamespace(e,i,r){let o=this._sanitizer.sanitize(Bn.HTML,i);if(!o)throw rz(i);let a=h_(o);return this._addSvgIconSetConfig(e,new Bl("",a,r))}registerFontClassAlias(e,i=e){return this._fontCssClassesByAlias.set(e,i),this}classNameForFontAlias(e){return this._fontCssClassesByAlias.get(e)||e}setDefaultFontSetClass(...e){return this._defaultFontSetClass=e,this}getDefaultFontSetClass(){return this._defaultFontSetClass}getSvgIconFromUrl(e){let i=this._sanitizer.sanitize(Bn.RESOURCE_URL,e);if(!i)throw nz(e);let r=this._cachedIconsByUrl.get(i);return r?Q(NC(r)):this._loadSvgIconFromConfig(new Bl(e,null)).pipe(He(o=>this._cachedIconsByUrl.set(i,o)),se(o=>NC(o)))}getNamedSvgIcon(e,i=""){let r=oz(i,e),o=this._svgIconConfigs.get(r);if(o)return this._getSvgFromConfig(o);if(o=this._getIconConfigFromResolvers(i,e),o)return this._svgIconConfigs.set(r,o),this._getSvgFromConfig(o);let a=this._iconSetConfigs.get(i);return a?this._getSvgFromIconSetConfigs(e,a):er(iz(r))}ngOnDestroy(){this._resolvers=[],this._svgIconConfigs.clear(),this._iconSetConfigs.clear(),this._cachedIconsByUrl.clear()}_getSvgFromConfig(e){return e.svgText?Q(NC(this._svgElementFromConfig(e))):this._loadSvgIconFromConfig(e).pipe(se(i=>NC(i)))}_getSvgFromIconSetConfigs(e,i){let r=this._extractIconWithNameFromAnySet(e,i);if(r)return Q(r);let o=i.filter(a=>!a.svgText).map(a=>this._loadSvgIconSetFromConfig(a).pipe(ei(s=>{let c=`Loading icon set URL: ${this._sanitizer.sanitize(Bn.RESOURCE_URL,a.url)} failed: ${s.message}`;return this._errorHandler.handleError(new Error(c)),Q(null)})));return cs(o).pipe(se(()=>{let a=this._extractIconWithNameFromAnySet(e,i);if(!a)throw iz(e);return a}))}_extractIconWithNameFromAnySet(e,i){for(let r=i.length-1;r>=0;r--){let o=i[r];if(o.svgText&&o.svgText.toString().indexOf(e)>-1){let a=this._svgElementFromConfig(o),s=this._extractSvgIconFromSet(a,e,o.options);if(s)return s}}return null}_loadSvgIconFromConfig(e){return this._fetchIcon(e).pipe(He(i=>e.svgText=i),se(()=>this._svgElementFromConfig(e)))}_loadSvgIconSetFromConfig(e){return e.svgText?Q(null):this._fetchIcon(e).pipe(He(i=>e.svgText=i))}_extractSvgIconFromSet(e,i,r){let o=e.querySelector(`[id="${i}"]`);if(!o)return null;let a=o.cloneNode(!0);if(a.removeAttribute("id"),a.nodeName.toLowerCase()==="svg")return this._setSvgAttributes(a,r);if(a.nodeName.toLowerCase()==="symbol")return this._setSvgAttributes(this._toSvgElement(a),r);let s=this._svgElementFromString(h_(""));return s.appendChild(a),this._setSvgAttributes(s,r)}_svgElementFromString(e){let i=this._document.createElement("DIV");i.innerHTML=e;let r=i.querySelector("svg");if(!r)throw Error(" tag not found");return r}_toSvgElement(e){let i=this._svgElementFromString(h_("")),r=e.attributes;for(let o=0;oh_(c)),Xr(()=>this._inProgressUrlFetches.delete(a)),ec());return this._inProgressUrlFetches.set(a,l),l}_addSvgIconConfig(e,i,r){return this._svgIconConfigs.set(oz(e,i),r),this}_addSvgIconSetConfig(e,i){let r=this._iconSetConfigs.get(e);return r?r.push(i):this._iconSetConfigs.set(e,[i]),this}_svgElementFromConfig(e){if(!e.svgElement){let i=this._svgElementFromString(e.svgText);this._setSvgAttributes(i,e.options),e.svgElement=i}return e.svgElement}_getIconConfigFromResolvers(e,i){for(let r=0;rn?n.pathname+n.search:""}}var sz=["clip-path","color-profile","src","cursor","fill","filter","marker","marker-start","marker-mid","marker-end","mask","stroke"],pue=sz.map(t=>`[${t}]`).join(", "),fue=/^url\(['"]?#(.*?)['"]?\)$/,Ze=(()=>{class t{_elementRef=u(Y);_iconRegistry=u(az);_location=u(mue);_errorHandler=u(Ln);_defaultColor;get color(){return this._color||this._defaultColor}set color(e){this._color=e}_color;inline=!1;get svgIcon(){return this._svgIcon}set svgIcon(e){e!==this._svgIcon&&(e?this._updateSvgIcon(e):this._svgIcon&&this._clearSvgElement(),this._svgIcon=e)}_svgIcon;get fontSet(){return this._fontSet}set fontSet(e){let i=this._cleanupFontValue(e);i!==this._fontSet&&(this._fontSet=i,this._updateFontIconClasses())}_fontSet;get fontIcon(){return this._fontIcon}set fontIcon(e){let i=this._cleanupFontValue(e);i!==this._fontIcon&&(this._fontIcon=i,this._updateFontIconClasses())}_fontIcon;_previousFontSetClass=[];_previousFontIconClass;_svgName;_svgNamespace;_previousPath;_elementsWithExternalReferences;_currentIconFetch=ke.EMPTY;constructor(){let e=u(new Li("aria-hidden"),{optional:!0}),i=u(uue,{optional:!0});i&&(i.color&&(this.color=this._defaultColor=i.color),i.fontSet&&(this.fontSet=i.fontSet)),e||this._elementRef.nativeElement.setAttribute("aria-hidden","true")}_splitIconName(e){if(!e)return["",""];let i=e.split(":");switch(i.length){case 1:return["",i[0]];case 2:return i;default:throw Error(`Invalid icon name: "${e}"`)}}ngOnInit(){this._updateFontIconClasses()}ngAfterViewChecked(){let e=this._elementsWithExternalReferences;if(e&&e.size){let i=this._location.getPathname();i!==this._previousPath&&(this._previousPath=i,this._prependPathToReferences(i))}}ngOnDestroy(){this._currentIconFetch.unsubscribe(),this._elementsWithExternalReferences&&this._elementsWithExternalReferences.clear()}_usingFontIcon(){return!this.svgIcon}_setSvgElement(e){this._clearSvgElement();let i=this._location.getPathname();this._previousPath=i,this._cacheChildrenWithExternalReferences(e),this._prependPathToReferences(i),this._elementRef.nativeElement.appendChild(e)}_clearSvgElement(){let e=this._elementRef.nativeElement,i=e.childNodes.length;for(this._elementsWithExternalReferences&&this._elementsWithExternalReferences.clear();i--;){let r=e.childNodes[i];(r.nodeType!==1||r.nodeName.toLowerCase()==="svg")&&r.remove()}}_updateFontIconClasses(){if(!this._usingFontIcon())return;let e=this._elementRef.nativeElement,i=(this.fontSet?this._iconRegistry.classNameForFontAlias(this.fontSet).split(/ +/):this._iconRegistry.getDefaultFontSetClass()).filter(r=>r.length>0);this._previousFontSetClass.forEach(r=>e.classList.remove(r)),i.forEach(r=>e.classList.add(r)),this._previousFontSetClass=i,this.fontIcon!==this._previousFontIconClass&&!i.includes("mat-ligature-font")&&(this._previousFontIconClass&&e.classList.remove(this._previousFontIconClass),this.fontIcon&&e.classList.add(this.fontIcon),this._previousFontIconClass=this.fontIcon)}_cleanupFontValue(e){return typeof e=="string"?e.trim().split(" ")[0]:e}_prependPathToReferences(e){let i=this._elementsWithExternalReferences;i&&i.forEach((r,o)=>{r.forEach(a=>{o.setAttribute(a.name,`url('${e}#${a.value}')`)})})}_cacheChildrenWithExternalReferences(e){let i=e.querySelectorAll(pue),r=this._elementsWithExternalReferences=this._elementsWithExternalReferences||new Map;for(let o=0;o{let s=i[o],l=s.getAttribute(a),c=l?l.match(fue):null;if(c){let d=r.get(s);d||(d=[],r.set(s,d)),d.push({name:a,value:c[1]})}})}_updateSvgIcon(e){if(this._svgNamespace=null,this._svgName=null,this._currentIconFetch.unsubscribe(),e){let[i,r]=this._splitIconName(e);i&&(this._svgNamespace=i),r&&(this._svgName=r),this._currentIconFetch=this._iconRegistry.getNamedSvgIcon(r,i).pipe(mt(1)).subscribe(o=>this._setSvgElement(o),o=>{let a=`Error retrieving icon ${i}:${r}! ${o.message}`;this._errorHandler.handleError(new Error(a))})}}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-icon"]],hostAttrs:["role","img",1,"mat-icon","notranslate"],hostVars:10,hostBindings:function(i,r){i&2&&(X("data-mat-icon-type",r._usingFontIcon()?"font":"svg")("data-mat-icon-name",r._svgName||r.fontIcon)("data-mat-icon-namespace",r._svgNamespace||r.fontSet)("fontIcon",r._usingFontIcon()?r.fontIcon:null),at(r.color?"mat-"+r.color:""),G("mat-icon-inline",r.inline)("mat-icon-no-color",r.color!=="primary"&&r.color!=="accent"&&r.color!=="warn"))},inputs:{color:"color",inline:[2,"inline","inline",B],svgIcon:"svgIcon",fontSet:"fontSet",fontIcon:"fontIcon"},exportAs:["matIcon"],ngContentSelectors:due,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),ne(0))},styles:[`mat-icon,mat-icon.mat-primary,mat-icon.mat-accent,mat-icon.mat-warn{color:var(--mat-icon-color, inherit)}.mat-icon{-webkit-user-select:none;user-select:none;background-repeat:no-repeat;display:inline-block;fill:currentColor;height:24px;width:24px;overflow:hidden}.mat-icon.mat-icon-inline{font-size:inherit;height:inherit;line-height:inherit;width:inherit}.mat-icon.mat-ligature-font[fontIcon]::before{content:attr(fontIcon)}[dir=rtl] .mat-icon-rtl-mirror{transform:scale(-1, 1)}.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-icon,.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-icon{display:block}.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-icon-button .mat-icon,.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-icon-button .mat-icon{margin:auto} +`],encapsulation:2,changeDetection:0})}return t})(),Ge=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,De]})}return t})();function gue(t,n){if(t&1&&(m(0,"a",3),f(1),h()),t&2){let e=x().$implicit;g(),N(e)}}function _ue(t,n){if(t&1&&(m(0,"mat-icon",4),f(1,"chevron_right"),h(),m(2,"span"),f(3),me(4,"translate"),h()),t&2){let e=x().$implicit;g(3),N(Re(4,1,e))}}function bue(t,n){if(t&1&&(m(0,"li",2),L(1,gue,2,1,"a",3),L(2,_ue,5,3),h()),t&2){let e=n.$index;g(),V(e===0?1:-1),g(),V(e!==0?2:-1)}}var lz=(()=>{let n=class n{constructor(){this.router=u(Ae),this.menu=u(bo),this.nav=re([]),this.navItems=[]}ngOnInit(){this.router.events.pipe(ce(i=>i instanceof Si),Ue(this.router)).subscribe(()=>{this.genBreadcrumb()})}genBreadcrumb(){let i=this.router.url.slice(1).split("/");this.nav().length>0?this.navItems=[...this.nav()]:(this.navItems=this.menu.getLevel(i),this.navItems.unshift("home"))}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["breadcrumb"]],inputs:{nav:[1,"nav"]},decls:4,vars:0,consts:[["aria-label","breadcrumb"],[1,"matero-breadcrumb"],[1,"matero-breadcrumb-item"],["href","#",1,"link"],[1,"chevron"]],template:function(r,o){r&1&&(m(0,"nav",0)(1,"ol",1),Mt(2,bue,3,2,"li",2,qi),h()()),r&2&&(g(2),Et(o.navItems))},dependencies:[Ge,Ze,Rr,Or],styles:[`.matero-breadcrumb{display:flex;flex-wrap:wrap;padding:0;margin-bottom:1rem;font-size:.875rem;list-style:none}.matero-breadcrumb-item{line-height:1.125rem;text-transform:capitalize}.matero-breadcrumb-item>*{vertical-align:middle}.matero-breadcrumb-item>a.link{color:currentColor;text-decoration:none}.matero-breadcrumb-item>a.link:hover{color:currentColor;text-decoration:underline}.matero-breadcrumb-item>.chevron{width:1.125rem;height:1.125rem;font-size:1.125rem;-webkit-user-select:none;user-select:none} +`],encapsulation:2});let t=n;return t})();function vue(t,n){}var id=class{viewContainerRef;injector;id;role="dialog";panelClass="";hasBackdrop=!0;backdropClass="";disableClose=!1;closePredicate;width="";height="";minWidth;minHeight;maxWidth;maxHeight;positionStrategy;data=null;direction;ariaDescribedBy=null;ariaLabelledBy=null;ariaLabel=null;ariaModal=!1;autoFocus="first-tabbable";restoreFocus=!0;scrollStrategy;closeOnNavigation=!0;closeOnDestroy=!0;closeOnOverlayDetachments=!0;disableAnimations=!1;providers;container;templateContext};var gI=(()=>{class t extends kc{_elementRef=u(Y);_focusTrapFactory=u(eh);_config;_interactivityChecker=u(Dc);_ngZone=u(ae);_focusMonitor=u(oi);_renderer=u(ze);_changeDetectorRef=u(ye);_injector=u(de);_platform=u(Ye);_document=u(_e);_portalOutlet;_focusTrapped=new z;_focusTrap=null;_elementFocusedBeforeDialogWasOpened=null;_closeInteractionType=null;_ariaLabelledByQueue=[];_isDestroyed=!1;constructor(){super(),this._config=u(id,{optional:!0})||new id,this._config.ariaLabelledBy&&this._ariaLabelledByQueue.push(this._config.ariaLabelledBy)}_addAriaLabelledBy(e){this._ariaLabelledByQueue.push(e),this._changeDetectorRef.markForCheck()}_removeAriaLabelledBy(e){let i=this._ariaLabelledByQueue.indexOf(e);i>-1&&(this._ariaLabelledByQueue.splice(i,1),this._changeDetectorRef.markForCheck())}_contentAttached(){this._initializeFocusTrap(),this._captureInitialFocus()}_captureInitialFocus(){this._trapFocus()}ngOnDestroy(){this._focusTrapped.complete(),this._isDestroyed=!0,this._restoreFocus()}attachComponentPortal(e){this._portalOutlet.hasAttached();let i=this._portalOutlet.attachComponentPortal(e);return this._contentAttached(),i}attachTemplatePortal(e){this._portalOutlet.hasAttached();let i=this._portalOutlet.attachTemplatePortal(e);return this._contentAttached(),i}attachDomPortal=e=>{this._portalOutlet.hasAttached();let i=this._portalOutlet.attachDomPortal(e);return this._contentAttached(),i};_recaptureFocus(){this._containsFocus()||this._trapFocus()}_forceFocus(e,i){this._interactivityChecker.isFocusable(e)||(e.tabIndex=-1,this._ngZone.runOutsideAngular(()=>{let r=()=>{o(),a(),e.removeAttribute("tabindex")},o=this._renderer.listen(e,"blur",r),a=this._renderer.listen(e,"mousedown",r)})),e.focus(i)}_focusByCssSelector(e,i){let r=this._elementRef.nativeElement.querySelector(e);r&&this._forceFocus(r,i)}_trapFocus(e){this._isDestroyed||vt(()=>{let i=this._elementRef.nativeElement;switch(this._config.autoFocus){case!1:case"dialog":this._containsFocus()||i.focus(e);break;case!0:case"first-tabbable":this._focusTrap?.focusInitialElement(e)||this._focusDialogContainer(e);break;case"first-heading":this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]',e);break;default:this._focusByCssSelector(this._config.autoFocus,e);break}this._focusTrapped.next()},{injector:this._injector})}_restoreFocus(){let e=this._config.restoreFocus,i=null;if(typeof e=="string"?i=this._document.querySelector(e):typeof e=="boolean"?i=e?this._elementFocusedBeforeDialogWasOpened:null:e&&(i=e),this._config.restoreFocus&&i&&typeof i.focus=="function"){let r=So(),o=this._elementRef.nativeElement;(!r||r===this._document.body||r===o||o.contains(r))&&(this._focusMonitor?(this._focusMonitor.focusVia(i,this._closeInteractionType),this._closeInteractionType=null):i.focus())}this._focusTrap&&this._focusTrap.destroy()}_focusDialogContainer(e){this._elementRef.nativeElement.focus?.(e)}_containsFocus(){let e=this._elementRef.nativeElement,i=So();return e===i||e.contains(i)}_initializeFocusTrap(){this._platform.isBrowser&&(this._focusTrap=this._focusTrapFactory.create(this._elementRef.nativeElement),this._document&&(this._elementFocusedBeforeDialogWasOpened=So()))}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["cdk-dialog-container"]],viewQuery:function(i,r){if(i&1&&ie(Ir,7),i&2){let o;j(o=H())&&(r._portalOutlet=o.first)}},hostAttrs:["tabindex","-1",1,"cdk-dialog-container"],hostVars:6,hostBindings:function(i,r){i&2&&X("id",r._config.id||null)("role",r._config.role)("aria-modal",r._config.ariaModal)("aria-labelledby",r._config.ariaLabel?null:r._ariaLabelledByQueue[0])("aria-label",r._config.ariaLabel)("aria-describedby",r._config.ariaDescribedBy||null)},features:[le],decls:1,vars:0,consts:[["cdkPortalOutlet",""]],template:function(i,r){i&1&&A(0,vue,0,0,"ng-template",0)},dependencies:[Ir],styles:[`.cdk-dialog-container{display:block;width:100%;height:100%;min-height:inherit;max-height:inherit} +`],encapsulation:2})}return t})(),p_=class{overlayRef;config;componentInstance;componentRef;containerInstance;disableClose;closed=new z;backdropClick;keydownEvents;outsidePointerEvents;id;_detachSubscription;constructor(n,e){this.overlayRef=n,this.config=e,this.disableClose=e.disableClose,this.backdropClick=n.backdropClick(),this.keydownEvents=n.keydownEvents(),this.outsidePointerEvents=n.outsidePointerEvents(),this.id=e.id,this.keydownEvents.subscribe(i=>{i.keyCode===27&&!this.disableClose&&!Gt(i)&&(i.preventDefault(),this.close(void 0,{focusOrigin:"keyboard"}))}),this.backdropClick.subscribe(()=>{!this.disableClose&&this._canClose()?this.close(void 0,{focusOrigin:"mouse"}):this.containerInstance._recaptureFocus?.()}),this._detachSubscription=n.detachments().subscribe(()=>{e.closeOnOverlayDetachments!==!1&&this.close()})}close(n,e){if(this._canClose(n)){let i=this.closed;this.containerInstance._closeInteractionType=e?.focusOrigin||"program",this._detachSubscription.unsubscribe(),this.overlayRef.dispose(),i.next(n),i.complete(),this.componentInstance=this.containerInstance=null}}updatePosition(){return this.overlayRef.updatePosition(),this}updateSize(n="",e=""){return this.overlayRef.updateSize({width:n,height:e}),this}addPanelClass(n){return this.overlayRef.addPanelClass(n),this}removePanelClass(n){return this.overlayRef.removePanelClass(n),this}_canClose(n){let e=this.config;return!!this.containerInstance&&(!e.closePredicate||e.closePredicate(n,e,this.componentInstance))}},yue=new O("DialogScrollStrategy",{providedIn:"root",factory:()=>{let t=u(de);return()=>Tc(t)}}),xue=new O("DialogData"),Cue=new O("DefaultDialogConfig");function wue(t){let n=he(t),e=new U;return{valueSignal:n,get value(){return n()},change:e,ngOnDestroy(){e.complete()}}}var _I=(()=>{class t{_injector=u(de);_defaultOptions=u(Cue,{optional:!0});_parentDialog=u(t,{optional:!0,skipSelf:!0});_overlayContainer=u(ax);_idGenerator=u(et);_openDialogsAtThisLevel=[];_afterAllClosedAtThisLevel=new z;_afterOpenedAtThisLevel=new z;_ariaHiddenElements=new Map;_scrollStrategy=u(yue);get openDialogs(){return this._parentDialog?this._parentDialog.openDialogs:this._openDialogsAtThisLevel}get afterOpened(){return this._parentDialog?this._parentDialog.afterOpened:this._afterOpenedAtThisLevel}afterAllClosed=Fn(()=>this.openDialogs.length?this._getAfterAllClosed():this._getAfterAllClosed().pipe(Ue(void 0)));constructor(){}open(e,i){let r=this._defaultOptions||new id;i=I(I({},r),i),i.id=i.id||this._idGenerator.getId("cdk-dialog-"),i.id&&this.getDialogById(i.id);let o=this._getOverlayConfig(i),a=qr(this._injector,o),s=new p_(a,i),l=this._attachContainer(a,s,i);if(s.containerInstance=l,!this.openDialogs.length){let c=this._overlayContainer.getContainerElement();l._focusTrapped?l._focusTrapped.pipe(mt(1)).subscribe(()=>{this._hideNonDialogContentFromAssistiveTechnology(c)}):this._hideNonDialogContentFromAssistiveTechnology(c)}return this._attachDialogContent(e,s,l,i),this.openDialogs.push(s),s.closed.subscribe(()=>this._removeOpenDialog(s,!0)),this.afterOpened.next(s),s}closeAll(){fI(this.openDialogs,e=>e.close())}getDialogById(e){return this.openDialogs.find(i=>i.id===e)}ngOnDestroy(){fI(this._openDialogsAtThisLevel,e=>{e.config.closeOnDestroy===!1&&this._removeOpenDialog(e,!1)}),fI(this._openDialogsAtThisLevel,e=>e.close()),this._afterAllClosedAtThisLevel.complete(),this._afterOpenedAtThisLevel.complete(),this._openDialogsAtThisLevel=[]}_getOverlayConfig(e){let i=new Gr({positionStrategy:e.positionStrategy||Us().centerHorizontally().centerVertically(),scrollStrategy:e.scrollStrategy||this._scrollStrategy(),panelClass:e.panelClass,hasBackdrop:e.hasBackdrop,direction:e.direction,minWidth:e.minWidth,minHeight:e.minHeight,maxWidth:e.maxWidth,maxHeight:e.maxHeight,width:e.width,height:e.height,disposeOnNavigation:e.closeOnNavigation,disableAnimations:e.disableAnimations});return e.backdropClass&&(i.backdropClass=e.backdropClass),i}_attachContainer(e,i,r){let o=r.injector||r.viewContainerRef?.injector,a=[{provide:id,useValue:r},{provide:p_,useValue:i},{provide:xh,useValue:e}],s;r.container?typeof r.container=="function"?s=r.container:(s=r.container.type,a.push(...r.container.providers(r))):s=gI;let l=new ao(s,r.viewContainerRef,de.create({parent:o||this._injector,providers:a}));return e.attach(l).instance}_attachDialogContent(e,i,r,o){if(e instanceof te){let a=this._createInjector(o,i,r,void 0),s={$implicit:o.data,dialogRef:i};o.templateContext&&(s=I(I({},s),typeof o.templateContext=="function"?o.templateContext():o.templateContext)),r.attachTemplatePortal(new kn(e,null,s,a))}else{let a=this._createInjector(o,i,r,this._injector),s=r.attachComponentPortal(new ao(e,o.viewContainerRef,a));i.componentRef=s,i.componentInstance=s.instance}}_createInjector(e,i,r,o){let a=e.injector||e.viewContainerRef?.injector,s=[{provide:xue,useValue:e.data},{provide:p_,useValue:i}];return e.providers&&(typeof e.providers=="function"?s.push(...e.providers(i,e,r)):s.push(...e.providers)),e.direction&&(!a||!a.get(Yt,null,{optional:!0}))&&s.push({provide:Yt,useValue:wue(e.direction)}),de.create({parent:a||o,providers:s})}_removeOpenDialog(e,i){let r=this.openDialogs.indexOf(e);r>-1&&(this.openDialogs.splice(r,1),this.openDialogs.length||(this._ariaHiddenElements.forEach((o,a)=>{o?a.setAttribute("aria-hidden",o):a.removeAttribute("aria-hidden")}),this._ariaHiddenElements.clear(),i&&this._getAfterAllClosed().next()))}_hideNonDialogContentFromAssistiveTechnology(e){if(e.parentElement){let i=e.parentElement.children;for(let r=i.length-1;r>-1;r--){let o=i[r];o!==e&&o.nodeName!=="SCRIPT"&&o.nodeName!=="STYLE"&&!o.hasAttribute("aria-live")&&(this._ariaHiddenElements.set(o,o.getAttribute("aria-hidden")),o.setAttribute("aria-hidden","true"))}}}_getAfterAllClosed(){let e=this._parentDialog;return e?e._getAfterAllClosed():this._afterAllClosedAtThisLevel}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function fI(t,n){let e=t.length;for(;e--;)n(t[e])}var cz=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[_I],imports:[cr,Oo,El,Oo]})}return t})();function Due(t,n){}var g_=class{viewContainerRef;injector;id;role="dialog";panelClass="";hasBackdrop=!0;backdropClass="";disableClose=!1;closePredicate;width="";height="";minWidth;minHeight;maxWidth;maxHeight;position;data=null;direction;ariaDescribedBy=null;ariaLabelledBy=null;ariaLabel=null;ariaModal=!1;autoFocus="first-tabbable";restoreFocus=!0;delayFocusTrap=!0;scrollStrategy;closeOnNavigation=!0;enterAnimationDuration;exitAnimationDuration},bI="mdc-dialog--open",dz="mdc-dialog--opening",uz="mdc-dialog--closing",Mue=150,Eue=75,pz=(()=>{class t extends gI{_animationStateChanged=new U;_animationsEnabled=!Qe();_actionSectionCount=0;_hostElement=this._elementRef.nativeElement;_enterAnimationDuration=this._animationsEnabled?hz(this._config.enterAnimationDuration)??Mue:0;_exitAnimationDuration=this._animationsEnabled?hz(this._config.exitAnimationDuration)??Eue:0;_animationTimer=null;_contentAttached(){super._contentAttached(),this._startOpenAnimation()}_startOpenAnimation(){this._animationStateChanged.emit({state:"opening",totalTime:this._enterAnimationDuration}),this._animationsEnabled?(this._hostElement.style.setProperty(mz,`${this._enterAnimationDuration}ms`),this._requestAnimationFrame(()=>this._hostElement.classList.add(dz,bI)),this._waitForAnimationToComplete(this._enterAnimationDuration,this._finishDialogOpen)):(this._hostElement.classList.add(bI),Promise.resolve().then(()=>this._finishDialogOpen()))}_startExitAnimation(){this._animationStateChanged.emit({state:"closing",totalTime:this._exitAnimationDuration}),this._hostElement.classList.remove(bI),this._animationsEnabled?(this._hostElement.style.setProperty(mz,`${this._exitAnimationDuration}ms`),this._requestAnimationFrame(()=>this._hostElement.classList.add(uz)),this._waitForAnimationToComplete(this._exitAnimationDuration,this._finishDialogClose)):Promise.resolve().then(()=>this._finishDialogClose())}_updateActionSectionCount(e){this._actionSectionCount+=e,this._changeDetectorRef.markForCheck()}_finishDialogOpen=()=>{this._clearAnimationClasses(),this._openAnimationDone(this._enterAnimationDuration)};_finishDialogClose=()=>{this._clearAnimationClasses(),this._animationStateChanged.emit({state:"closed",totalTime:this._exitAnimationDuration})};_clearAnimationClasses(){this._hostElement.classList.remove(dz,uz)}_waitForAnimationToComplete(e,i){this._animationTimer!==null&&clearTimeout(this._animationTimer),this._animationTimer=setTimeout(i,e)}_requestAnimationFrame(e){this._ngZone.runOutsideAngular(()=>{typeof requestAnimationFrame=="function"?requestAnimationFrame(e):e()})}_captureInitialFocus(){this._config.delayFocusTrap||this._trapFocus()}_openAnimationDone(e){this._config.delayFocusTrap&&this._trapFocus(),this._animationStateChanged.next({state:"opened",totalTime:e})}ngOnDestroy(){super.ngOnDestroy(),this._animationTimer!==null&&clearTimeout(this._animationTimer)}attachComponentPortal(e){let i=super.attachComponentPortal(e);return i.location.nativeElement.classList.add("mat-mdc-dialog-component-host"),i}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-dialog-container"]],hostAttrs:["tabindex","-1",1,"mat-mdc-dialog-container","mdc-dialog"],hostVars:10,hostBindings:function(i,r){i&2&&(pi("id",r._config.id),X("aria-modal",r._config.ariaModal)("role",r._config.role)("aria-labelledby",r._config.ariaLabel?null:r._ariaLabelledByQueue[0])("aria-label",r._config.ariaLabel)("aria-describedby",r._config.ariaDescribedBy||null),G("_mat-animation-noopable",!r._animationsEnabled)("mat-mdc-dialog-container-with-actions",r._actionSectionCount>0))},features:[le],decls:3,vars:0,consts:[[1,"mat-mdc-dialog-inner-container","mdc-dialog__container"],[1,"mat-mdc-dialog-surface","mdc-dialog__surface"],["cdkPortalOutlet",""]],template:function(i,r){i&1&&(m(0,"div",0)(1,"div",1),A(2,Due,0,0,"ng-template",2),h()())},dependencies:[Ir],styles:[`.mat-mdc-dialog-container{width:100%;height:100%;display:block;box-sizing:border-box;max-height:inherit;min-height:inherit;min-width:inherit;max-width:inherit;outline:0}.cdk-overlay-pane.mat-mdc-dialog-panel{max-width:var(--mat-dialog-container-max-width, 560px);min-width:var(--mat-dialog-container-min-width, 280px)}@media(max-width: 599px){.cdk-overlay-pane.mat-mdc-dialog-panel{max-width:var(--mat-dialog-container-small-max-width, calc(100vw - 32px))}}.mat-mdc-dialog-inner-container{display:flex;flex-direction:row;align-items:center;justify-content:space-around;box-sizing:border-box;height:100%;opacity:0;transition:opacity linear var(--mat-dialog-transition-duration, 0ms);max-height:inherit;min-height:inherit;min-width:inherit;max-width:inherit}.mdc-dialog--closing .mat-mdc-dialog-inner-container{transition:opacity 75ms linear;transform:none}.mdc-dialog--open .mat-mdc-dialog-inner-container{opacity:1}._mat-animation-noopable .mat-mdc-dialog-inner-container{transition:none}.mat-mdc-dialog-surface{display:flex;flex-direction:column;flex-grow:0;flex-shrink:0;box-sizing:border-box;width:100%;height:100%;position:relative;overflow-y:auto;outline:0;transform:scale(0.8);transition:transform var(--mat-dialog-transition-duration, 0ms) cubic-bezier(0, 0, 0.2, 1);max-height:inherit;min-height:inherit;min-width:inherit;max-width:inherit;box-shadow:var(--mat-dialog-container-elevation-shadow, none);border-radius:var(--mat-dialog-container-shape, var(--mat-sys-corner-extra-large, 4px));background-color:var(--mat-dialog-container-color, var(--mat-sys-surface, white))}[dir=rtl] .mat-mdc-dialog-surface{text-align:right}.mdc-dialog--open .mat-mdc-dialog-surface,.mdc-dialog--closing .mat-mdc-dialog-surface{transform:none}._mat-animation-noopable .mat-mdc-dialog-surface{transition:none}.mat-mdc-dialog-surface::before{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;border:2px solid rgba(0,0,0,0);border-radius:inherit;content:"";pointer-events:none}.mat-mdc-dialog-title{display:block;position:relative;flex-shrink:0;box-sizing:border-box;margin:0 0 1px;padding:var(--mat-dialog-headline-padding, 6px 24px 13px)}.mat-mdc-dialog-title::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}[dir=rtl] .mat-mdc-dialog-title{text-align:right}.mat-mdc-dialog-container .mat-mdc-dialog-title{color:var(--mat-dialog-subhead-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87)));font-family:var(--mat-dialog-subhead-font, var(--mat-sys-headline-small-font, inherit));line-height:var(--mat-dialog-subhead-line-height, var(--mat-sys-headline-small-line-height, 1.5rem));font-size:var(--mat-dialog-subhead-size, var(--mat-sys-headline-small-size, 1rem));font-weight:var(--mat-dialog-subhead-weight, var(--mat-sys-headline-small-weight, 400));letter-spacing:var(--mat-dialog-subhead-tracking, var(--mat-sys-headline-small-tracking, 0.03125em))}.mat-mdc-dialog-content{display:block;flex-grow:1;box-sizing:border-box;margin:0;overflow:auto;max-height:65vh}.mat-mdc-dialog-content>:first-child{margin-top:0}.mat-mdc-dialog-content>:last-child{margin-bottom:0}.mat-mdc-dialog-container .mat-mdc-dialog-content{color:var(--mat-dialog-supporting-text-color, var(--mat-sys-on-surface-variant, rgba(0, 0, 0, 0.6)));font-family:var(--mat-dialog-supporting-text-font, var(--mat-sys-body-medium-font, inherit));line-height:var(--mat-dialog-supporting-text-line-height, var(--mat-sys-body-medium-line-height, 1.5rem));font-size:var(--mat-dialog-supporting-text-size, var(--mat-sys-body-medium-size, 1rem));font-weight:var(--mat-dialog-supporting-text-weight, var(--mat-sys-body-medium-weight, 400));letter-spacing:var(--mat-dialog-supporting-text-tracking, var(--mat-sys-body-medium-tracking, 0.03125em))}.mat-mdc-dialog-container .mat-mdc-dialog-content{padding:var(--mat-dialog-content-padding, 20px 24px)}.mat-mdc-dialog-container-with-actions .mat-mdc-dialog-content{padding:var(--mat-dialog-with-actions-content-padding, 20px 24px 0)}.mat-mdc-dialog-container .mat-mdc-dialog-title+.mat-mdc-dialog-content{padding-top:0}.mat-mdc-dialog-actions{display:flex;position:relative;flex-shrink:0;flex-wrap:wrap;align-items:center;box-sizing:border-box;min-height:52px;margin:0;border-top:1px solid rgba(0,0,0,0);padding:var(--mat-dialog-actions-padding, 16px 24px);justify-content:var(--mat-dialog-actions-alignment, flex-end)}@media(forced-colors: active){.mat-mdc-dialog-actions{border-top-color:CanvasText}}.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-start,.mat-mdc-dialog-actions[align=start]{justify-content:start}.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-center,.mat-mdc-dialog-actions[align=center]{justify-content:center}.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-end,.mat-mdc-dialog-actions[align=end]{justify-content:flex-end}.mat-mdc-dialog-actions .mat-button-base+.mat-button-base,.mat-mdc-dialog-actions .mat-mdc-button-base+.mat-mdc-button-base{margin-left:8px}[dir=rtl] .mat-mdc-dialog-actions .mat-button-base+.mat-button-base,[dir=rtl] .mat-mdc-dialog-actions .mat-mdc-button-base+.mat-mdc-button-base{margin-left:0;margin-right:8px}.mat-mdc-dialog-component-host{display:contents} +`],encapsulation:2})}return t})(),mz="--mat-dialog-transition-duration";function hz(t){return t==null?null:typeof t=="number"?t:t.endsWith("ms")?Gn(t.substring(0,t.length-2)):t.endsWith("s")?Gn(t.substring(0,t.length-1))*1e3:t==="0"?0:null}var f_=(function(t){return t[t.OPEN=0]="OPEN",t[t.CLOSING=1]="CLOSING",t[t.CLOSED=2]="CLOSED",t})(f_||{}),Ou=class{_ref;_config;_containerInstance;componentInstance;componentRef;disableClose;id;_afterOpened=new z;_beforeClosed=new z;_result;_closeFallbackTimeout;_state=f_.OPEN;_closeInteractionType;constructor(n,e,i){this._ref=n,this._config=e,this._containerInstance=i,this.disableClose=e.disableClose,this.id=n.id,n.addPanelClass("mat-mdc-dialog-panel"),i._animationStateChanged.pipe(ce(r=>r.state==="opened"),mt(1)).subscribe(()=>{this._afterOpened.next(),this._afterOpened.complete()}),i._animationStateChanged.pipe(ce(r=>r.state==="closed"),mt(1)).subscribe(()=>{clearTimeout(this._closeFallbackTimeout),this._finishDialogClose()}),n.overlayRef.detachments().subscribe(()=>{this._beforeClosed.next(this._result),this._beforeClosed.complete(),this._finishDialogClose()}),it(this.backdropClick(),this.keydownEvents().pipe(ce(r=>r.keyCode===27&&!this.disableClose&&!Gt(r)))).subscribe(r=>{this.disableClose||(r.preventDefault(),fz(this,r.type==="keydown"?"keyboard":"mouse"))})}close(n){let e=this._config.closePredicate;e&&!e(n,this._config,this.componentInstance)||(this._result=n,this._containerInstance._animationStateChanged.pipe(ce(i=>i.state==="closing"),mt(1)).subscribe(i=>{this._beforeClosed.next(n),this._beforeClosed.complete(),this._ref.overlayRef.detachBackdrop(),this._closeFallbackTimeout=setTimeout(()=>this._finishDialogClose(),i.totalTime+100)}),this._state=f_.CLOSING,this._containerInstance._startExitAnimation())}afterOpened(){return this._afterOpened}afterClosed(){return this._ref.closed}beforeClosed(){return this._beforeClosed}backdropClick(){return this._ref.backdropClick}keydownEvents(){return this._ref.keydownEvents}updatePosition(n){let e=this._ref.config.positionStrategy;return n&&(n.left||n.right)?n.left?e.left(n.left):e.right(n.right):e.centerHorizontally(),n&&(n.top||n.bottom)?n.top?e.top(n.top):e.bottom(n.bottom):e.centerVertically(),this._ref.updatePosition(),this}updateSize(n="",e=""){return this._ref.updateSize(n,e),this}addPanelClass(n){return this._ref.addPanelClass(n),this}removePanelClass(n){return this._ref.removePanelClass(n),this}getState(){return this._state}_finishDialogClose(){this._state=f_.CLOSED,this._ref.close(this._result,{focusOrigin:this._closeInteractionType}),this.componentInstance=null}};function fz(t,n,e){return t._closeInteractionType=n,t.close(e)}var LC=new O("MatMdcDialogData"),gz=new O("mat-mdc-dialog-default-options"),_z=new O("mat-mdc-dialog-scroll-strategy",{providedIn:"root",factory:()=>{let t=u(de);return()=>Tc(t)}}),Rn=(()=>{class t{_defaultOptions=u(gz,{optional:!0});_scrollStrategy=u(_z);_parentDialog=u(t,{optional:!0,skipSelf:!0});_idGenerator=u(et);_injector=u(de);_dialog=u(_I);_animationsDisabled=Qe();_openDialogsAtThisLevel=[];_afterAllClosedAtThisLevel=new z;_afterOpenedAtThisLevel=new z;dialogConfigClass=g_;_dialogRefConstructor;_dialogContainerType;_dialogDataToken;get openDialogs(){return this._parentDialog?this._parentDialog.openDialogs:this._openDialogsAtThisLevel}get afterOpened(){return this._parentDialog?this._parentDialog.afterOpened:this._afterOpenedAtThisLevel}_getAfterAllClosed(){let e=this._parentDialog;return e?e._getAfterAllClosed():this._afterAllClosedAtThisLevel}afterAllClosed=Fn(()=>this.openDialogs.length?this._getAfterAllClosed():this._getAfterAllClosed().pipe(Ue(void 0)));constructor(){this._dialogRefConstructor=Ou,this._dialogContainerType=pz,this._dialogDataToken=LC}open(e,i){let r;i=I(I({},this._defaultOptions||new g_),i),i.id=i.id||this._idGenerator.getId("mat-mdc-dialog-"),i.scrollStrategy=i.scrollStrategy||this._scrollStrategy();let o=this._dialog.open(e,Me(I({},i),{positionStrategy:Us(this._injector).centerHorizontally().centerVertically(),disableClose:!0,closePredicate:void 0,closeOnDestroy:!1,closeOnOverlayDetachments:!1,disableAnimations:this._animationsDisabled||i.enterAnimationDuration?.toLocaleString()==="0"||i.exitAnimationDuration?.toString()==="0",container:{type:this._dialogContainerType,providers:()=>[{provide:this.dialogConfigClass,useValue:i},{provide:id,useValue:i}]},templateContext:()=>({dialogRef:r}),providers:(a,s,l)=>(r=new this._dialogRefConstructor(a,i,l),r.updatePosition(i?.position),[{provide:this._dialogContainerType,useValue:l},{provide:this._dialogDataToken,useValue:s.data},{provide:this._dialogRefConstructor,useValue:r}])}));return r.componentRef=o.componentRef,r.componentInstance=o.componentInstance,this.openDialogs.push(r),this.afterOpened.next(r),r.afterClosed().subscribe(()=>{let a=this.openDialogs.indexOf(r);a>-1&&(this.openDialogs.splice(a,1),this.openDialogs.length||this._getAfterAllClosed().next())}),r}closeAll(){this._closeDialogs(this.openDialogs)}getDialogById(e){return this.openDialogs.find(i=>i.id===e)}ngOnDestroy(){this._closeDialogs(this._openDialogsAtThisLevel),this._afterAllClosedAtThisLevel.complete(),this._afterOpenedAtThisLevel.complete()}_closeDialogs(e){let i=e.length;for(;i--;)e[i].close()}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var bz=(()=>{class t{_dialogRef=u(Ou,{optional:!0});_elementRef=u(Y);_dialog=u(Rn);constructor(){}ngOnInit(){this._dialogRef||(this._dialogRef=Sue(this._elementRef,this._dialog.openDialogs)),this._dialogRef&&Promise.resolve().then(()=>{this._onAdd()})}ngOnDestroy(){this._dialogRef?._containerInstance&&Promise.resolve().then(()=>{this._onRemove()})}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t})}return t})(),vI=(()=>{class t extends bz{id=u(et).getId("mat-mdc-dialog-title-");_onAdd(){this._dialogRef._containerInstance?._addAriaLabelledBy?.(this.id)}_onRemove(){this._dialogRef?._containerInstance?._removeAriaLabelledBy?.(this.id)}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","mat-dialog-title",""],["","matDialogTitle",""]],hostAttrs:[1,"mat-mdc-dialog-title","mdc-dialog__title"],hostVars:1,hostBindings:function(i,r){i&2&&pi("id",r.id)},inputs:{id:"id"},exportAs:["matDialogTitle"],features:[le]})}return t})(),yI=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","mat-dialog-content",""],["mat-dialog-content"],["","matDialogContent",""]],hostAttrs:[1,"mat-mdc-dialog-content","mdc-dialog__content"],features:[Mm([Xa])]})}return t})(),xI=(()=>{class t extends bz{align;_onAdd(){this._dialogRef._containerInstance?._updateActionSectionCount?.(1)}_onRemove(){this._dialogRef._containerInstance?._updateActionSectionCount?.(-1)}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","mat-dialog-actions",""],["mat-dialog-actions"],["","matDialogActions",""]],hostAttrs:[1,"mat-mdc-dialog-actions","mdc-dialog__actions"],hostVars:6,hostBindings:function(i,r){i&2&&G("mat-mdc-dialog-actions-align-start",r.align==="start")("mat-mdc-dialog-actions-align-center",r.align==="center")("mat-mdc-dialog-actions-align-end",r.align==="end")},inputs:{align:"align"},features:[le]})}return t})();function Sue(t,n){let e=t.nativeElement.parentElement;for(;e&&!e.classList.contains("mat-mdc-dialog-container");)e=e.parentElement;return e?n.find(i=>i.id===e.id):null}var Pn=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[Rn],imports:[cz,cr,Oo,De,De]})}return t})();var Fr=(()=>{let n=class n{constructor(){this.dialogRef=u(Ou),this.data=u(LC)}confirm(){this.dialogRef.close(!0)}cancel(){this.dialogRef.close(!1)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-confirm-dialog"]],decls:12,vars:4,consts:[["mat-dialog-title",""],["color","warn",2,"vertical-align","middle","margin-right","8px"],["align","end"],["mat-button","",3,"click"],["mat-raised-button","","color","warn","cdkFocusInitial","",3,"click"]],template:function(r,o){r&1&&(m(0,"h2",0)(1,"mat-icon",1),f(2,"warning"),h(),f(3),h(),m(4,"mat-dialog-content")(5,"p"),f(6),h()(),m(7,"mat-dialog-actions",2)(8,"button",3),S("click",function(){return o.cancel()}),f(9),h(),m(10,"button",4),S("click",function(){return o.confirm()}),f(11),h()()),r&2&&(g(3),fe(" ",o.data.title,` +`),g(3),N(o.data.message),g(3),fe(" ",o.data.cancelText||"Cancel"," "),g(2),fe(" ",o.data.confirmText||"Delete"," "))},dependencies:[Pn,vI,xI,yI,Fe,_t,Ge,Ze],encapsulation:2});let t=n;return t})();function Tue(t,n){if(t&1&&(m(0,"div",2),f(1),h()),t&2){let e=x();g(),N(e.title())}}function Iue(t,n){if(t&1&&(m(0,"div",3),f(1),h()),t&2){let e=x();g(),N(e.message())}}var ap=(()=>{let n=class n{constructor(){this.code=re(""),this.title=re(""),this.message=re("")}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["error-code"]],inputs:{code:[1,"code"],title:[1,"title"],message:[1,"message"]},decls:8,vars:3,consts:[[1,"matero-error-wrap"],[1,"matero-error-code"],[1,"matero-error-title"],[1,"matero-error-message"],["matButton","elevated","color","primary","routerLink","/"]],template:function(r,o){r&1&&(m(0,"div",0)(1,"div",1),f(2),h(),L(3,Tue,2,1,"div",2),L(4,Iue,2,1,"div",3),m(5,"div")(6,"a",4),f(7,"Back to Home"),h()()()),r&2&&(g(2),N(o.code()),g(),V(o.title()?3:-1),g(),V(o.message()?4:-1))},dependencies:[Wn,Fe,_t],styles:[`.matero-error-wrap{text-align:center}.matero-error-code{padding:1.25rem 0;font-size:10rem;text-shadow:.1325825215rem .1325825215rem 0 rgba(0,0,0,.028575),.2651650429rem .2651650429rem 0 rgba(0,0,0,.02715),.3977475644rem .3977475644rem 0 rgba(0,0,0,.025725),.5303300859rem .5303300859rem 0 rgba(0,0,0,.0243),.6629126074rem .6629126074rem 0 rgba(0,0,0,.022875),.7954951288rem .7954951288rem 0 rgba(0,0,0,.02145),.9280776503rem .9280776503rem 0 rgba(0,0,0,.020025),1.0606601718rem 1.0606601718rem 0 rgba(0,0,0,.0186),1.1932426933rem 1.1932426933rem 0 rgba(0,0,0,.017175),1.3258252147rem 1.3258252147rem 0 rgba(0,0,0,.01575),1.4584077362rem 1.4584077362rem 0 rgba(0,0,0,.014325),1.5909902577rem 1.5909902577rem 0 rgba(0,0,0,.0129),1.7235727791rem 1.7235727791rem 0 rgba(0,0,0,.011475),1.8561553006rem 1.8561553006rem 0 rgba(0,0,0,.01005),1.9887378221rem 1.9887378221rem 0 rgba(0,0,0,.008625),2.1213203436rem 2.1213203436rem 0 rgba(0,0,0,.0072),2.253902865rem 2.253902865rem 0 rgba(0,0,0,.005775),2.3864853865rem 2.3864853865rem 0 rgba(0,0,0,.00435),2.519067908rem 2.519067908rem 0 rgba(0,0,0,.002925),2.6516504294rem 2.6516504294rem 0 rgba(0,0,0,.0015)}.matero-error-title{margin:0 0 1rem;font-size:1.25rem;font-weight:500;line-height:2rem}.matero-error-message{margin:0 0 1rem;font-size:1rem;font-weight:400;line-height:1.75rem} +`],encapsulation:2});let t=n;return t})();function Aue(t,n){if(t&1&&M(0,"breadcrumb",2),t&2){let e=x();v("nav",e.nav())}}var Lt=(()=>{let n=class n{constructor(){this.router=u(Ae),this.menu=u(bo),this.title=re(""),this.subtitle=re(""),this.nav=re([]),this.hideBreadcrumb=re(!1,{transform:B}),this.titleName=ci(()=>{let i=this.router.url.slice(1).split("/"),r=this.menu.getLevel(i);return this.title()||r[r.length-1]})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["page-header"]],hostAttrs:[1,"matero-page-header"],inputs:{title:[1,"title"],subtitle:[1,"subtitle"],nav:[1,"nav"],hideBreadcrumb:[1,"hideBreadcrumb"]},decls:7,vars:5,consts:[[1,"matero-page-header-inner"],[1,"matero-page-title"],[3,"nav"]],template:function(r,o){r&1&&(m(0,"div",0)(1,"h1",1),f(2),me(3,"translate"),m(4,"small"),f(5),h()(),L(6,Aue,1,1,"breadcrumb",2),h()),r&2&&(g(2),fe(" ",Re(3,3,o.titleName())," "),g(3),N(o.subtitle()),g(),V(o.hideBreadcrumb()?-1:6))},dependencies:[lz,Rr,Or],styles:[`.matero-page-header{display:block;padding:1rem;margin-bottom:1rem;color:#ffffffde;background-color:#0074e9;border-radius:var(--mat-sys-corner-medium)}.matero-page-header .matero-breadcrumb{margin-top:.5rem;margin-bottom:0}.matero-page-title{margin:0;font-size:1.5rem;font-weight:400} +`],encapsulation:2});let t=n;return t})();var vz=(()=>{let n=class n{constructor(){this.change=new U,this.valueSignal=he("ltr")}get value(){return this.valueSignal()}set value(i){this.valueSignal.set(i),this.change.next(i)}ngOnDestroy(){this.change.complete()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var yz=(()=>{let n=class n{constructor(){this.messages=[]}add(i){this.messages.push(i)}clear(){this.messages=[]}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var xz=(()=>{let n=class n{get(i){return JSON.parse(localStorage.getItem(i)||"{}")||{}}set(i,r){return localStorage.setItem(i,JSON.stringify(r)),!0}has(i){return!!localStorage.getItem(i)}remove(i){localStorage.removeItem(i)}clear(){localStorage.clear()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var Cz=(()=>{let n=class n{constructor(){this.translate=u(ca),this.paginatorIntl=new Pc,this.translate.onLangChange.subscribe(i=>this.getPaginatorIntl())}getPaginatorIntl(){return this.paginatorIntl.itemsPerPageLabel=this.translate.instant("paginator.items_per_page_label"),this.paginatorIntl.previousPageLabel=this.translate.instant("paginator.previous_page_label"),this.paginatorIntl.nextPageLabel=this.translate.instant("paginator.next_page_label"),this.paginatorIntl.firstPageLabel=this.translate.instant("paginator.first_page_label"),this.paginatorIntl.lastPageLabel=this.translate.instant("paginator.last_page_label"),this.paginatorIntl.getRangeLabel=this.getRangeLabel.bind(this),this.paginatorIntl.changes.next(),this.paginatorIntl}getRangeLabel(i,r,o){if(o===0||r===0)return this.translate.instant("paginator.range_page_label_1",{length:o});o=Math.max(o,0);let a=i*r,s=a{if(t.destroyed){e.next();return}return t.onDestroy(e.next.bind(e))});return e=>e.pipe(xe(n))}function wz(t){let n=KM(t);return new Ne(e=>{let i=n?.onDestroy(()=>e.complete()),r=t.subscribe(o=>e.next(o));return()=>{r.unsubscribe(),i?.()}})}var Oue=["content"],Rue=["scroll"],Pue=["padding"],Ez=["*"],__=t=>({searchTerm:t});function Fue(t,n){if(t&1&&(m(0,"div",3),qe(1,6),h()),t&2){let e=x();g(),v("ngTemplateOutlet",e.headerTemplate())("ngTemplateOutletContext",$t(2,__,e.filterValue()))}}function Nue(t,n){if(t&1&&(m(0,"div",5),qe(1,6),h()),t&2){let e=x();g(),v("ngTemplateOutlet",e.footerTemplate())("ngTemplateOutletContext",$t(2,__,e.filterValue()))}}var Lue=["searchInput"],Vue=["clearButton"],Bue=(t,n,e)=>({item:t,clear:n,label:e}),jue=(t,n)=>({items:t,clear:n}),Hue=(t,n,e,i)=>({item:t,item$:n,index:e,searchTerm:i});function zue(t,n){if(t&1&&(m(0,"div",20),f(1),h()),t&2){let e=x(2);g(),N(e.placeholder()??e.config.placeholder)}}function Uue(t,n){}function $ue(t,n){if(t&1&&A(0,zue,2,1,"ng-template",null,1,Mi)(2,Uue,0,0,"ng-template",19),t&2){let e=Te(1),i=x();g(2),v("ngTemplateOutlet",i.placeholderTemplate()||e)}}function Wue(t,n){if(t&1){let e=q();m(0,"span",23),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.unselect(r))}),f(1,"\xD7"),h(),M(2,"span",24)}if(t&2){let e=x().$implicit,i=x(2);g(2),v("ngItemLabel",e.label)("escape",i.escapeHTML)}}function Gue(t,n){}function que(t,n){if(t&1&&(m(0,"div",22),A(1,Wue,3,2,"ng-template",null,2,Mi)(3,Gue,0,0,"ng-template",12),h()),t&2){let e=n.$implicit,i=Te(2),r=x(2);G("ng-value-disabled",e.disabled),g(3),v("ngTemplateOutlet",r.labelTemplate()||i)("ngTemplateOutletContext",Hd(4,Bue,e.value,r.clearItem,e.label))}}function Yue(t,n){if(t&1&&Mt(0,que,4,8,"div",21,A0().trackByOption,!0),t&2){let e=x();Et(e.selectedItems)}}function Que(t,n){}function Kue(t,n){if(t&1&&A(0,Que,0,0,"ng-template",12),t&2){let e=x();v("ngTemplateOutlet",e.multiLabelTemplate())("ngTemplateOutletContext",ja(2,jue,e.selectedValues,e.clearItem))}}function Zue(t,n){t&1&&M(0,"div",25)}function Xue(t,n){}function Jue(t,n){if(t&1&&A(0,Zue,1,0,"ng-template",null,3,Mi)(2,Xue,0,0,"ng-template",19),t&2){let e=Te(1),i=x();g(2),v("ngTemplateOutlet",i.loadingSpinnerTemplate()||e)}}function eme(t,n){if(t&1&&qe(0,19),t&2){let e=x(2);v("ngTemplateOutlet",e.clearButtonTemplate())}}function tme(t,n){if(t&1){let e=q();m(0,"span",27,4),S("click",function(r){k(e);let o=x(2);return T(o.handleClearClick(r))}),m(2,"span",28),f(3,"\xD7"),h()()}if(t&2){let e=x(2);v("title",WM(e.clearAllText()||e.config.clearAllText)),X("tabindex",e.tabFocusOnClear()?0:-1)}}function ime(t,n){if(t&1&&L(0,eme,1,1,"ng-container",19)(1,tme,4,3,"span",26),t&2){let e=x();V(e.clearButtonTemplate()?0:1)}}function nme(t,n){if(t&1&&M(0,"span",33),t&2){let e=x().$implicit,i=x(2);v("ngItemLabel",e.label)("escape",i.escapeHTML)}}function rme(t,n){}function ome(t,n){if(t&1){let e=q();m(0,"div",32),S("click",function(){let r=k(e).$implicit,o=x(2);return T(o.toggleItem(r))})("mouseover",function(){let r=k(e).$implicit,o=x(2);return T(o.onItemHover(r))}),A(1,nme,1,2,"ng-template",null,5,Mi)(3,rme,0,0,"ng-template",12),h()}if(t&2){let e=n.$implicit,i=Te(2),r=x(2);G("ng-option-disabled",e.disabled)("ng-option-selected",e.selected)("ng-optgroup",e.children)("ng-option",!e.children)("ng-option-child",!!e.parent)("ng-option-marked",e===r.itemsList.markedItem),X("role",e.children?"group":"option")("aria-selected",e.selected)("id",e==null?null:e.htmlId)("aria-setsize",r.itemsList.filteredItems.length)("aria-posinset",e.index+1),g(3),v("ngTemplateOutlet",e.children?r.optgroupTemplate()||i:r.optionTemplate()||i)("ngTemplateOutletContext",km(19,Hue,e.value,e,e.index,r.searchTerm))}}function ame(t,n){if(t&1&&(m(0,"span")(1,"span",35),f(2),h(),f(3),h()),t&2){let e=x(3);g(2),N(e.addTagText()||e.config.addTagText),g(),fe('"',e.searchTerm,'"')}}function sme(t,n){}function lme(t,n){if(t&1){let e=q();m(0,"div",34),S("mouseover",function(){k(e);let r=x(2);return T(r.itemsList.unmarkItem())})("click",function(){k(e);let r=x(2);return T(r.selectTag())}),A(1,ame,4,2,"ng-template",null,6,Mi)(3,sme,0,0,"ng-template",12),h()}if(t&2){let e=Te(2),i=x(2);G("ng-option-marked",!i.itemsList.markedItem),g(3),v("ngTemplateOutlet",i.tagTemplate()||e)("ngTemplateOutletContext",$t(4,__,i.searchTerm))}}function cme(t,n){if(t&1&&(m(0,"div",36),f(1),h()),t&2){let e=x(3);g(),N(e.notFoundText()??e.config.notFoundText)}}function dme(t,n){}function ume(t,n){if(t&1&&A(0,cme,2,1,"ng-template",null,7,Mi)(2,dme,0,0,"ng-template",12),t&2){let e=Te(1),i=x(2);g(2),v("ngTemplateOutlet",i.notFoundTemplate()||e)("ngTemplateOutletContext",$t(2,__,i.searchTerm))}}function mme(t,n){if(t&1&&(m(0,"div",36),f(1),h()),t&2){let e=x(3);g(),N(e.typeToSearchText()||e.config.typeToSearchText)}}function hme(t,n){}function pme(t,n){if(t&1&&A(0,mme,2,1,"ng-template",null,8,Mi)(2,hme,0,0,"ng-template",19),t&2){let e=Te(1),i=x(2);g(2),v("ngTemplateOutlet",i.typeToSearchTemplate()||e)}}function fme(t,n){if(t&1&&(m(0,"div",36),f(1),h()),t&2){let e=x(3);g(),N(e.loadingText()||e.config.loadingText)}}function gme(t,n){}function _me(t,n){if(t&1&&A(0,fme,2,1,"ng-template",null,9,Mi)(2,gme,0,0,"ng-template",12),t&2){let e=Te(1),i=x(2);g(2),v("ngTemplateOutlet",i.loadingTextTemplate()||e)("ngTemplateOutletContext",$t(2,__,i.searchTerm))}}function bme(t,n){if(t&1){let e=q();m(0,"ng-dropdown-panel",29),S("update",function(r){k(e);let o=x();return T(o.viewPortItems=r)})("scroll",function(r){k(e);let o=x();return T(o.scroll.emit(r))})("scrollToEnd",function(r){k(e);let o=x();return T(o.scrollToEnd.emit(r))})("outsideClick",function(){k(e);let r=x();return T(r.close())}),lt(1),Mt(2,ome,4,24,"div",30,A0().trackByOption,!0),L(4,lme,4,6,"div",31),ot(),L(5,ume,3,4),L(6,pme,3,1),L(7,_me,3,4),h()}if(t&2){let e=x(),i=e.appendTo()||e.config.appendTo;at(i?e.ngClass()?e.ngClass():e.classes:null),G("ng-select-multiple",e.multiple()),v("virtualScroll",e.virtualScroll()??!e.config.disableVirtualScroll??!1)("bufferAmount",e.bufferAmount())("appendTo",i)("position",e.dropdownPosition())("outsideClickEvent",e.outsideClickEvent())("headerTemplate",e.headerTemplate())("footerTemplate",e.footerTemplate())("filterValue",e.searchTerm)("items",e.itemsList.filteredItems)("showAddTag",e.showAddTag)("markedItem",e.itemsList.markedItem)("id",e.dropdownId)("ariaLabelDropdown",e.ariaLabelDropdown()),g(2),Et(e.viewPortItems),g(2),V(e.showAddTag?4:-1),g(),V(e.showNoItemsFound()?5:-1),g(),V(e.showTypeToSearch()?6:-1),g(),V(e.loading()&&e.itemsList.filteredItems.length===0?7:-1)}}function vme(t,n){if(t&1&&f(0),t&2){let e=x();fe(" ",e.notFoundText()??e.config.notFoundText," ")}}var Sz=/[&<>"']/g,yme=RegExp(Sz.source),xme={"&":"&","<":"<",">":">",'"':""","'":"'"};function Cme(t){return t&&yme.test(t)?t.replace(Sz,n=>xme[n]):t}function on(t){return t!=null}function Pu(t){return typeof t=="object"&&on(t)}function wme(t){return t instanceof Promise}function BC(t){return t instanceof Function}var Dme=(()=>{let n=class n{constructor(){this.element=u(Y),this.ngItemLabel=re(),this.escape=re(!0),zr(()=>{this.element.nativeElement.innerHTML=this.escape()?Cme(this.ngItemLabel()):this.ngItemLabel()})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ngItemLabel",""]],inputs:{ngItemLabel:[1,"ngItemLabel"],escape:[1,"escape"]}});let t=n;return t})(),jC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-option-tmp",""]]});let t=n;return t})(),HC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-optgroup-tmp",""]]});let t=n;return t})(),zC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-label-tmp",""]]});let t=n;return t})(),UC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-multi-label-tmp",""]]});let t=n;return t})(),$C=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-header-tmp",""]]});let t=n;return t})(),WC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-footer-tmp",""]]});let t=n;return t})(),GC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-notfound-tmp",""]]});let t=n;return t})(),qC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-placeholder-tmp",""]]});let t=n;return t})(),YC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-typetosearch-tmp",""]]});let t=n;return t})(),QC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-loadingtext-tmp",""]]});let t=n;return t})(),KC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-tag-tmp",""]]});let t=n;return t})(),ZC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-loadingspinner-tmp",""]]});let t=n;return t})(),XC=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-clearbutton-tmp",""]]});let t=n;return t})(),Mme=(()=>{let n=class n{constructor(){this.fixedPlaceholder=!0,this.notFoundText="No items found",this.typeToSearchText="Type to search",this.addTagText="Add item",this.loadingText="Loading...",this.clearAllText="Clear all",this.disableVirtualScroll=!0,this.openOnEnter=!0,this.appearance="underline",this.tabFocusOnClear=!0,this.outsideClickEvent="click"}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})(),Eme=(()=>{let n=class n{warn(i){console.warn(i)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();function kz(){return"axxxxxxxxxxx".replace(/[x]/g,()=>(Math.random()*16|0).toString(16))}var Sme={"\u24B6":"A",\uFF21:"A",\u00C0:"A",\u00C1:"A",\u00C2:"A",\u1EA6:"A",\u1EA4:"A",\u1EAA:"A",\u1EA8:"A",\u00C3:"A",\u0100:"A",\u0102:"A",\u1EB0:"A",\u1EAE:"A",\u1EB4:"A",\u1EB2:"A",\u0226:"A",\u01E0:"A",\u00C4:"A",\u01DE:"A",\u1EA2:"A",\u00C5:"A",\u01FA:"A",\u01CD:"A",\u0200:"A",\u0202:"A",\u1EA0:"A",\u1EAC:"A",\u1EB6:"A",\u1E00:"A",\u0104:"A","\u023A":"A","\u2C6F":"A","\uA732":"AA",\u00C6:"AE",\u01FC:"AE",\u01E2:"AE","\uA734":"AO","\uA736":"AU","\uA738":"AV","\uA73A":"AV","\uA73C":"AY","\u24B7":"B",\uFF22:"B",\u1E02:"B",\u1E04:"B",\u1E06:"B","\u0243":"B",\u0182:"B",\u0181:"B","\u24B8":"C",\uFF23:"C",\u0106:"C",\u0108:"C",\u010A:"C",\u010C:"C",\u00C7:"C",\u1E08:"C",\u0187:"C","\u023B":"C","\uA73E":"C","\u24B9":"D",\uFF24:"D",\u1E0A:"D",\u010E:"D",\u1E0C:"D",\u1E10:"D",\u1E12:"D",\u1E0E:"D",\u0110:"D",\u018B:"D",\u018A:"D",\u0189:"D","\uA779":"D",\u01F1:"DZ",\u01C4:"DZ",\u01F2:"Dz",\u01C5:"Dz","\u24BA":"E",\uFF25:"E",\u00C8:"E",\u00C9:"E",\u00CA:"E",\u1EC0:"E",\u1EBE:"E",\u1EC4:"E",\u1EC2:"E",\u1EBC:"E",\u0112:"E",\u1E14:"E",\u1E16:"E",\u0114:"E",\u0116:"E",\u00CB:"E",\u1EBA:"E",\u011A:"E",\u0204:"E",\u0206:"E",\u1EB8:"E",\u1EC6:"E",\u0228:"E",\u1E1C:"E",\u0118:"E",\u1E18:"E",\u1E1A:"E",\u0190:"E",\u018E:"E","\u24BB":"F",\uFF26:"F",\u1E1E:"F",\u0191:"F","\uA77B":"F","\u24BC":"G",\uFF27:"G",\u01F4:"G",\u011C:"G",\u1E20:"G",\u011E:"G",\u0120:"G",\u01E6:"G",\u0122:"G",\u01E4:"G",\u0193:"G","\uA7A0":"G","\uA77D":"G","\uA77E":"G","\u24BD":"H",\uFF28:"H",\u0124:"H",\u1E22:"H",\u1E26:"H",\u021E:"H",\u1E24:"H",\u1E28:"H",\u1E2A:"H",\u0126:"H","\u2C67":"H","\u2C75":"H","\uA78D":"H","\u24BE":"I",\uFF29:"I",\u00CC:"I",\u00CD:"I",\u00CE:"I",\u0128:"I",\u012A:"I",\u012C:"I",\u0130:"I",\u00CF:"I",\u1E2E:"I",\u1EC8:"I",\u01CF:"I",\u0208:"I",\u020A:"I",\u1ECA:"I",\u012E:"I",\u1E2C:"I",\u0197:"I","\u24BF":"J",\uFF2A:"J",\u0134:"J","\u0248":"J","\u24C0":"K",\uFF2B:"K",\u1E30:"K",\u01E8:"K",\u1E32:"K",\u0136:"K",\u1E34:"K",\u0198:"K","\u2C69":"K","\uA740":"K","\uA742":"K","\uA744":"K","\uA7A2":"K","\u24C1":"L",\uFF2C:"L",\u013F:"L",\u0139:"L",\u013D:"L",\u1E36:"L",\u1E38:"L",\u013B:"L",\u1E3C:"L",\u1E3A:"L",\u0141:"L","\u023D":"L","\u2C62":"L","\u2C60":"L","\uA748":"L","\uA746":"L","\uA780":"L",\u01C7:"LJ",\u01C8:"Lj","\u24C2":"M",\uFF2D:"M",\u1E3E:"M",\u1E40:"M",\u1E42:"M","\u2C6E":"M",\u019C:"M","\u24C3":"N",\uFF2E:"N",\u01F8:"N",\u0143:"N",\u00D1:"N",\u1E44:"N",\u0147:"N",\u1E46:"N",\u0145:"N",\u1E4A:"N",\u1E48:"N","\u0220":"N",\u019D:"N","\uA790":"N","\uA7A4":"N",\u01CA:"NJ",\u01CB:"Nj","\u24C4":"O",\uFF2F:"O",\u00D2:"O",\u00D3:"O",\u00D4:"O",\u1ED2:"O",\u1ED0:"O",\u1ED6:"O",\u1ED4:"O",\u00D5:"O",\u1E4C:"O",\u022C:"O",\u1E4E:"O",\u014C:"O",\u1E50:"O",\u1E52:"O",\u014E:"O",\u022E:"O",\u0230:"O",\u00D6:"O",\u022A:"O",\u1ECE:"O",\u0150:"O",\u01D1:"O",\u020C:"O",\u020E:"O",\u01A0:"O",\u1EDC:"O",\u1EDA:"O",\u1EE0:"O",\u1EDE:"O",\u1EE2:"O",\u1ECC:"O",\u1ED8:"O",\u01EA:"O",\u01EC:"O",\u00D8:"O",\u01FE:"O",\u0186:"O",\u019F:"O","\uA74A":"O","\uA74C":"O",\u01A2:"OI","\uA74E":"OO",\u0222:"OU","\u24C5":"P",\uFF30:"P",\u1E54:"P",\u1E56:"P",\u01A4:"P","\u2C63":"P","\uA750":"P","\uA752":"P","\uA754":"P","\u24C6":"Q",\uFF31:"Q","\uA756":"Q","\uA758":"Q","\u024A":"Q","\u24C7":"R",\uFF32:"R",\u0154:"R",\u1E58:"R",\u0158:"R",\u0210:"R",\u0212:"R",\u1E5A:"R",\u1E5C:"R",\u0156:"R",\u1E5E:"R","\u024C":"R","\u2C64":"R","\uA75A":"R","\uA7A6":"R","\uA782":"R","\u24C8":"S",\uFF33:"S","\u1E9E":"S",\u015A:"S",\u1E64:"S",\u015C:"S",\u1E60:"S",\u0160:"S",\u1E66:"S",\u1E62:"S",\u1E68:"S",\u0218:"S",\u015E:"S","\u2C7E":"S","\uA7A8":"S","\uA784":"S","\u24C9":"T",\uFF34:"T",\u1E6A:"T",\u0164:"T",\u1E6C:"T",\u021A:"T",\u0162:"T",\u1E70:"T",\u1E6E:"T",\u0166:"T",\u01AC:"T",\u01AE:"T","\u023E":"T","\uA786":"T","\uA728":"TZ","\u24CA":"U",\uFF35:"U",\u00D9:"U",\u00DA:"U",\u00DB:"U",\u0168:"U",\u1E78:"U",\u016A:"U",\u1E7A:"U",\u016C:"U",\u00DC:"U",\u01DB:"U",\u01D7:"U",\u01D5:"U",\u01D9:"U",\u1EE6:"U",\u016E:"U",\u0170:"U",\u01D3:"U",\u0214:"U",\u0216:"U",\u01AF:"U",\u1EEA:"U",\u1EE8:"U",\u1EEE:"U",\u1EEC:"U",\u1EF0:"U",\u1EE4:"U",\u1E72:"U",\u0172:"U",\u1E76:"U",\u1E74:"U","\u0244":"U","\u24CB":"V",\uFF36:"V",\u1E7C:"V",\u1E7E:"V",\u01B2:"V","\uA75E":"V","\u0245":"V","\uA760":"VY","\u24CC":"W",\uFF37:"W",\u1E80:"W",\u1E82:"W",\u0174:"W",\u1E86:"W",\u1E84:"W",\u1E88:"W","\u2C72":"W","\u24CD":"X",\uFF38:"X",\u1E8A:"X",\u1E8C:"X","\u24CE":"Y",\uFF39:"Y",\u1EF2:"Y",\u00DD:"Y",\u0176:"Y",\u1EF8:"Y",\u0232:"Y",\u1E8E:"Y",\u0178:"Y",\u1EF6:"Y",\u1EF4:"Y",\u01B3:"Y","\u024E":"Y","\u1EFE":"Y","\u24CF":"Z",\uFF3A:"Z",\u0179:"Z",\u1E90:"Z",\u017B:"Z",\u017D:"Z",\u1E92:"Z",\u1E94:"Z",\u01B5:"Z",\u0224:"Z","\u2C7F":"Z","\u2C6B":"Z","\uA762":"Z","\u24D0":"a",\uFF41:"a",\u1E9A:"a",\u00E0:"a",\u00E1:"a",\u00E2:"a",\u1EA7:"a",\u1EA5:"a",\u1EAB:"a",\u1EA9:"a",\u00E3:"a",\u0101:"a",\u0103:"a",\u1EB1:"a",\u1EAF:"a",\u1EB5:"a",\u1EB3:"a",\u0227:"a",\u01E1:"a",\u00E4:"a",\u01DF:"a",\u1EA3:"a",\u00E5:"a",\u01FB:"a",\u01CE:"a",\u0201:"a",\u0203:"a",\u1EA1:"a",\u1EAD:"a",\u1EB7:"a",\u1E01:"a",\u0105:"a","\u2C65":"a",\u0250:"a","\uA733":"aa",\u00E6:"ae",\u01FD:"ae",\u01E3:"ae","\uA735":"ao","\uA737":"au","\uA739":"av","\uA73B":"av","\uA73D":"ay","\u24D1":"b",\uFF42:"b",\u1E03:"b",\u1E05:"b",\u1E07:"b",\u0180:"b",\u0183:"b",\u0253:"b","\u24D2":"c",\uFF43:"c",\u0107:"c",\u0109:"c",\u010B:"c",\u010D:"c",\u00E7:"c",\u1E09:"c",\u0188:"c","\u023C":"c","\uA73F":"c","\u2184":"c","\u24D3":"d",\uFF44:"d",\u1E0B:"d",\u010F:"d",\u1E0D:"d",\u1E11:"d",\u1E13:"d",\u1E0F:"d",\u0111:"d",\u018C:"d",\u0256:"d",\u0257:"d","\uA77A":"d",\u01F3:"dz",\u01C6:"dz","\u24D4":"e",\uFF45:"e",\u00E8:"e",\u00E9:"e",\u00EA:"e",\u1EC1:"e",\u1EBF:"e",\u1EC5:"e",\u1EC3:"e",\u1EBD:"e",\u0113:"e",\u1E15:"e",\u1E17:"e",\u0115:"e",\u0117:"e",\u00EB:"e",\u1EBB:"e",\u011B:"e",\u0205:"e",\u0207:"e",\u1EB9:"e",\u1EC7:"e",\u0229:"e",\u1E1D:"e",\u0119:"e",\u1E19:"e",\u1E1B:"e","\u0247":"e",\u025B:"e",\u01DD:"e","\u24D5":"f",\uFF46:"f",\u1E1F:"f",\u0192:"f","\uA77C":"f","\u24D6":"g",\uFF47:"g",\u01F5:"g",\u011D:"g",\u1E21:"g",\u011F:"g",\u0121:"g",\u01E7:"g",\u0123:"g",\u01E5:"g",\u0260:"g","\uA7A1":"g","\u1D79":"g","\uA77F":"g","\u24D7":"h",\uFF48:"h",\u0125:"h",\u1E23:"h",\u1E27:"h",\u021F:"h",\u1E25:"h",\u1E29:"h",\u1E2B:"h",\u1E96:"h",\u0127:"h","\u2C68":"h","\u2C76":"h",\u0265:"h",\u0195:"hv","\u24D8":"i",\uFF49:"i",\u00EC:"i",\u00ED:"i",\u00EE:"i",\u0129:"i",\u012B:"i",\u012D:"i",\u00EF:"i",\u1E2F:"i",\u1EC9:"i",\u01D0:"i",\u0209:"i",\u020B:"i",\u1ECB:"i",\u012F:"i",\u1E2D:"i",\u0268:"i",\u0131:"i","\u24D9":"j",\uFF4A:"j",\u0135:"j",\u01F0:"j","\u0249":"j","\u24DA":"k",\uFF4B:"k",\u1E31:"k",\u01E9:"k",\u1E33:"k",\u0137:"k",\u1E35:"k",\u0199:"k","\u2C6A":"k","\uA741":"k","\uA743":"k","\uA745":"k","\uA7A3":"k","\u24DB":"l",\uFF4C:"l",\u0140:"l",\u013A:"l",\u013E:"l",\u1E37:"l",\u1E39:"l",\u013C:"l",\u1E3D:"l",\u1E3B:"l",\u017F:"l",\u0142:"l",\u019A:"l",\u026B:"l","\u2C61":"l","\uA749":"l","\uA781":"l","\uA747":"l",\u01C9:"lj","\u24DC":"m",\uFF4D:"m",\u1E3F:"m",\u1E41:"m",\u1E43:"m",\u0271:"m",\u026F:"m","\u24DD":"n",\uFF4E:"n",\u01F9:"n",\u0144:"n",\u00F1:"n",\u1E45:"n",\u0148:"n",\u1E47:"n",\u0146:"n",\u1E4B:"n",\u1E49:"n",\u019E:"n",\u0272:"n",\u0149:"n","\uA791":"n","\uA7A5":"n",\u01CC:"nj","\u24DE":"o",\uFF4F:"o",\u00F2:"o",\u00F3:"o",\u00F4:"o",\u1ED3:"o",\u1ED1:"o",\u1ED7:"o",\u1ED5:"o",\u00F5:"o",\u1E4D:"o",\u022D:"o",\u1E4F:"o",\u014D:"o",\u1E51:"o",\u1E53:"o",\u014F:"o",\u022F:"o",\u0231:"o",\u00F6:"o",\u022B:"o",\u1ECF:"o",\u0151:"o",\u01D2:"o",\u020D:"o",\u020F:"o",\u01A1:"o",\u1EDD:"o",\u1EDB:"o",\u1EE1:"o",\u1EDF:"o",\u1EE3:"o",\u1ECD:"o",\u1ED9:"o",\u01EB:"o",\u01ED:"o",\u00F8:"o",\u01FF:"o",\u0254:"o","\uA74B":"o","\uA74D":"o",\u0275:"o",\u01A3:"oi",\u0223:"ou","\uA74F":"oo","\u24DF":"p",\uFF50:"p",\u1E55:"p",\u1E57:"p",\u01A5:"p","\u1D7D":"p","\uA751":"p","\uA753":"p","\uA755":"p","\u24E0":"q",\uFF51:"q","\u024B":"q","\uA757":"q","\uA759":"q","\u24E1":"r",\uFF52:"r",\u0155:"r",\u1E59:"r",\u0159:"r",\u0211:"r",\u0213:"r",\u1E5B:"r",\u1E5D:"r",\u0157:"r",\u1E5F:"r","\u024D":"r",\u027D:"r","\uA75B":"r","\uA7A7":"r","\uA783":"r","\u24E2":"s",\uFF53:"s",\u00DF:"s",\u015B:"s",\u1E65:"s",\u015D:"s",\u1E61:"s",\u0161:"s",\u1E67:"s",\u1E63:"s",\u1E69:"s",\u0219:"s",\u015F:"s","\u023F":"s","\uA7A9":"s","\uA785":"s",\u1E9B:"s","\u24E3":"t",\uFF54:"t",\u1E6B:"t",\u1E97:"t",\u0165:"t",\u1E6D:"t",\u021B:"t",\u0163:"t",\u1E71:"t",\u1E6F:"t",\u0167:"t",\u01AD:"t",\u0288:"t","\u2C66":"t","\uA787":"t","\uA729":"tz","\u24E4":"u",\uFF55:"u",\u00F9:"u",\u00FA:"u",\u00FB:"u",\u0169:"u",\u1E79:"u",\u016B:"u",\u1E7B:"u",\u016D:"u",\u00FC:"u",\u01DC:"u",\u01D8:"u",\u01D6:"u",\u01DA:"u",\u1EE7:"u",\u016F:"u",\u0171:"u",\u01D4:"u",\u0215:"u",\u0217:"u",\u01B0:"u",\u1EEB:"u",\u1EE9:"u",\u1EEF:"u",\u1EED:"u",\u1EF1:"u",\u1EE5:"u",\u1E73:"u",\u0173:"u",\u1E77:"u",\u1E75:"u",\u0289:"u","\u24E5":"v",\uFF56:"v",\u1E7D:"v",\u1E7F:"v",\u028B:"v","\uA75F":"v",\u028C:"v","\uA761":"vy","\u24E6":"w",\uFF57:"w",\u1E81:"w",\u1E83:"w",\u0175:"w",\u1E87:"w",\u1E85:"w",\u1E98:"w",\u1E89:"w","\u2C73":"w","\u24E7":"x",\uFF58:"x",\u1E8B:"x",\u1E8D:"x","\u24E8":"y",\uFF59:"y",\u1EF3:"y",\u00FD:"y",\u0177:"y",\u1EF9:"y",\u0233:"y",\u1E8F:"y",\u00FF:"y",\u1EF7:"y",\u1E99:"y",\u1EF5:"y",\u01B4:"y","\u024F":"y","\u1EFF":"y","\u24E9":"z",\uFF5A:"z",\u017A:"z",\u1E91:"z",\u017C:"z",\u017E:"z",\u1E93:"z",\u1E95:"z",\u01B6:"z",\u0225:"z","\u0240":"z","\u2C6C":"z","\uA763":"z",\u0386:"\u0391",\u0388:"\u0395",\u0389:"\u0397",\u038A:"\u0399",\u03AA:"\u0399",\u038C:"\u039F",\u038E:"\u03A5",\u03AB:"\u03A5",\u038F:"\u03A9",\u03AC:"\u03B1",\u03AD:"\u03B5",\u03AE:"\u03B7",\u03AF:"\u03B9",\u03CA:"\u03B9",\u0390:"\u03B9",\u03CC:"\u03BF",\u03CD:"\u03C5",\u03CB:"\u03C5",\u03B0:"\u03C5",\u03C9:"\u03C9",\u03C2:"\u03C3"};function VC(t){let n=e=>Sme[e]||e;return t.replace(/[^\u0000-\u007E]/g,n)}var wI=class{constructor(n,e){this._ngSelect=n,this._selectionModel=e,this._items=[],this._filteredItems=[],this._markedIndex=-1}get items(){return this._items}get filteredItems(){return this._filteredItems}get markedIndex(){return this._markedIndex}get selectedItems(){return this._selectionModel.value}get markedItem(){return this._filteredItems[this._markedIndex]}get noItemsToSelect(){return this._ngSelect.hideSelected()&&this._items.length===this.selectedItems.length}get maxItemsSelected(){return this._ngSelect.multiple()&&this._ngSelect.maxSelectedItems()<=this.selectedItems.length}get lastSelectedItem(){let n=this.selectedItems.length-1;for(;n>=0;n--){let e=this.selectedItems[n];if(!e.disabled)return e}return null}setItems(n){this._items=n.map((i,r)=>this.mapItem(i,r));let e=this._ngSelect.groupBy();e?(this._groups=this._groupBy(this._items,e),this._items=this._flatten(this._groups)):(this._groups=new Map,this._groups.set(void 0,this._items)),this._filteredItems=[...this._items]}select(n){if(n.selected||this.maxItemsSelected)return;let e=this._ngSelect.multiple();e||this.clearSelected(),this._selectionModel.select(n,e,this._ngSelect.selectableGroupAsModel()),this._ngSelect.hideSelected()&&this._hideSelected(n)}unselect(n){if(!n.selected)return;let e=this._ngSelect.multiple();this._selectionModel.unselect(n,e),this._ngSelect.hideSelected()&&on(n.index)&&e&&this._showSelected(n)}findItem(n){let e;return this._ngSelect.compareWith()?e=i=>this._ngSelect.compareWith()(i.value,n):this._ngSelect.bindValue()?e=i=>!i.children&&this.resolveNested(i.value,this._ngSelect.bindValue())===n:e=i=>i.value===n||!i.children&&i.label&&i.label===this.resolveNested(n,this._ngSelect.bindLabel()),this._items.find(i=>e(i))}addItem(n){let e=this.mapItem(n,this._items.length);return this._items.push(e),this._filteredItems.push(e),e}clearSelected(n=!1){this._selectionModel.clear(n),this._items.forEach(e=>{e.selected=n&&e.selected&&e.disabled,e.marked=!1}),this._ngSelect.hideSelected()&&this.resetFilteredItems()}findByLabel(n){return n=VC(n).toLocaleLowerCase(),this.filteredItems.find(e=>VC(e.label).toLocaleLowerCase().substr(0,n.length)===n)}filter(n){if(!n){this.resetFilteredItems();return}this._filteredItems=[],n=this._ngSelect.searchFn()?n:VC(n).toLocaleLowerCase();let e=this._ngSelect.searchFn()||this._defaultSearchFn,i=this._ngSelect.hideSelected();for(let r of Array.from(this._groups.keys())){let o=[];for(let a of this._groups.get(r)){if(i&&(a.parent&&a.parent.selected||a.selected))continue;let s=this._ngSelect.searchFn()?a.value:a;e(n,s)&&o.push(a)}if(o.length>0){let[a]=o.slice(-1);if(a.parent){let s=this._items.find(l=>l===a.parent);this._filteredItems.push(s)}this._filteredItems.push(...o)}}}resetFilteredItems(){this._filteredItems.length!==this._items.length&&(this._ngSelect.hideSelected()&&this.selectedItems.length>0?this._filteredItems=this._items.filter(n=>!n.selected):this._filteredItems=this._items)}unmarkItem(){this._markedIndex=-1}markNextItem(){this._stepToItem(1)}markPreviousItem(){this._stepToItem(-1)}markItem(n){this._markedIndex=this._filteredItems.indexOf(n)}markSelectedOrDefault(n){if(this._filteredItems.length===0)return;let e=this._getLastMarkedIndex();e>-1?this._markedIndex=e:this._markedIndex=n?this.filteredItems.findIndex(i=>!i.disabled):-1}resolveNested(n,e){if(!Pu(n))return n;if(e.indexOf(".")===-1)return n[e];{let i=e.split("."),r=n;for(let o=0,a=i.length;othis._ngSelect.compareWith()(o.value,e.value));else{let o=i?this.resolveNested(e.value,i):e.value;r=on(o)?this.findItem(o):null}this._selectionModel.unselect(e,n),this._selectionModel.select(r||e,n,this._ngSelect.selectableGroupAsModel())}this._ngSelect.hideSelected()&&(this._filteredItems=this.filteredItems.filter(e=>this.selectedItems.indexOf(e)===-1))}_showSelected(n){if(this._filteredItems.push(n),n.parent){let e=n.parent;this._filteredItems.find(r=>r===e)||this._filteredItems.push(e)}else if(n.children)for(let e of n.children)e.selected=!1,this._filteredItems.push(e);this._filteredItems=[...this._filteredItems.sort((e,i)=>e.index-i.index)]}_hideSelected(n){this._filteredItems=this._filteredItems.filter(e=>e!==n),n.parent?n.parent.children.every(i=>i.selected)&&(this._filteredItems=this._filteredItems.filter(i=>i!==n.parent)):n.children&&(this._filteredItems=this.filteredItems.filter(e=>e.parent!==n))}_defaultSearchFn(n,e){return VC(e.label).toLocaleLowerCase().indexOf(n)>-1}_getNextItemIndex(n){return n>0?this._markedIndex>=this._filteredItems.length-1?0:this._markedIndex+1:this._markedIndex<=0?this._filteredItems.length-1:this._markedIndex-1}_stepToItem(n){this._filteredItems.length===0||this._filteredItems.every(e=>e.disabled)||(this._markedIndex=this._getNextItemIndex(n),this.markedItem.disabled&&this._stepToItem(n))}_getLastMarkedIndex(){if(this._ngSelect.hideSelected()||this._markedIndex>-1&&this.markedItem===void 0)return-1;let n=this._filteredItems.indexOf(this.lastSelectedItem);return this.lastSelectedItem&&n<0?-1:Math.max(this.markedIndex,n)}_groupBy(n,e){let i=new Map;if(n.length===0)return i;if(Array.isArray(n[0].value[e])){for(let a of n){let s=(a.value[e]||[]).map((l,c)=>this.mapItem(l,c));i.set(a,s)}return i}let r=BC(this._ngSelect.groupBy()),o=a=>{let s=r?e(a.value):a.value[e];return on(s)?s:void 0};for(let a of n){let s=o(a),l=i.get(s);l?l.push(a):i.set(s,[a])}return i}_flatten(n){let e=BC(this._ngSelect.groupBy()),i=[];for(let r of Array.from(n.keys())){let o=i.length;if(r===void 0){let p=n.get(void 0)||[];i.push(...p.map(_=>(_.index=o++,_)));continue}let a=Pu(r),s={label:a?"":String(r),children:void 0,parent:null,index:o++,disabled:!this._ngSelect.selectableGroup(),htmlId:kz()},l=e?this._ngSelect.bindLabel():this._ngSelect.groupBy(),c=this._ngSelect.groupValue()||(()=>a?r.value:{[l]:r}),d=n.get(r).map(p=>(p.parent=s,p.children=void 0,p.index=o++,p));s.children=d,s.value=c(r,d.map(p=>p.value)),i.push(s),i.push(...d)}return i}},Tz=(()=>{let n=class n{constructor(){this._dimensions={itemHeight:0,panelHeight:0,itemsPerViewport:0}}get dimensions(){return this._dimensions}calculateItems(i,r,o){let a=this._dimensions,s=a.itemHeight*r,c=Math.max(0,i)/s*r,d=Math.min(r,Math.ceil(c)+(a.itemsPerViewport+1)),_=Math.max(0,d-a.itemsPerViewport),b=Math.min(_,Math.floor(c)),y=a.itemHeight*Math.ceil(b)-a.itemHeight*Math.min(b,o);return y=isNaN(y)?0:y,b=isNaN(b)?-1:b,d=isNaN(d)?-1:d,b-=o,b=Math.max(0,b),d+=o,d=Math.min(r,d),{topPadding:y,scrollHeight:s,start:b,end:d}}setDimensions(i,r){let o=Math.max(1,Math.floor(r/i));this._dimensions={itemHeight:i,panelHeight:r,itemsPerViewport:o}}getScrollTo(i,r,o){let{panelHeight:a}=this.dimensions,s=i+r,l=o,c=l+a;return a>=s&&o===i?null:s>c?l+s-c:i<=l?i:null}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac});let t=n;return t})(),Dz=["top","right","bottom","left"],kme=typeof requestAnimationFrame<"u"?Z_:Y_,CI=(()=>{let n=class n{get currentPosition(){return this._currentPosition}get itemsLength(){return this._itemsLength}set itemsLength(i){i!==this._itemsLength&&(this._itemsLength=i,this._onItemsLengthChanged())}get _startOffset(){if(this.markedItem()){let{itemHeight:i,panelHeight:r}=this._panelService.dimensions,o=this.markedItem().index*i;return r>o?0:o}return 0}constructor(){this._renderer=u(ze),this._zone=u(ae),this._panelService=u(Tz),this._document=u(_e,{optional:!0}),this._destroyRef=u(ln),this._dropdown=u(Y).nativeElement,this.items=re([]),this.showAddTag=re(!1,{transform:B}),this.markedItem=re(void 0),this.position=re("auto"),this.appendTo=re(void 0),this.bufferAmount=re(void 0),this.virtualScroll=re(!1,{transform:B}),this.headerTemplate=re(void 0),this.footerTemplate=re(void 0),this.filterValue=re(null),this.ariaLabelDropdown=re(null),this.outsideClickEvent=re("click"),this.update=Ei(),this.scroll=Ei(),this.scrollToEnd=Ei(),this.outsideClick=Ei(),this.contentElementRef=ir("content",{read:Y}),this.scrollElementRef=ir("scroll",{read:Y}),this.paddingElementRef=ir("padding",{read:Y}),this._virtualPadding=ci(()=>this.paddingElementRef()?.nativeElement),this._scrollablePanel=ci(()=>this.scrollElementRef()?.nativeElement),this._contentPanel=ci(()=>this.contentElementRef()?.nativeElement),this._scrollToEndFired=!1,this._updateScrollHeight=!1,this._lastScrollPosition=0,this._destroyRef.onDestroy(()=>{this.appendTo()&&this._renderer.removeChild(this._dropdown.parentNode,this._dropdown)})}ngOnInit(){this._select=this._dropdown.parentElement,this._handleScroll(),this._handleOutsideClick(),this._appendDropdown(),this._setupMousedownListener()}ngOnChanges(i){if(i.items){let r=i.items;this._onItemsOrShowAddTagChange(r.currentValue,this.showAddTag(),r.firstChange)}if(i.showAddTag){let r=i.showAddTag;this._onItemsOrShowAddTagChange(this.items(),r.currentValue,r.firstChange)}}scrollTo(i,r=!1){if(!i)return;let o=this.items().indexOf(i);if(o<0||o>=this.itemsLength)return;let a;if(this.virtualScroll()){let s=this._panelService.dimensions.itemHeight;a=this._panelService.getScrollTo(o*s,s,this._lastScrollPosition)}else{let s=this._dropdown.querySelector(`#${i.htmlId}`),l=r?s.offsetTop:this._lastScrollPosition;a=this._panelService.getScrollTo(s.offsetTop,s.clientHeight,l)}on(a)&&(this._scrollablePanel().scrollTop=a)}scrollToTag(){let i=this._scrollablePanel();i.scrollTop=i.scrollHeight-i.clientHeight}adjustPosition(){this._updateYPosition()}_handleDropdownPosition(){this._currentPosition=this._calculateCurrentPosition(this._dropdown),Dz.includes(this._currentPosition)?this._updateDropdownClass(this._currentPosition):this._updateDropdownClass("bottom"),this.appendTo()&&this._updateYPosition(),this._dropdown.style.opacity="1"}_updateDropdownClass(i){Dz.forEach(o=>{let a=`ng-select-${o}`;this._renderer.removeClass(this._dropdown,a),this._renderer.removeClass(this._select,a)});let r=`ng-select-${i}`;this._renderer.addClass(this._dropdown,r),this._renderer.addClass(this._select,r)}_handleScroll(){this._zone.runOutsideAngular(()=>{this._scrollablePanel()&&ol(this._scrollablePanel(),"scroll").pipe(Ru(this._destroyRef),Xl(0,kme)).subscribe(i=>{let r=i.target;r&&"scrollTop"in r&&this._onContentScrolled(r.scrollTop)})})}_handleOutsideClick(){this._document&&this._zone.runOutsideAngular(()=>{ol(this._document,this.outsideClickEvent(),{capture:!0}).pipe(Ru(this._destroyRef)).subscribe(i=>this._checkToClose(i))})}_checkToClose(i){if(this._select.contains(i.target)||this._dropdown.contains(i.target))return;let r=i.path||i.composedPath&&i.composedPath();i.target&&i.target.shadowRoot&&r&&r[0]&&this._select.contains(r[0])||this._zone.run(()=>this.outsideClick.emit())}_onItemsOrShowAddTagChange(i=[],r,o){this._scrollToEndFired=!1,this.itemsLength=i.length,r&&i.length&&this.itemsLength++,this.virtualScroll()?this._updateItemsRange(o):(this._setVirtualHeight(),this._updateItems(o))}_updateItems(i){this.update.emit(this.items()),i!==!1&&this._zone.runOutsideAngular(()=>{Promise.resolve().then(()=>{let r=this._scrollablePanel().clientHeight;this._panelService.setDimensions(0,r),this._handleDropdownPosition(),this.scrollTo(this.markedItem(),i)})})}_updateItemsRange(i){this._zone.runOutsideAngular(()=>{this._measureDimensions().then(()=>{i?(this._renderItemsRange(this._startOffset),this._handleDropdownPosition()):this._renderItemsRange()})})}_onContentScrolled(i){this.virtualScroll()&&this._renderItemsRange(i),this._lastScrollPosition=i,this._fireScrollToEnd(i)}_updateVirtualHeight(i){this._updateScrollHeight&&(this._virtualPadding().style.height=`${i}px`,this._updateScrollHeight=!1)}_setVirtualHeight(){this._virtualPadding()&&(this._virtualPadding().style.height="0px")}_onItemsLengthChanged(){this._updateScrollHeight=!0}_renderItemsRange(i=null){if(i&&this._lastScrollPosition===i)return;i=i||this._scrollablePanel().scrollTop;let r=this._panelService.calculateItems(i,this.itemsLength,this.bufferAmount());this._updateVirtualHeight(r.scrollHeight),this._contentPanel().style.transform=`translateY(${r.topPadding}px)`,this._zone.run(()=>{this.update.emit(this.items().slice(r.start,r.end)),this.scroll.emit({start:r.start,end:r.end})}),on(i)&&this._lastScrollPosition===0&&(this._scrollablePanel().scrollTop=i,this._lastScrollPosition=i)}_measureDimensions(){if(this._panelService.dimensions.itemHeight>0||this.itemsLength===0)return Promise.resolve(this._panelService.dimensions);let[i]=this.items();return this.update.emit([i]),Promise.resolve().then(()=>{let o=this._dropdown.querySelector(`#${i.htmlId}`).clientHeight;this._virtualPadding().style.height=`${o*this.itemsLength}px`;let a=this._scrollablePanel().clientHeight;return this._panelService.setDimensions(o,a),this._panelService.dimensions})}_fireScrollToEnd(i){if(this._scrollToEndFired||i===0)return;let r=this.virtualScroll()?this._virtualPadding():this._contentPanel();i+this._dropdown.clientHeight>=r.clientHeight-1&&(this._zone.run(()=>this.scrollToEnd.emit()),this._scrollToEndFired=!0)}_calculateCurrentPosition(i){let r=this.position();if(r!=="auto")return r;let o=this._select.getBoundingClientRect(),a=document.documentElement.scrollTop||document.body.scrollTop,s=o.top+window.pageYOffset,l=o.height,c=i.getBoundingClientRect().height;return s+l+c>a+document.documentElement.clientHeight?"top":"bottom"}_appendDropdown(){if(this.appendTo()){if(this._parent=this._dropdown.shadowRoot?this._dropdown.shadowRoot.querySelector(this.appendTo()):document.querySelector(this.appendTo()),!this._parent)throw new Error(`appendTo selector ${this.appendTo()} did not found any parent element`);this._updateXPosition(),this._parent.appendChild(this._dropdown)}}_updateXPosition(){let i=this._select.getBoundingClientRect(),r=this._parent.getBoundingClientRect(),o=document.documentElement.dir==="rtl",a=i.left-r.left;if(o){let s=r.right-i.right;this._dropdown.style.right=s+"px",this._dropdown.style.left="auto"}else this._dropdown.style.left=a+"px",this._dropdown.style.right="auto";this._dropdown.style.width=i.width+"px",this._dropdown.style.minWidth=i.width+"px"}_updateYPosition(){let i=this._select.getBoundingClientRect(),r=this._parent.getBoundingClientRect(),o=i.height;if(this._currentPosition==="top"){let a=r.bottom-i.bottom;this._dropdown.style.bottom=a+o+"px",this._dropdown.style.top="auto"}else if(this._currentPosition==="bottom"){let a=i.top-r.top;this._dropdown.style.top=a+o+"px",this._dropdown.style.bottom="auto"}}_setupMousedownListener(){this._zone.runOutsideAngular(()=>{ol(this._dropdown,"mousedown").pipe(Ru(this._destroyRef)).subscribe(i=>{i.target.tagName!=="INPUT"&&i.preventDefault()})})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["ng-dropdown-panel"]],viewQuery:function(r,o){r&1&&(zn(o.contentElementRef,Oue,5,Y),zn(o.scrollElementRef,Rue,5,Y),zn(o.paddingElementRef,Pue,5,Y)),r&2&&Ko(3)},inputs:{items:[1,"items"],showAddTag:[1,"showAddTag"],markedItem:[1,"markedItem"],position:[1,"position"],appendTo:[1,"appendTo"],bufferAmount:[1,"bufferAmount"],virtualScroll:[1,"virtualScroll"],headerTemplate:[1,"headerTemplate"],footerTemplate:[1,"footerTemplate"],filterValue:[1,"filterValue"],ariaLabelDropdown:[1,"ariaLabelDropdown"],outsideClickEvent:[1,"outsideClickEvent"]},outputs:{update:"update",scroll:"scroll",scrollToEnd:"scrollToEnd",outsideClick:"outsideClick"},features:[Oe],ngContentSelectors:Ez,decls:9,vars:7,consts:[["scroll",""],["padding",""],["content",""],[1,"ng-dropdown-header"],["role","listbox",1,"ng-dropdown-panel-items","scroll-host"],[1,"ng-dropdown-footer"],[3,"ngTemplateOutlet","ngTemplateOutletContext"]],template:function(r,o){r&1&&(Ee(),L(0,Fue,2,4,"div",3),m(1,"div",4,0),M(3,"div",null,1),m(5,"div",null,2),ne(7),h()(),L(8,Nue,2,4,"div",5)),r&2&&(V(o.headerTemplate()?0:-1),g(),X("aria-label",o.ariaLabelDropdown()),g(2),G("total-padding",o.virtualScroll()),g(2),G("scrollable-content",o.virtualScroll()&&o.items().length),g(3),V(o.footerTemplate()?8:-1))},dependencies:[$n],encapsulation:2,changeDetection:0});let t=n;return t})(),Mz=(()=>{let n=class n{constructor(){this.value=re(),this.disabled=re(!1,{transform:B}),this.elementRef=u(Y),this.label=he(""),vt(()=>{let i=(this.elementRef.nativeElement.innerHTML||"").trim();i!==this.label()&&this.label.set(i)})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["ng-option"]],inputs:{value:[1,"value"],disabled:[1,"disabled"]},ngContentSelectors:Ez,decls:1,vars:0,template:function(r,o){r&1&&(Ee(),ne(0))},encapsulation:2,changeDetection:0});let t=n;return t})(),Xs=(function(t){return t.Tab="Tab",t.Enter="Enter",t.Esc="Escape",t.Space=" ",t.ArrowUp="ArrowUp",t.ArrowDown="ArrowDown",t.Backspace="Backspace",t})(Xs||{});function Iz(){return new DI}var DI=class{constructor(){this._selected=[]}get value(){return this._selected}select(n,e,i){if(n.selected=!0,(!n.children||!e&&i)&&this._selected.push(n),e)if(n.parent){let r=n.parent.children.length,o=n.parent.children.filter(a=>a.selected).length;n.parent.selected=r===o}else n.children&&(this._setChildrenSelectedState(n.children,!0),this._removeChildren(n),i&&this._activeChildren(n)?this._selected=[...this._selected.filter(r=>r.parent!==n),n]:this._selected=[...this._selected,...n.children.filter(r=>!r.disabled)])}unselect(n,e){if(this._selected=this._selected.filter(i=>i!==n),n.selected=!1,e)if(n.parent&&n.parent.selected){let i=n.parent.children;this._removeParent(n.parent),this._removeChildren(n.parent),this._selected.push(...i.filter(r=>r!==n&&!r.disabled)),n.parent.selected=!1}else n.children&&(this._setChildrenSelectedState(n.children,!1),this._removeChildren(n))}clear(n){this._selected=n?this._selected.filter(e=>e.disabled):[]}_setChildrenSelectedState(n,e){for(let i of n)i.disabled||(i.selected=e)}_removeChildren(n){this._selected=[...this._selected.filter(e=>e.parent!==n),...n.children.filter(e=>e.parent===n&&e.disabled&&e.selected)]}_removeParent(n){this._selected=this._selected.filter(e=>e!==n)}_activeChildren(n){return n.children.every(e=>!e.disabled||e.selected)}},Az=new O("ng-select-selection-model"),Oz=(()=>{let n=class n{constructor(){this.classes=u(new Li("class"),{optional:!0}),this.config=u(Mme),this._cd=u(ye),this._console=u(Eme),this._destroyRef=u(ln),this._disabled=he(!1),this.ariaLabelDropdown=re("Options List"),this.ariaLabel=re(void 0),this.markFirst=re(!0,{transform:B}),this.placeholder=re(this.config.placeholder),this.fixedPlaceholder=re(!0),this.notFoundText=re(void 0),this.typeToSearchText=re(void 0),this.preventToggleOnRightClick=re(!1),this.addTagText=re(void 0),this.loadingText=re(void 0),this.clearAllText=re(void 0),this.dropdownPosition=re("auto"),this.appendTo=re(void 0),this.outsideClickEvent=re(this.config.outsideClickEvent),this.loading=re(!1,{transform:B}),this.closeOnSelect=re(!0,{transform:B}),this.hideSelected=re(!1,{transform:B}),this.selectOnTab=re(!1,{transform:B}),this.openOnEnter=re(void 0,{transform:B}),this.maxSelectedItems=re(void 0,{transform:ht}),this.groupBy=re(void 0),this.groupValue=re(void 0),this.bufferAmount=re(4,{transform:ht}),this.virtualScroll=re(void 0,{transform:B}),this.selectableGroup=re(!1,{transform:B}),this.tabFocusOnClearButton=re(),this.selectableGroupAsModel=re(!0,{transform:B}),this.searchFn=re(null),this.trackByFn=re(null),this.clearOnBackspace=re(!0,{transform:B}),this.labelForId=re(null),this.inputAttrs=re({}),this.tabIndex=re(void 0,{transform:ht}),this.readonly=re(!1,{transform:B}),this.searchWhileComposing=re(!0,{transform:B}),this.minTermLength=re(0,{transform:ht}),this.editableSearchTerm=re(!1,{transform:B}),this.ngClass=re(null),this.typeahead=re(void 0),this.multiple=re(!1,{transform:B}),this.addTag=re(!1),this.searchable=re(!0,{transform:B}),this.clearable=re(!0,{transform:B}),this.deselectOnClick=re(),this.clearSearchOnAdd=re(void 0),this.compareWith=re(void 0,{transform:a=>{if(a!=null&&!BC(a))throw Error("`compareWith` must be a function.");return a}}),this.keyDownFn=re(a=>!0),this.bindLabel=Im(void 0),this.bindValue=Im(void 0),this.appearance=Im(void 0),this.isOpen=Im(!1),this.items=Im([]),this.blurEvent=Ei({alias:"blur"}),this.focusEvent=Ei({alias:"focus"}),this.changeEvent=Ei({alias:"change"}),this.openEvent=Ei({alias:"open"}),this.closeEvent=Ei({alias:"close"}),this.searchEvent=Ei({alias:"search"}),this.clearEvent=Ei({alias:"clear"}),this.addEvent=Ei({alias:"add"}),this.removeEvent=Ei({alias:"remove"}),this.scroll=Ei({alias:"scroll"}),this.scrollToEnd=Ei({alias:"scrollToEnd"}),this.disabled=ci(()=>this.readonly()||this._disabled()),this.clearSearchOnAddValue=ci(()=>on(this.clearSearchOnAdd())?this.clearSearchOnAdd():on(this.config.clearSearchOnAdd)?this.config.clearSearchOnAdd:this.closeOnSelect()),this.deselectOnClickValue=ci(()=>on(this.deselectOnClick())?this.deselectOnClick():on(this.config.deselectOnClick)?this.config.deselectOnClick:this.multiple()),this.optionTemplate=Mr(jC,{read:te}),this.optgroupTemplate=Mr(HC,{read:te}),this.labelTemplate=Mr(zC,{read:te}),this.multiLabelTemplate=Mr(UC,{read:te}),this.headerTemplate=Mr($C,{read:te}),this.footerTemplate=Mr(WC,{read:te}),this.notFoundTemplate=Mr(GC,{read:te}),this.placeholderTemplate=Mr(qC,{read:te}),this.typeToSearchTemplate=Mr(YC,{read:te}),this.loadingTextTemplate=Mr(QC,{read:te}),this.tagTemplate=Mr(KC,{read:te}),this.loadingSpinnerTemplate=Mr(ZC,{read:te}),this.clearButtonTemplate=Mr(XC,{read:te}),this.ngOptions=u2(Mz,{descendants:!0}),this.dropdownPanel=ir(li(()=>CI)),this.searchInput=ir("searchInput"),this.clearButton=ir("clearButton"),this.dropdownId=kz(),this.escapeHTML=!0,this.viewPortItems=[],this.tabFocusOnClear=he(!0),this.autoFocus=u(new Li("autofocus"),{optional:!0}),this._defaultLabel="label",this._editableSearchTerm=ci(()=>this.editableSearchTerm()&&!this.multiple()),this._injector=u(de),this._isComposing=!1,this._keyPress$=new z,this._pressedKeys=[],this._searchTerm=he(null),this._validTerm=ci(()=>{let a=this._searchTerm()?.trim();return a&&a.length>=this.minTermLength()}),this.clearItem=a=>{let s=this.selectedItems.find(l=>l.value===a);this.unselect(s)},this.trackByOption=(a,s)=>this.trackByFn()?this.trackByFn()(s.value):s,this._onChange=a=>{},this._onTouched=()=>{};let i=this.config,r=u(Az,{optional:!0}),o=u(Y);this._mergeGlobalConfig(i),this.itemsList=new wI(this,r?r():Iz()),this.element=o.nativeElement}get filtered(){return!!this.searchTerm&&this.searchable()||this._isComposing}get focused(){return this._focused}get searchTerm(){return this._searchTerm()}get selectedItems(){return this.itemsList.selectedItems}get selectedValues(){return this.selectedItems.map(i=>i.value)}get hasValue(){return this.selectedItems.length>0}get currentPanelPosition(){if(this.dropdownPanel())return this.dropdownPanel().currentPosition}get showAddTag(){if(!this._validTerm())return!1;let i=this.searchTerm.toLowerCase().trim();return this.addTag()&&!this.itemsList.filteredItems.some(r=>r.label.toLowerCase()===i)&&(!this.hideSelected()&&this.isOpen()||!this.selectedItems.some(r=>r.label.toLowerCase()===i))&&!this.loading()}ngOnInit(){this._handleKeyPresses(),this._setInputAttributes()}ngOnChanges(i){i.multiple&&this.itemsList.clearSelected(),i.items&&(this._itemsAreUsed=!0,this._setItems(i.items.currentValue||[])),i.isOpen&&(this._manualOpen=on(i.isOpen.currentValue)),i.groupBy&&(i.items||this._setItems([...this.items()])),i.inputAttrs&&this._setInputAttributes(),this._setTabFocusOnClear()}ngAfterViewInit(){this._itemsAreUsed||(this.escapeHTML=!1,this._setItemsFromNgOptions()),on(this.autoFocus)&&this.focus()}handleKeyDown(i){let r=i.key;if(Object.values(Xs).includes(r)){if(this.keyDownFn()(i)===!1)return;this.handleKeyCode(i)}else r&&r.length===1&&this._keyPress$.next(r.toLocaleLowerCase())}handleKeyCode(i){let r=i.target;this.clearButton()&&this.clearButton().nativeElement===r?this.handleKeyCodeClear(i):this.handleKeyCodeInput(i)}handleKeyCodeInput(i){switch(i.key){case Xs.ArrowDown:this._handleArrowDown(i);break;case Xs.ArrowUp:this._handleArrowUp(i);break;case Xs.Space:this._handleSpace(i);break;case Xs.Enter:this._handleEnter(i);break;case Xs.Tab:this._handleTab(i);break;case Xs.Esc:this.close(),i.preventDefault();break;case Xs.Backspace:this._handleBackspace();break}}handleKeyCodeClear(i){switch(i.key){case Xs.Enter:this.handleClearClick(),i.preventDefault();break}}handleMousedown(i){if(this.disabled())return;if(this.preventToggleOnRightClick()&&i.button===2)return!1;let r=i.target;if(r.tagName!=="INPUT"&&i.preventDefault(),!r.classList.contains("ng-clear-wrapper")){if(r.classList.contains("ng-arrow-wrapper")){this.handleArrowClick();return}r.classList.contains("ng-value-icon")||(this._focused||this.focus(),this.searchable()?this.open():this.toggle())}}handleArrowClick(){this.isOpen()?this.close():this.open()}handleClearClick(i){this.hasValue&&(this.itemsList.clearSelected(!0),this._updateNgModel()),this._clearSearch(),this.focus(),this.clearEvent.emit(),this._onSelectionChanged()}clearModel(){this.clearable()&&(this.itemsList.clearSelected(),this._updateNgModel())}writeValue(i){this.itemsList.clearSelected(),this._handleWriteValue(i),this._editableSearchTerm()&&this._setSearchTermFromItems(),this._cd.markForCheck()}registerOnChange(i){this._onChange=i}registerOnTouched(i){this._onTouched=i}setDisabledState(i){this._disabled.set(i),this._cd.markForCheck()}toggle(){this.isOpen()?this.close():this.open()}open(){this.disabled()||this.isOpen()||this._manualOpen||!this.typeahead()?.observed&&!this.addTag()&&this.itemsList.noItemsToSelect||(this.isOpen.set(!0),this.itemsList.markSelectedOrDefault(this.markFirst()),this.openEvent.emit(),this.searchTerm||this.focus(),this.detectChanges())}close(){!this.isOpen()||this._manualOpen||(this.isOpen.set(!1),this._isComposing=!1,this._editableSearchTerm()?this.itemsList.resetFilteredItems():this._clearSearch(),this.itemsList.unmarkItem(),this._onTouched(),this.closeEvent.emit(),this._cd.markForCheck())}toggleItem(i){!i||i.disabled||this.disabled()||(this.deselectOnClickValue()&&i.selected?this.unselect(i):this.select(i),this._editableSearchTerm()&&this._setSearchTermFromItems())}select(i){i.selected||(this.itemsList.select(i),this.clearSearchOnAddValue()&&!this._editableSearchTerm()&&this._clearSearch(),this._updateNgModel(),this.multiple()&&this.addEvent.emit(i.value)),(this.closeOnSelect()||this.itemsList.noItemsToSelect)&&this.close(),this._onSelectionChanged()}focus(){this.searchInput().nativeElement.focus()}blur(){this.searchInput().nativeElement.blur()}unselect(i){i&&(this.itemsList.unselect(i),this.focus(),this._updateNgModel(),this.removeEvent.emit(i.value),this._onSelectionChanged())}selectTag(){let i;BC(this.addTag())?i=this.addTag()(this.searchTerm):i=this._primitive?this.searchTerm:{[this.bindLabel()]:this.searchTerm};let r=o=>this.typeahead()?.observed||!this.isOpen()?this.itemsList.mapItem(o,null):this.itemsList.addItem(o);wme(i)?i.then(o=>this.select(r(o))).catch(()=>{}):i&&this.select(r(i))}showClear(){return this.clearable()&&(this.hasValue||this.searchTerm)&&!this.disabled()}focusOnClear(){this.blur(),this.clearButton()&&this.clearButton().nativeElement.focus()}showNoItemsFound(){let i=this.itemsList.filteredItems.length===0;return(i&&!this.typeahead()?.observed&&!this.loading()||i&&this.typeahead()?.observed&&this._validTerm()&&!this.loading())&&!this.showAddTag}showTypeToSearch(){return this.itemsList.filteredItems.length===0&&this.typeahead()?.observed&&!this._validTerm()&&!this.loading()}onCompositionStart(){this._isComposing=!0}onCompositionEnd(i){this._isComposing=!1,!this.searchWhileComposing()&&this.filter(i)}filter(i){this._isComposing&&!this.searchWhileComposing()||(this._searchTerm.set(i),this.typeahead()?.observed&&(this._validTerm()||this.minTermLength()===0)&&this.typeahead().next(i),this.typeahead()?.observed||(this.itemsList.filter(i),this.isOpen()&&this.itemsList.markSelectedOrDefault(this.markFirst())),this.searchEvent.emit({term:i,items:this.itemsList.filteredItems.map(r=>r.value)}),this.open())}onInputFocus(i){this._focused||(this._editableSearchTerm()&&this._setSearchTermFromItems(),this.element.classList.add("ng-select-focused"),this.focusEvent.emit(i),this._focused=!0)}onInputBlur(i){this.element.classList.remove("ng-select-focused"),this.blurEvent.emit(i),!this.isOpen()&&!this.disabled()&&this._onTouched(),this._editableSearchTerm()&&this._setSearchTermFromItems(),this._focused=!1}onItemHover(i){i.disabled||this.itemsList.markItem(i)}detectChanges(){this._cd.destroyed||this._cd.detectChanges()}_setSearchTermFromItems(){let i=this.selectedItems?.[0];this._searchTerm.set(i?.label??null)}_setItems(i){let r=i[0];this.bindLabel.set(this.bindLabel()||this._defaultLabel),this._primitive=on(r)?!Pu(r):this._primitive||this.bindLabel()===this._defaultLabel,this.itemsList.setItems(i),i.length>0&&this.hasValue&&this.itemsList.mapSelectedItems(),this.isOpen()&&on(this.searchTerm)&&!this.typeahead()?.observed&&this.itemsList.filter(this.searchTerm),(this.typeahead()?.observed||this.isOpen())&&this.itemsList.markSelectedOrDefault(this.markFirst())}_setItemsFromNgOptions(){zr(()=>{let i=this.ngOptions();this.bindLabel.set(this._defaultLabel);let r=i.map(o=>({$ngOptionValue:o.value(),$ngOptionLabel:o.elementRef.nativeElement.innerHTML,disabled:o.disabled()}))??[];this.items.set(r),this.itemsList.setItems(r),this.hasValue&&this.itemsList.mapSelectedItems(),this._cd.detectChanges(),i.map(o=>({option:o,item:this.itemsList.findItem(o.value())})).filter(({item:o})=>on(o)).forEach(({option:o,item:a})=>{a.disabled=o.disabled(),a.label=o.label()||a.label})},{injector:this._injector})}_isValidWriteValue(i){if(!on(i)||this.multiple()&&i===""||Array.isArray(i)&&i.length===0)return!1;let r=o=>!on(this.compareWith())&&Pu(o)&&this.bindValue()?(this._console.warn(`Setting object(${JSON.stringify(o)}) as your model with bindValue is not allowed unless [compareWith] is used.`),!1):!0;return this.multiple()?Array.isArray(i)?i.every(o=>r(o)):(this._console.warn("Multiple select ngModel should be array."),!1):r(i)}_handleWriteValue(i){if(!this._isValidWriteValue(i))return;let r=o=>{let a=this.itemsList.findItem(o);if(a)this.itemsList.select(a);else{let s=Pu(o),l=!s&&!this.bindValue();s||l?this.itemsList.select(this.itemsList.mapItem(o,null)):this.bindValue()&&(a={[this.bindLabel()]:null,[this.bindValue()]:o},this.itemsList.select(this.itemsList.mapItem(a,null)))}};this.multiple()?i.forEach(o=>r(o)):r(i)}_handleKeyPresses(){this.searchable()||this._keyPress$.pipe(Ru(this._destroyRef),He(i=>this._pressedKeys.push(i)),Dt(200),ce(()=>this._pressedKeys.length>0),se(()=>this._pressedKeys.join(""))).subscribe(i=>{let r=this.itemsList.findByLabel(i);r&&(this.isOpen()?(this.itemsList.markItem(r),this._scrollToMarked(),this._cd.markForCheck()):this.select(r)),this._pressedKeys=[]})}_setInputAttributes(){let i=this.searchInput().nativeElement,r=I({type:"text",autocorrect:"off",autocapitalize:"off",autocomplete:"off","aria-controls":this.dropdownId},this.inputAttrs());for(let o of Object.keys(r))i.setAttribute(o,r[o])}_setTabFocusOnClear(){this.tabFocusOnClear.set(on(this.tabFocusOnClearButton())?!!this.tabFocusOnClearButton():this.config.tabFocusOnClear)}_updateNgModel(){let i=[];for(let o of this.selectedItems)if(this.bindValue()){let a=null;if(o.children){let s=this.groupValue()?this.bindValue():this.groupBy();a=o.value[s||this.groupBy()]}else a=this.itemsList.resolveNested(o.value,this.bindValue());i.push(a)}else i.push(o.value);let r=this.selectedItems.map(o=>o.value);this.multiple()?(this._onChange(i),this.changeEvent.emit(r)):(this._onChange(on(i[0])?i[0]:null),this.changeEvent.emit(r[0])),this._cd.markForCheck()}_clearSearch(){this.searchTerm&&(this._changeSearch(null),this.itemsList.resetFilteredItems())}_changeSearch(i){this._searchTerm.set(i),this.typeahead()?.observed&&this.typeahead().next(i)}_scrollToMarked(){!this.isOpen()||!this.dropdownPanel()||this.dropdownPanel().scrollTo(this.itemsList.markedItem)}_scrollToTag(){!this.isOpen()||!this.dropdownPanel()||this.dropdownPanel().scrollToTag()}_onSelectionChanged(){let i=this.appendTo()??this.config.appendTo;this.isOpen()&&this.deselectOnClickValue()&&i&&(this._cd.detectChanges(),this.dropdownPanel().adjustPosition())}_handleTab(i){if(this.isOpen()===!1){if(this.showClear()&&!i.shiftKey&&this.tabFocusOnClear())this.focusOnClear(),i.preventDefault();else if(!this.addTag())return}this.selectOnTab()?this.itemsList.markedItem?(this.toggleItem(this.itemsList.markedItem),i.preventDefault()):this.showAddTag?(this.selectTag(),i.preventDefault()):this.close():this.close()}_handleEnter(i){let r=this.openOnEnter()??this.config.openOnEnter;if(this.isOpen()||this._manualOpen)this.itemsList.markedItem?this.toggleItem(this.itemsList.markedItem):this.showAddTag&&this.selectTag();else if(r)this.open();else return;i.preventDefault()}_handleSpace(i){this.isOpen()||this._manualOpen||(this.open(),i.preventDefault())}_handleArrowDown(i){this._nextItemIsTag(1)?(this.itemsList.unmarkItem(),this._scrollToTag()):(this.itemsList.markNextItem(),this._scrollToMarked()),this.open(),i.preventDefault()}_handleArrowUp(i){this.isOpen()&&(this._nextItemIsTag(-1)?(this.itemsList.unmarkItem(),this._scrollToTag()):(this.itemsList.markPreviousItem(),this._scrollToMarked()),i.preventDefault())}_nextItemIsTag(i){let r=this.itemsList.markedIndex+i;return this.addTag()&&this.searchTerm&&this.itemsList.markedItem&&(r<0||r===this.itemsList.filteredItems.length)}_handleBackspace(){this.searchTerm||!this.clearable()||!this.clearOnBackspace()||!this.hasValue||(this.multiple()?this.unselect(this.itemsList.lastSelectedItem):this.clearModel())}_mergeGlobalConfig(i){this.bindValue.set(this.bindValue()||i.bindValue),this.bindLabel.set(this.bindLabel()||i.bindLabel),this.appearance.set(this.appearance()||i.appearance),this._setTabFocusOnClear()}getVirtualScroll(i){return on(this.virtualScroll)?this.virtualScroll():this.isVirtualScrollDisabled(i)}isVirtualScrollDisabled(i){return on(i.disableVirtualScroll)?!i.disableVirtualScroll:!1}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["ng-select"]],contentQueries:function(r,o,a){r&1&&(Hn(a,o.optionTemplate,jC,5,te),Hn(a,o.optgroupTemplate,HC,5,te),Hn(a,o.labelTemplate,zC,5,te),Hn(a,o.multiLabelTemplate,UC,5,te),Hn(a,o.headerTemplate,$C,5,te),Hn(a,o.footerTemplate,WC,5,te),Hn(a,o.notFoundTemplate,GC,5,te),Hn(a,o.placeholderTemplate,qC,5,te),Hn(a,o.typeToSearchTemplate,YC,5,te),Hn(a,o.loadingTextTemplate,QC,5,te),Hn(a,o.tagTemplate,KC,5,te),Hn(a,o.loadingSpinnerTemplate,ZC,5,te),Hn(a,o.clearButtonTemplate,XC,5,te),Hn(a,o.ngOptions,Mz,5)),r&2&&Ko(14)},viewQuery:function(r,o){r&1&&(zn(o.dropdownPanel,CI,5),zn(o.searchInput,Lue,5),zn(o.clearButton,Vue,5)),r&2&&Ko(3)},hostVars:20,hostBindings:function(r,o){r&1&&S("keydown",function(s){return o.handleKeyDown(s)}),r&2&&G("ng-select",!0)("ng-select-single",!o.multiple())("ng-select-typeahead",o.typeahead())("ng-select-multiple",o.multiple())("ng-select-taggable",o.addTag())("ng-select-searchable",o.searchable())("ng-select-clearable",o.clearable())("ng-select-opened",o.isOpen())("ng-select-filtered",o.filtered)("ng-select-disabled",o.disabled())},inputs:{ariaLabelDropdown:[1,"ariaLabelDropdown"],ariaLabel:[1,"ariaLabel"],markFirst:[1,"markFirst"],placeholder:[1,"placeholder"],fixedPlaceholder:[1,"fixedPlaceholder"],notFoundText:[1,"notFoundText"],typeToSearchText:[1,"typeToSearchText"],preventToggleOnRightClick:[1,"preventToggleOnRightClick"],addTagText:[1,"addTagText"],loadingText:[1,"loadingText"],clearAllText:[1,"clearAllText"],dropdownPosition:[1,"dropdownPosition"],appendTo:[1,"appendTo"],outsideClickEvent:[1,"outsideClickEvent"],loading:[1,"loading"],closeOnSelect:[1,"closeOnSelect"],hideSelected:[1,"hideSelected"],selectOnTab:[1,"selectOnTab"],openOnEnter:[1,"openOnEnter"],maxSelectedItems:[1,"maxSelectedItems"],groupBy:[1,"groupBy"],groupValue:[1,"groupValue"],bufferAmount:[1,"bufferAmount"],virtualScroll:[1,"virtualScroll"],selectableGroup:[1,"selectableGroup"],tabFocusOnClearButton:[1,"tabFocusOnClearButton"],selectableGroupAsModel:[1,"selectableGroupAsModel"],searchFn:[1,"searchFn"],trackByFn:[1,"trackByFn"],clearOnBackspace:[1,"clearOnBackspace"],labelForId:[1,"labelForId"],inputAttrs:[1,"inputAttrs"],tabIndex:[1,"tabIndex"],readonly:[1,"readonly"],searchWhileComposing:[1,"searchWhileComposing"],minTermLength:[1,"minTermLength"],editableSearchTerm:[1,"editableSearchTerm"],ngClass:[1,"ngClass"],typeahead:[1,"typeahead"],multiple:[1,"multiple"],addTag:[1,"addTag"],searchable:[1,"searchable"],clearable:[1,"clearable"],deselectOnClick:[1,"deselectOnClick"],clearSearchOnAdd:[1,"clearSearchOnAdd"],compareWith:[1,"compareWith"],keyDownFn:[1,"keyDownFn"],bindLabel:[1,"bindLabel"],bindValue:[1,"bindValue"],appearance:[1,"appearance"],isOpen:[1,"isOpen"],items:[1,"items"]},outputs:{bindLabel:"bindLabelChange",bindValue:"bindValueChange",appearance:"appearanceChange",isOpen:"isOpenChange",items:"itemsChange",blurEvent:"blur",focusEvent:"focus",changeEvent:"change",openEvent:"open",closeEvent:"close",searchEvent:"search",clearEvent:"clear",addEvent:"add",removeEvent:"remove",scroll:"scroll",scrollToEnd:"scrollToEnd"},exportAs:["ngSelect"],features:[we([{provide:dr,useExisting:li(()=>n),multi:!0},Tz]),Oe],decls:15,vars:20,consts:[["searchInput",""],["defaultPlaceholderTemplate",""],["defaultLabelTemplate",""],["defaultLoadingSpinnerTemplate",""],["clearButton",""],["defaultOptionTemplate",""],["defaultTagTemplate",""],["defaultNotFoundTemplate",""],["defaultTypeToSearchTemplate",""],["defaultLoadingTextTemplate",""],[1,"ng-select-container",3,"mousedown"],[1,"ng-value-container"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"ng-input"],["aria-autocomplete","list","role","combobox",3,"blur","change","compositionend","compositionstart","focus","input","disabled","readOnly","value"],[1,"ng-arrow-wrapper"],[1,"ng-arrow"],[1,"ng-dropdown-panel",3,"virtualScroll","bufferAmount","appendTo","position","outsideClickEvent","headerTemplate","footerTemplate","filterValue","items","showAddTag","markedItem","ng-select-multiple","class","id","ariaLabelDropdown"],["aria-atomic","true","aria-live","polite","role","status",1,"ng-visually-hidden"],[3,"ngTemplateOutlet"],[1,"ng-placeholder"],[1,"ng-value",3,"ng-value-disabled"],[1,"ng-value"],["aria-hidden","true",1,"ng-value-icon","left",3,"click"],[1,"ng-value-label",3,"ngItemLabel","escape"],[1,"ng-spinner-loader"],["role","button","tabindex","0",1,"ng-clear-wrapper",3,"title"],["role","button","tabindex","0",1,"ng-clear-wrapper",3,"click","title"],["aria-hidden","true",1,"ng-clear"],[1,"ng-dropdown-panel",3,"update","scroll","scrollToEnd","outsideClick","virtualScroll","bufferAmount","appendTo","position","outsideClickEvent","headerTemplate","footerTemplate","filterValue","items","showAddTag","markedItem","id","ariaLabelDropdown"],[1,"ng-option",3,"ng-option-disabled","ng-option-selected","ng-optgroup","ng-option","ng-option-child","ng-option-marked"],["role","option",1,"ng-option",3,"ng-option-marked"],[1,"ng-option",3,"click","mouseover"],[1,"ng-option-label",3,"ngItemLabel","escape"],["role","option",1,"ng-option",3,"mouseover","click"],[1,"ng-tag-label"],[1,"ng-option","ng-option-disabled"]],template:function(r,o){if(r&1){let a=q();m(0,"div",10),S("mousedown",function(l){return k(a),T(o.handleMousedown(l))}),m(1,"div",11),L(2,$ue,3,1),L(3,Yue,2,0),L(4,Kue,1,5,null,12),m(5,"div",13)(6,"input",14,0),S("blur",function(l){return k(a),T(o.onInputBlur(l))})("change",function(l){return k(a),T(l.stopPropagation())})("compositionend",function(){k(a);let l=Te(7);return T(o.onCompositionEnd(l.value))})("compositionstart",function(){return k(a),T(o.onCompositionStart())})("focus",function(l){return k(a),T(o.onInputFocus(l))})("input",function(){k(a);let l=Te(7);return T(o.filter(l.value))}),h()()(),L(8,Jue,3,1),L(9,ime,2,1),m(10,"span",15),M(11,"span",16),h()(),L(12,bme,8,21,"ng-dropdown-panel",17),m(13,"div",18),L(14,vme,1,1),h()}r&2&&(G("ng-appearance-outline",o.appearance()==="outline")("ng-has-value",o.hasValue),g(2),V(o.selectedItems.length===0&&!o.searchTerm||(o.fixedPlaceholder()??o.config.fixedPlaceholder)?2:-1),g(),V((!o.multiLabelTemplate()||!o.multiple())&&o.selectedItems.length>0?3:-1),g(),V(o.multiple()&&o.multiLabelTemplate()&&o.selectedValues.length>0?4:-1),g(2),v("disabled",o.disabled())("readOnly",!o.searchable()||o.itemsList.maxItemsSelected)("value",o.searchTerm??""),X("aria-activedescendant",o.isOpen()?o.itemsList==null||o.itemsList.markedItem==null?null:o.itemsList.markedItem.htmlId:null)("aria-controls",o.isOpen()?o.dropdownId:null)("aria-expanded",o.isOpen())("aria-label",o.ariaLabel())("id",o.labelForId())("tabindex",o.tabIndex()),g(2),V(o.loading()?8:-1),g(),V(o.showClear()?9:-1),g(3),V(o.isOpen()?12:-1),g(2),V(o.isOpen()&&o.showNoItemsFound()?14:-1))},dependencies:[$n,Dme,CI],styles:[`@charset "UTF-8";.ng-select{position:relative;display:block;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.ng-select div,.ng-select input,.ng-select span{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.ng-select [hidden]{display:none}.ng-select.ng-select-searchable .ng-select-container .ng-value-container .ng-input{opacity:1}.ng-select.ng-select-opened .ng-select-container{z-index:1001}.ng-select.ng-select-disabled .ng-select-container .ng-value-container .ng-placeholder,.ng-select.ng-select-disabled .ng-select-container .ng-value-container .ng-value{-webkit-user-select:none;user-select:none;cursor:default}.ng-select.ng-select-disabled .ng-arrow-wrapper{cursor:default}.ng-select.ng-select-filtered .ng-placeholder{display:none}.ng-select .ng-select-container{cursor:default;display:flex;outline:none;overflow:hidden;position:relative;width:100%}.ng-select .ng-select-container .ng-value-container{display:flex;flex:1}.ng-select .ng-select-container .ng-value-container .ng-input{opacity:0}.ng-select .ng-select-container .ng-value-container .ng-input>input{box-sizing:content-box;background:none transparent;border:0 none;box-shadow:none;outline:none;padding:0;cursor:default;width:100%}.ng-select .ng-select-container .ng-value-container .ng-input>input::-ms-clear{display:none}.ng-select .ng-select-container .ng-value-container .ng-input>input[readonly]{-webkit-user-select:unset;user-select:unset;width:0;padding:0}.ng-select.ng-select-single.ng-select-filtered .ng-select-container .ng-value-container .ng-value{visibility:hidden}.ng-select.ng-select-single .ng-select-container .ng-value-container,.ng-select.ng-select-single .ng-select-container .ng-value-container .ng-value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ng-select.ng-select-single .ng-select-container .ng-value-container .ng-value .ng-value-icon{display:none}.ng-select.ng-select-single .ng-select-container .ng-value-container .ng-input{position:absolute;left:0;width:100%}.ng-select.ng-select-multiple.ng-select-disabled>.ng-select-container .ng-value-container .ng-value .ng-value-icon{display:none}.ng-select.ng-select-multiple .ng-select-container .ng-value-container{flex-wrap:wrap}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-placeholder{position:absolute}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value{white-space:nowrap}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value.ng-value-disabled .ng-value-icon{display:none}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value .ng-value-icon{cursor:pointer}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-input{flex:1;z-index:2}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-placeholder{z-index:1}.ng-select .ng-clear-wrapper{cursor:pointer;position:relative;width:17px;-webkit-user-select:none;user-select:none}.ng-select .ng-clear-wrapper .ng-clear{display:inline-block;font-size:18px;line-height:1;pointer-events:none}.ng-select .ng-spinner-loader{border-radius:50%;width:17px;height:17px;margin-right:5px;font-size:10px;position:relative;text-indent:-9999em;border-top:2px solid rgba(66,66,66,.2);border-right:2px solid rgba(66,66,66,.2);border-bottom:2px solid rgba(66,66,66,.2);border-left:2px solid #424242;transform:translateZ(0);animation:load8 .8s infinite linear}.ng-select .ng-spinner-loader:after{border-radius:50%;width:17px;height:17px}@-webkit-keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.ng-select .ng-arrow-wrapper{cursor:pointer;position:relative;text-align:center;-webkit-user-select:none;user-select:none}.ng-select .ng-arrow-wrapper .ng-arrow{pointer-events:none;display:inline-block;height:0;width:0;position:relative}.ng-dropdown-panel{box-sizing:border-box;position:absolute;opacity:0;width:100%;z-index:1050;-webkit-overflow-scrolling:touch}.ng-dropdown-panel .ng-dropdown-panel-items{display:block;height:auto;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;max-height:240px;overflow-y:auto}.ng-dropdown-panel .ng-dropdown-panel-items .ng-optgroup{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option{box-sizing:border-box;cursor:pointer;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option .ng-option-label:empty:before{content:"\\200b"}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option .highlighted{font-weight:700;text-decoration:underline}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.disabled{cursor:default}.ng-dropdown-panel .scroll-host{overflow:hidden;overflow-y:auto;position:relative;display:block;-webkit-overflow-scrolling:touch}.ng-dropdown-panel .scrollable-content{top:0;left:0;width:100%;height:100%;position:absolute}.ng-dropdown-panel .total-padding{width:1px;opacity:0}.ng-visually-hidden{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);border:0;white-space:nowrap} +`],encapsulation:2,changeDetection:0});let t=n;return t})(),MI=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275mod=ee({type:n}),n.\u0275inj=J({providers:Tme()});let t=n;return t})();function Tme(){return[{provide:Az,useValue:Iz}]}var Ame=["*"],Ome=["ngSelect"],Rme=()=>({standalone:!0}),Pz=(t,n,e,i)=>({item:t,item$:n,index:e,searchTerm:i}),Pme=(t,n,e)=>({item:t,clear:n,label:e}),Fme=(t,n)=>({items:t,clear:n}),EI=t=>({searchTerm:t});function Nme(t,n){}function Lme(t,n){if(t&1&&A(0,Nme,0,0,"ng-template",15),t&2){let e=n.item,i=n.item$,r=n.index,o=n.searchTerm,a=x(2);v("ngTemplateOutlet",a.optionTemplate)("ngTemplateOutletContext",km(2,Pz,e,i,r,o))}}function Vme(t,n){t&1&&A(0,Lme,1,7,"ng-template",2)}function Bme(t,n){}function jme(t,n){if(t&1&&A(0,Bme,0,0,"ng-template",15),t&2){let e=n.item,i=n.item$,r=n.index,o=n.searchTerm,a=x(2);v("ngTemplateOutlet",a.optgroupTemplate)("ngTemplateOutletContext",km(2,Pz,e,i,r,o))}}function Hme(t,n){t&1&&A(0,jme,1,7,"ng-template",3)}function zme(t,n){}function Ume(t,n){if(t&1&&A(0,zme,0,0,"ng-template",15),t&2){let e=n.item,i=n.clear,r=n.label,o=x(2);v("ngTemplateOutlet",o.labelTemplate)("ngTemplateOutletContext",Hd(2,Pme,e,i,r))}}function $me(t,n){t&1&&A(0,Ume,1,6,"ng-template",4)}function Wme(t,n){}function Gme(t,n){if(t&1&&A(0,Wme,0,0,"ng-template",15),t&2){let e=n.items,i=n.clear,r=x(2);v("ngTemplateOutlet",r.multiLabelTemplate)("ngTemplateOutletContext",ja(2,Fme,e,i))}}function qme(t,n){t&1&&A(0,Gme,1,5,"ng-template",5)}function Yme(t,n){}function Qme(t,n){if(t&1&&A(0,Yme,0,0,"ng-template",16),t&2){let e=x(2);v("ngTemplateOutlet",e.headerTemplate)}}function Kme(t,n){t&1&&A(0,Qme,1,1,"ng-template",6)}function Zme(t,n){}function Xme(t,n){if(t&1&&A(0,Zme,0,0,"ng-template",16),t&2){let e=x(2);v("ngTemplateOutlet",e.footerTemplate)}}function Jme(t,n){t&1&&A(0,Xme,1,1,"ng-template",7)}function ehe(t,n){}function the(t,n){if(t&1&&A(0,ehe,0,0,"ng-template",15),t&2){let e=n.searchTerm,i=x(2);v("ngTemplateOutlet",i.notFoundTemplate)("ngTemplateOutletContext",$t(2,EI,e))}}function ihe(t,n){t&1&&A(0,the,1,4,"ng-template",8)}function nhe(t,n){}function rhe(t,n){if(t&1&&A(0,nhe,0,0,"ng-template",16),t&2){let e=x(2);v("ngTemplateOutlet",e.typeToSearchTemplate)}}function ohe(t,n){t&1&&A(0,rhe,1,1,"ng-template",9)}function ahe(t,n){}function she(t,n){if(t&1&&A(0,ahe,0,0,"ng-template",15),t&2){let e=n.searchTerm,i=x(2);v("ngTemplateOutlet",i.loadingTextTemplate)("ngTemplateOutletContext",$t(2,EI,e))}}function lhe(t,n){t&1&&A(0,she,1,4,"ng-template",10)}function che(t,n){}function dhe(t,n){if(t&1&&A(0,che,0,0,"ng-template",15),t&2){let e=n.searchTerm,i=x(2);v("ngTemplateOutlet",i.tagTemplate)("ngTemplateOutletContext",$t(2,EI,e))}}function uhe(t,n){t&1&&A(0,dhe,1,4,"ng-template",11)}function mhe(t,n){}function hhe(t,n){if(t&1&&A(0,mhe,0,0,"ng-template",16),t&2){let e=x(2);v("ngTemplateOutlet",e.loadingSpinnerTemplate)}}function phe(t,n){t&1&&A(0,hhe,1,1,"ng-template",12)}function fhe(t,n){}function ghe(t,n){if(t&1&&A(0,fhe,0,0,"ng-template",16),t&2){let e=x(2);v("ngTemplateOutlet",e.placeholderTemplate)}}function _he(t,n){t&1&&A(0,ghe,1,1,"ng-template",13)}function bhe(t,n){}function vhe(t,n){if(t&1&&A(0,bhe,0,0,"ng-template",16),t&2){let e=x(2);v("ngTemplateOutlet",e.clearbuttonTemplate)}}function yhe(t,n){t&1&&A(0,vhe,1,1,"ng-template",14)}var xhe=(()=>{let n=class n{constructor(){this.elementRef=u(Y),this.disabled=!1,this.stateChange$=new z}get label(){return(this.elementRef.nativeElement.textContent||"").trim()}ngOnChanges(i){i.disabled&&this.stateChange$.next({value:this.value,disabled:this.disabled})}ngAfterViewChecked(){this.label!==this._previousLabel&&(this._previousLabel=this.label,this.stateChange$.next({value:this.value,disabled:this.disabled,label:this.elementRef.nativeElement.innerHTML}))}ngOnDestroy(){this.stateChange$.complete()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["mtx-option"]],inputs:{value:"value",disabled:[2,"disabled","disabled",B]},exportAs:["mtxOption"],features:[Oe],ngContentSelectors:Ame,decls:1,vars:0,template:function(r,o){r&1&&(Ee(),ne(0))},encapsulation:2,changeDetection:0});let t=n;return t})(),Che=(()=>{let n=class n{constructor(){this._defaultOptions=u(Fz,{optional:!0}),this.changes=new z,this.placeholder=this._defaultOptions?.placeholder,this.notFoundText=this._defaultOptions?.notFoundText??"No items found",this.typeToSearchText=this._defaultOptions?.typeToSearchText??"Type to search",this.addTagText=this._defaultOptions?.addTagText??"Add item",this.loadingText=this._defaultOptions?.loadingText??"Loading...",this.clearAllText=this._defaultOptions?.clearAllText??"Clear all"}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})(),whe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-option-tmp",""]]});let t=n;return t})(),Dhe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-optgroup-tmp",""]]});let t=n;return t})(),Mhe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-label-tmp",""]]});let t=n;return t})(),Ehe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-multi-label-tmp",""]]});let t=n;return t})(),She=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-header-tmp",""]]});let t=n;return t})(),khe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-footer-tmp",""]]});let t=n;return t})(),The=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-notfound-tmp",""]]});let t=n;return t})(),Ihe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-typetosearch-tmp",""]]});let t=n;return t})(),Ahe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-loadingtext-tmp",""]]});let t=n;return t})(),Ohe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-tag-tmp",""]]});let t=n;return t})(),Rhe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-loadingspinner-tmp",""]]});let t=n;return t})(),Phe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-placeholder-tmp",""]]});let t=n;return t})(),Fhe=(()=>{let n=class n{constructor(){this.template=u(te)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ng-clearbutton-tmp",""]]});let t=n;return t})(),Fz=new O("mtx-select-default-options"),Rz=0,SI=(()=>{let n=class n{get clearSearchOnAdd(){return this._clearSearchOnAdd??this.closeOnSelect}set clearSearchOnAdd(i){this._clearSearchOnAdd=i}get items(){return this._items}set items(i){this._itemsAreUsed=!0,this._items=i}get value(){return this._value}set value(i){this._assignValue(i)&&this._onChange(i)}get id(){return this._id}set id(i){this._id=i||this._uid,this.stateChanges.next()}get placeholder(){return this._placeholder}set placeholder(i){this._placeholder=i,this.stateChanges.next()}get focused(){return this._focused}get empty(){return this.value==null||Array.isArray(this.value)&&this.value.length===0}get shouldLabelFloat(){return this.focused||!this.empty}get required(){return this._required??this.ngControl?.control?.hasValidator(Ve.required)??!1}set required(i){this._required=i,this.stateChanges.next()}get errorStateMatcher(){return this._errorStateTracker.matcher}set errorStateMatcher(i){this._errorStateTracker.matcher=i}get panelOpen(){return!!this.ngSelect.isOpen}get errorState(){return this._errorStateTracker.errorState}set errorState(i){this._errorStateTracker.errorState=i}constructor(){this._intl=u(Che),this._changeDetectorRef=u(ye),this._elementRef=u(Y),this._focusMonitor=u(oi),this.ngControl=u(Kn,{optional:!0,self:!0}),this._parentFormField=u(oa,{optional:!0}),this._defaultOptions=u(Fz,{optional:!0}),this._document=u(_e),this.addTag=!1,this.appearance="underline",this.appendTo=this._defaultOptions?.appendTo??"body",this.bindLabel=this._defaultOptions?.bindLabel,this.bindValue=this._defaultOptions?.bindValue,this.closeOnSelect=!0,this.clearable=!0,this.clearOnBackspace=!0,this.dropdownPosition="auto",this.bufferAmount=4,this.selectableGroup=!1,this.selectableGroupAsModel=!0,this.hideSelected=!1,this.loading=!1,this.labelForId=null,this.markFirst=!0,this.multiple=!1,this.searchable=!0,this.readonly=!1,this.searchFn=null,this.searchWhileComposing=!0,this.selectOnTab=!1,this.trackByFn=null,this.inputAttrs={},this.openOnEnter=this._defaultOptions?.openOnEnter??!0,this.minTermLength=0,this.editableSearchTerm=!1,this.keyDownFn=l=>!0,this.virtualScroll=this._defaultOptions?.virtualScroll??!1,this.fixedPlaceholder=this._defaultOptions?.fixedPlaceholder??!1,this.deselectOnClick=this._defaultOptions?.deselectOnClick??!1,this.blurEvent=new U,this.focusEvent=new U,this.changeEvent=new U,this.openEvent=new U,this.closeEvent=new U,this.searchEvent=new U,this.clearEvent=new U,this.addEvent=new U,this.removeEvent=new U,this.scroll=new U,this.scrollToEnd=new U,this._clearSearchOnAdd=this._defaultOptions?.clearSearchOnAdd,this._items=[],this._itemsAreUsed=!1,this._destroy$=new z,this._value=null,this.stateChanges=new z,this._uid=`mtx-select-${Rz++}`,this._focused=!1,this.disabled=!1,this.ariaLabel="",this.ariaLabelledby=null,this._ariaDescribedby=null,this.controlType="mtx-select",this._onChange=()=>{},this._onTouched=()=>{},this._valueId=`mtx-select-value-${Rz++}`,this._intlChangesSubscription=ke.EMPTY;let i=this._focusMonitor,r=u(kl),o=u(Oc,{optional:!0}),a=u(nn,{optional:!0}),s=this.ngControl;this._intlChangesSubscription=this._intl.changes.subscribe(()=>{this._changeDetectorRef.detectChanges()}),i.monitor(this._elementRef,!0).subscribe(l=>{this._focused&&!l&&this._onTouched(),this._focused=!!l,this.stateChanges.next()}),this.ngControl&&(this.ngControl.valueAccessor=this),this._errorStateTracker=new Sl(r,s,a,o,this.stateChanges),this.id=this.id}ngOnInit(){this.compareWith&&(this.ngSelect.compareWith=this.compareWith)}ngAfterViewInit(){this._itemsAreUsed||(this.ngSelect.escapeHTML=!1,this._setItemsFromMtxOptions())}ngDoCheck(){if(this.ngControl){let i=this.ngControl;this._previousControl!==i.control&&(this._previousControl!==void 0&&i.disabled!==null&&i.disabled!==this.disabled&&(this.disabled=i.disabled),this._previousControl=i.control),this.updateErrorState()}}ngOnDestroy(){this._destroy$.next(),this._destroy$.complete(),this.stateChanges.complete(),this._focusMonitor.stopMonitoring(this._elementRef),this._intlChangesSubscription.unsubscribe()}_getAriaLabelledby(){if(this.ariaLabel)return null;let i=this._parentFormField?.getLabelId(),r=(i?i+" ":"")+this._valueId;return this.ariaLabelledby&&(r+=" "+this.ariaLabelledby),r}setDescribedByIds(i){this._ariaDescribedby=i.length?i.join(" "):null}setDisabledState(i){this.disabled=i,this._changeDetectorRef.markForCheck(),this.stateChanges.next()}onContainerClick(i){i.target.classList.contains("ng-arrow-wrapper")||(this.focus(),this.open())}writeValue(i){this._assignValue(i)}registerOnChange(i){this._onChange=i}registerOnTouched(i){this._onTouched=i}updateErrorState(){this._errorStateTracker.updateErrorState()}_assignValue(i){return i!==this._value||this.multiple&&Array.isArray(i)?(this._value=i,this._changeDetectorRef.markForCheck(),!0):!1}_setItemsFromMtxOptions(){let i=o=>{this.items=o.map(a=>({$ngOptionValue:a.value,$ngOptionLabel:a.elementRef.nativeElement.innerHTML,disabled:a.disabled})),this.ngSelect.itemsList.setItems(this.items),this.ngSelect.hasValue&&this.ngSelect.itemsList.mapSelectedItems(),this.ngSelect.detectChanges()},r=()=>{let o=it(this.mtxOptions.changes,this._destroy$);it(...this.mtxOptions.map(a=>a.stateChange$)).pipe(xe(o)).subscribe(a=>{let s=this.ngSelect.itemsList.findItem(a.value);s.disabled=a.disabled,s.label=a.label||s.label,this.ngSelect.detectChanges()})};this.mtxOptions.changes.pipe(Ue(this.mtxOptions),xe(this._destroy$)).subscribe(o=>{i(o),r()})}open(){this.ngSelect.open()}close(){this.ngSelect.close()}focus(){this.ngSelect.focus()}blur(){this.ngSelect.blur()}openChange(){this.openEvent.emit(),setTimeout(()=>{this._document.getElementById(this.ngSelect.dropdownId)?.classList.add("mat-"+this._parentFormField?.color)})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["mtx-select"]],contentQueries:function(r,o,a){if(r&1&&(Ce(a,whe,5,te),Ce(a,Dhe,5,te),Ce(a,Mhe,5,te),Ce(a,Ehe,5,te),Ce(a,She,5,te),Ce(a,khe,5,te),Ce(a,The,5,te),Ce(a,Ihe,5,te),Ce(a,Ahe,5,te),Ce(a,Ohe,5,te),Ce(a,Rhe,5,te),Ce(a,Phe,5,te),Ce(a,Fhe,5,te),Ce(a,xhe,5)),r&2){let s;j(s=H())&&(o.optionTemplate=s.first),j(s=H())&&(o.optgroupTemplate=s.first),j(s=H())&&(o.labelTemplate=s.first),j(s=H())&&(o.multiLabelTemplate=s.first),j(s=H())&&(o.headerTemplate=s.first),j(s=H())&&(o.footerTemplate=s.first),j(s=H())&&(o.notFoundTemplate=s.first),j(s=H())&&(o.typeToSearchTemplate=s.first),j(s=H())&&(o.loadingTextTemplate=s.first),j(s=H())&&(o.tagTemplate=s.first),j(s=H())&&(o.loadingSpinnerTemplate=s.first),j(s=H())&&(o.placeholderTemplate=s.first),j(s=H())&&(o.clearbuttonTemplate=s.first),j(s=H())&&(o.mtxOptions=s)}},viewQuery:function(r,o){if(r&1&&ie(Ome,7),r&2){let a;j(a=H())&&(o.ngSelect=a.first)}},hostAttrs:["role","combobox","aria-autocomplete","none",1,"mtx-select"],hostVars:20,hostBindings:function(r,o){r&2&&(X("id",o.id)("aria-expanded",o.panelOpen)("aria-label",o.ariaLabel||null)("aria-labelledby",o._getAriaLabelledby())("aria-describedby",o._ariaDescribedby||null)("aria-required",o.required.toString())("aria-disabled",o.disabled.toString())("aria-invalid",o.errorState),G("mtx-select-floating",o.shouldLabelFloat)("mtx-select-disabled",o.disabled)("mtx-select-invalid",o.errorState)("mtx-select-required",o.required)("mtx-select-empty",o.empty)("mtx-select-multiple",o.multiple))},inputs:{addTag:"addTag",addTagText:"addTagText",appearance:"appearance",appendTo:"appendTo",bindLabel:"bindLabel",bindValue:"bindValue",closeOnSelect:[2,"closeOnSelect","closeOnSelect",B],clearAllText:"clearAllText",clearable:[2,"clearable","clearable",B],clearOnBackspace:[2,"clearOnBackspace","clearOnBackspace",B],compareWith:"compareWith",dropdownPosition:"dropdownPosition",groupBy:"groupBy",groupValue:"groupValue",bufferAmount:"bufferAmount",selectableGroup:[2,"selectableGroup","selectableGroup",B],selectableGroupAsModel:[2,"selectableGroupAsModel","selectableGroupAsModel",B],hideSelected:[2,"hideSelected","hideSelected",B],loading:[2,"loading","loading",B],loadingText:"loadingText",labelForId:"labelForId",markFirst:[2,"markFirst","markFirst",B],maxSelectedItems:"maxSelectedItems",multiple:[2,"multiple","multiple",B],notFoundText:"notFoundText",searchable:[2,"searchable","searchable",B],readonly:[2,"readonly","readonly",B],searchFn:"searchFn",searchWhileComposing:[2,"searchWhileComposing","searchWhileComposing",B],selectOnTab:[2,"selectOnTab","selectOnTab",B],trackByFn:"trackByFn",inputAttrs:"inputAttrs",tabIndex:"tabIndex",openOnEnter:[2,"openOnEnter","openOnEnter",B],minTermLength:"minTermLength",editableSearchTerm:[2,"editableSearchTerm","editableSearchTerm",B],keyDownFn:"keyDownFn",virtualScroll:[2,"virtualScroll","virtualScroll",B],typeToSearchText:"typeToSearchText",typeahead:"typeahead",isOpen:"isOpen",fixedPlaceholder:[2,"fixedPlaceholder","fixedPlaceholder",B],deselectOnClick:[2,"deselectOnClick","deselectOnClick",B],clearSearchOnAdd:"clearSearchOnAdd",items:"items",value:"value",id:"id",placeholder:"placeholder",disabled:[2,"disabled","disabled",B],required:[2,"required","required",B],errorStateMatcher:"errorStateMatcher",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"]},outputs:{blurEvent:"blur",focusEvent:"focus",changeEvent:"change",openEvent:"open",closeEvent:"close",searchEvent:"search",clearEvent:"clear",addEvent:"add",removeEvent:"remove",scroll:"scroll",scrollToEnd:"scrollToEnd"},exportAs:["mtxSelect"],features:[we([{provide:Za,useExisting:n}])],decls:15,vars:63,consts:[["ngSelect",""],[3,"ngModelChange","blur","focus","change","open","close","search","clear","add","remove","scroll","scrollToEnd","ngModel","ngModelOptions","placeholder","items","addTag","addTagText","appendTo","appearance","bindLabel","bindValue","closeOnSelect","clearAllText","clearable","clearOnBackspace","dropdownPosition","groupBy","groupValue","bufferAmount","hideSelected","isOpen","inputAttrs","loading","loadingText","labelForId","markFirst","maxSelectedItems","multiple","notFoundText","readonly","typeahead","typeToSearchText","trackByFn","searchable","searchFn","searchWhileComposing","clearSearchOnAdd","selectableGroup","selectableGroupAsModel","selectOnTab","tabIndex","openOnEnter","minTermLength","editableSearchTerm","keyDownFn","virtualScroll","fixedPlaceholder","deselectOnClick"],["ng-option-tmp",""],["ng-optgroup-tmp",""],["ng-label-tmp",""],["ng-multi-label-tmp",""],["ng-header-tmp",""],["ng-footer-tmp",""],["ng-notfound-tmp",""],["ng-typetosearch-tmp",""],["ng-loadingtext-tmp",""],["ng-tag-tmp",""],["ng-loadingspinner-tmp",""],["ng-placeholder-tmp",""],["ng-clearbutton-tmp",""],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[3,"ngTemplateOutlet"]],template:function(r,o){if(r&1){let a=q();m(0,"ng-select",1,0),fn("ngModelChange",function(l){return k(a),Mn(o.value,l)||(o.value=l),T(l)}),S("blur",function(l){return k(a),T(o.blurEvent.emit(l))})("focus",function(l){return k(a),T(o.focusEvent.emit(l))})("change",function(l){return k(a),T(o.changeEvent.emit(l))})("open",function(){return k(a),T(o.openChange())})("close",function(){return k(a),T(o.closeEvent.emit())})("search",function(l){return k(a),T(o.searchEvent.emit(l))})("clear",function(l){return k(a),T(o.clearEvent.emit(l))})("add",function(l){return k(a),T(o.addEvent.emit(l))})("remove",function(l){return k(a),T(o.removeEvent.emit(l))})("scroll",function(l){return k(a),T(o.scroll.emit(l))})("scrollToEnd",function(){return k(a),T(o.scrollToEnd.emit())}),L(2,Vme,1,0,null,2),L(3,Hme,1,0,null,3),L(4,$me,1,0,null,4),L(5,qme,1,0,null,5),L(6,Kme,1,0,null,6),L(7,Jme,1,0,null,7),L(8,ihe,1,0,null,8),L(9,ohe,1,0,null,9),L(10,lhe,1,0,null,10),L(11,uhe,1,0,null,11),L(12,phe,1,0,null,12),L(13,_he,1,0,null,13),L(14,yhe,1,0,null,14),h()}r&2&&(G("ng-select-invalid",o.errorState),pn("ngModel",o.value),v("ngModelOptions",dt(62,Rme))("placeholder",o.placeholder||o._intl.placeholder)("items",o.items)("addTag",o.addTag)("addTagText",o.addTagText||o._intl.addTagText)("appendTo",o.appendTo)("appearance",o.appearance)("bindLabel",o.bindLabel)("bindValue",o.bindValue)("closeOnSelect",o.closeOnSelect)("clearAllText",o.clearAllText||o._intl.clearAllText)("clearable",o.clearable)("clearOnBackspace",o.clearOnBackspace)("dropdownPosition",o.dropdownPosition)("groupBy",o.groupBy)("groupValue",o.groupValue)("bufferAmount",o.bufferAmount)("hideSelected",o.hideSelected)("isOpen",o.isOpen)("inputAttrs",o.inputAttrs)("loading",o.loading)("loadingText",o.loadingText||o._intl.loadingText)("labelForId",o.labelForId)("markFirst",o.markFirst)("maxSelectedItems",o.maxSelectedItems)("multiple",o.multiple)("notFoundText",o.notFoundText||o._intl.notFoundText)("readonly",o.readonly||o.disabled)("typeahead",o.typeahead)("typeToSearchText",o.typeToSearchText||o._intl.typeToSearchText)("trackByFn",o.trackByFn)("searchable",o.searchable)("searchFn",o.searchFn)("searchWhileComposing",o.searchWhileComposing)("clearSearchOnAdd",o.clearSearchOnAdd)("selectableGroup",o.selectableGroup)("selectableGroupAsModel",o.selectableGroupAsModel)("selectOnTab",o.selectOnTab)("tabIndex",o.tabIndex)("openOnEnter",o.openOnEnter)("minTermLength",o.minTermLength)("editableSearchTerm",o.editableSearchTerm)("keyDownFn",o.keyDownFn)("virtualScroll",o.virtualScroll)("fixedPlaceholder",o.fixedPlaceholder)("deselectOnClick",o.deselectOnClick),g(2),V(o.optionTemplate?2:-1),g(),V(o.optgroupTemplate?3:-1),g(),V(o.labelTemplate?4:-1),g(),V(o.multiLabelTemplate?5:-1),g(),V(o.headerTemplate?6:-1),g(),V(o.footerTemplate?7:-1),g(),V(o.notFoundTemplate?8:-1),g(),V(o.typeToSearchTemplate?9:-1),g(),V(o.loadingTextTemplate?10:-1),g(),V(o.tagTemplate?11:-1),g(),V(o.loadingSpinnerTemplate?12:-1),g(),V(o.placeholderTemplate?13:-1),g(),V(o.clearbuttonTemplate?14:-1))},dependencies:[MI,Oz,HC,jC,zC,UC,$C,WC,qC,GC,YC,QC,KC,ZC,XC,Qr,Pt,Ro,$n],styles:[`.ng-select{padding:var(--mat-form-field-filled-with-label-container-padding-top, 24px) 16px var(--mat-form-field-filled-with-label-container-padding-bottom, 8px);margin:calc(var(--mat-form-field-filled-with-label-container-padding-top, 24px) * -1) -16px calc(var(--mat-form-field-filled-with-label-container-padding-bottom, 8px) * -1)}.mdc-text-field--outlined .ng-select,.mdc-text-field--no-label .ng-select{padding-top:var(--mat-form-field-container-vertical-padding, 16px);padding-bottom:var(--mat-form-field-container-vertical-padding, 16px);margin-top:calc(var(--mat-form-field-container-vertical-padding, 16px) * -1);margin-bottom:calc(var(--mat-form-field-container-vertical-padding, 16px) * -1)}.ng-select .ng-select-container{align-items:center;color:var(--mtx-select-container-text-color, var(--mat-sys-on-surface))}.ng-select .ng-select-container .ng-value-container{align-items:center;gap:4px}.ng-select .ng-select-container .ng-value-container .ng-input>input{height:var(--mat-form-field-container-text-line-height, var(--mat-sys-body-large-line-height));color:inherit;font:inherit}.ng-select .ng-select-container .ng-clear-wrapper{display:inline-flex;justify-content:center;align-items:center;width:24px;height:var(--mat-form-field-container-text-line-height, var(--mat-sys-body-large-line-height))}.ng-select .ng-placeholder{transition:opacity .2s;opacity:1;color:var(--mtx-select-placeholder-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-form-field-hide-placeholder .ng-select .ng-placeholder{opacity:0}.ng-select .ng-has-value .ng-placeholder{display:none}.ng-select .ng-clear-wrapper{color:var(--mtx-select-clear-icon-color, var(--mat-sys-on-surface))}.ng-select .ng-clear-wrapper:hover .ng-clear{color:var(--mtx-select-clear-icon-hover-color, var(--mat-sys-error))}.ng-select.ng-select-disabled .ng-value{color:var(--mtx-select-disabled-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.ng-select.ng-select-opened .ng-arrow-wrapper .ng-arrow{top:-2px;border-width:0 5px 5px}.ng-select.ng-select-single.ng-select-filtered .ng-placeholder{display:initial;visibility:hidden}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value{display:inline-flex;align-items:center;height:var(--mat-form-field-container-text-line-height, var(--mat-sys-body-large-line-height));padding:0 calc((var(--mat-form-field-container-text-line-height, var(--mat-sys-body-large-line-height)) - 16px) / 2);border-radius:9999px;font-size:.875em;background-color:var(--mtx-select-multiple-value-background-color, transparent);border:1px solid var(--mtx-select-multiple-value-outline-color, var(--mat-sys-outline))}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value.ng-value-disabled{opacity:.4}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value .ng-value-label{display:inline-block;margin:0 4px;line-height:16px}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value .ng-value-icon{width:16px;height:16px;line-height:16px;border-radius:50%;text-align:center}.ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value .ng-value-icon:hover{background-color:var(--mtx-select-multiple-value-icon-hover-background-color, var(--mat-sys-outline-variant))}.ng-select.ng-select-multiple.ng-select-disabled .ng-select-container .ng-value-container .ng-value{border-color:var(--mtx-select-multiple-value-disabled-outline-color, color-mix(in srgb, var(--mat-sys-outline) 38%, transparent))}.ng-select .ng-arrow-wrapper{width:10px}.ng-select .ng-arrow{border-width:5px 5px 2px;border-style:solid;border-color:var(--mtx-select-enabled-arrow-color, var(--mat-sys-on-surface)) transparent transparent}.ng-select.ng-select-disabled .ng-arrow{border-color:var(--mtx-select-disabled-arrow-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)) transparent transparent}.ng-select.ng-select-invalid .ng-arrow{border-color:var(--mtx-select-invalid-arrow-color, var(--mat-sys-error)) transparent transparent}.ng-select.ng-select-opened .ng-arrow{border-color:transparent transparent var(--mtx-select-enabled-arrow-color, var(--mat-sys-on-surface))}.ng-select.ng-select-opened.ng-select-invalid .ng-arrow{border-color:transparent transparent var(--mtx-select-invalid-arrow-color, var(--mat-sys-error))}.ng-dropdown-panel{background-color:var(--mtx-select-panel-background-color, var(--mat-sys-surface-container))}.ng-dropdown-panel.ng-select-bottom{top:100%;border-bottom-left-radius:var(--mtx-select-container-shape, var(--mat-sys-corner-extra-small));border-bottom-right-radius:var(--mtx-select-container-shape, var(--mat-sys-corner-extra-small));box-shadow:var(--mtx-select-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, .2), 0px 2px 2px 0px rgba(0, 0, 0, .14), 0px 1px 5px 0px rgba(0, 0, 0, .12))}.ng-dropdown-panel.ng-select-top{bottom:100%;border-top-left-radius:var(--mtx-select-container-shape, var(--mat-sys-corner-extra-small));border-top-right-radius:var(--mtx-select-container-shape, var(--mat-sys-corner-extra-small));box-shadow:var(--mtx-select-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, .2), 0px 2px 2px 0px rgba(0, 0, 0, .14), 0px 1px 5px 0px rgba(0, 0, 0, .12))}.ng-dropdown-panel .ng-dropdown-header,.ng-dropdown-panel .ng-dropdown-footer{padding:14px 16px}.ng-dropdown-panel .ng-dropdown-header{border-bottom:1px solid var(--mtx-select-panel-divider-color, var(--mat-sys-outline))}.ng-dropdown-panel .ng-dropdown-footer{border-top:1px solid var(--mtx-select-panel-divider-color, var(--mat-sys-outline))}.ng-dropdown-panel .ng-dropdown-panel-items .ng-optgroup{padding:14px 16px;font-weight:500;-webkit-user-select:none;user-select:none;cursor:pointer;color:var(--mtx-select-optgroup-label-text-color, var(--mat-sys-on-surface))}.ng-dropdown-panel .ng-dropdown-panel-items .ng-optgroup.ng-option-disabled{cursor:default}.ng-dropdown-panel .ng-dropdown-panel-items .ng-optgroup.ng-option-marked{background-color:var(--mtx-select-option-hover-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}.ng-dropdown-panel .ng-dropdown-panel-items .ng-optgroup.ng-option-selected{background-color:var(--mtx-select-option-selected-state-background-color, var(--mat-sys-secondary-container));color:var(--mtx-select-option-selected-state-text-color, var(--mat-sys-on-surface))}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option{position:relative;padding:14px 16px;text-overflow:ellipsis;text-decoration:none;text-align:left;white-space:nowrap;overflow:hidden;color:var(--mtx-select-option-label-text-color, var(--mat-sys-on-surface))}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked{background-color:var(--mtx-select-option-hover-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected{background-color:var(--mtx-select-option-selected-state-background-color, var(--mat-sys-secondary-container));color:var(--mtx-select-option-selected-state-text-color, var(--mat-sys-on-surface))}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-disabled{color:var(--mtx-select-option-disabled-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}[dir=rtl] .ng-dropdown-panel .ng-dropdown-panel-items .ng-option{text-align:right}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-child{padding-left:32px}[dir=rtl] .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-child{padding-right:32px;padding-left:0}.ng-dropdown-panel .ng-dropdown-panel-items .ng-option .ng-tag-label{margin-right:6px;font-size:80%}[dir=rtl] .ng-dropdown-panel .ng-dropdown-panel-items .ng-option .ng-tag-label{margin-left:6px;margin-right:0} +`],encapsulation:2,changeDetection:0});let t=n;return t})(),Nz=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275mod=ee({type:n}),n.\u0275inj=J({imports:[Je,Qr,Zn,MI,SI]});let t=n;return t})();var Lz=(()=>{let n=class n extends uo{get bindLabel(){return typeof this.props.labelProp=="string"?this.props.labelProp:""}get bindValue(){return typeof this.props.valueProp=="string"?this.props.valueProp:void 0}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-field-combobox"]],features:[le],decls:4,vars:13,consts:[["select",""],[3,"formControl","items","bindLabel","bindValue","multiple","placeholder","required","closeOnSelect","compareWith"]],template:function(r,o){r&1&&(M(0,"mtx-select",1,0),me(2,"toObservable"),me(3,"async")),r&2&&v("formControl",o.formControl)("items",Re(3,11,Re(2,9,o.props.options)))("bindLabel",o.bindLabel)("bindValue",o.bindValue)("multiple",o.props.multiple)("placeholder",o.props.placeholder)("required",o.props.required)("closeOnSelect",!o.props.multiple)("compareWith",o.props.compareWith)},dependencies:[Zn,Pt,Fo,Po,Nz,SI,f3,cn,p3],encapsulation:2});let t=n;return t})();var Vz=(()=>{let n=class n extends Sg{};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-wrapper-card"]],features:[le],decls:8,vars:1,consts:[["fieldComponent",""],[1,"card"],[1,"card-header"],[1,"card-body"]],template:function(r,o){r&1&&(gt(0,"div",1)(1,"h3",2),f(2,"Its time to party"),yt(),gt(3,"h3",2),f(4),yt(),gt(5,"div",3),df(6,null,0),yt()()),r&2&&(g(4),N(o.props.label))},encapsulation:2});let t=n;return t})(),Bz=(()=>{let n=class n extends Sg{};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275cmp=E({type:n,selectors:[["formly-wrapper-div"]],features:[le],decls:3,vars:0,consts:[["fieldComponent",""]],template:function(r,o){r&1&&(gt(0,"div"),df(1,null,0),yt())},encapsulation:2});let t=n;return t})();function jz(t){return{types:[{name:"combobox",component:Lz,wrappers:["form-field"]}],wrappers:[{name:"card",component:Vz},{name:"div",component:Bz}],validators:[],validationMessages:[{name:"required",message:(n,e)=>t.stream("validation.required")},{name:"min",message:(n,e)=>t.stream("validation.min",{number:e.props?.min})},{name:"max",message:(n,e)=>t.stream("validation.max",{number:e.props?.max})},{name:"minLength",message:(n,e)=>t.stream("validation.min_length",{number:e.props?.minLength})},{name:"maxLength",message:(n,e)=>t.stream("validation.max_length",{number:e.props?.maxLength})}]}}var Lhe={lessThanXSeconds:{one:"\u4E0D\u5230 1 \u79D2",other:"\u4E0D\u5230 {{count}} \u79D2"},xSeconds:{one:"1 \u79D2",other:"{{count}} \u79D2"},halfAMinute:"\u534A\u5206\u949F",lessThanXMinutes:{one:"\u4E0D\u5230 1 \u5206\u949F",other:"\u4E0D\u5230 {{count}} \u5206\u949F"},xMinutes:{one:"1 \u5206\u949F",other:"{{count}} \u5206\u949F"},xHours:{one:"1 \u5C0F\u65F6",other:"{{count}} \u5C0F\u65F6"},aboutXHours:{one:"\u5927\u7EA6 1 \u5C0F\u65F6",other:"\u5927\u7EA6 {{count}} \u5C0F\u65F6"},xDays:{one:"1 \u5929",other:"{{count}} \u5929"},aboutXWeeks:{one:"\u5927\u7EA6 1 \u4E2A\u661F\u671F",other:"\u5927\u7EA6 {{count}} \u4E2A\u661F\u671F"},xWeeks:{one:"1 \u4E2A\u661F\u671F",other:"{{count}} \u4E2A\u661F\u671F"},aboutXMonths:{one:"\u5927\u7EA6 1 \u4E2A\u6708",other:"\u5927\u7EA6 {{count}} \u4E2A\u6708"},xMonths:{one:"1 \u4E2A\u6708",other:"{{count}} \u4E2A\u6708"},aboutXYears:{one:"\u5927\u7EA6 1 \u5E74",other:"\u5927\u7EA6 {{count}} \u5E74"},xYears:{one:"1 \u5E74",other:"{{count}} \u5E74"},overXYears:{one:"\u8D85\u8FC7 1 \u5E74",other:"\u8D85\u8FC7 {{count}} \u5E74"},almostXYears:{one:"\u5C06\u8FD1 1 \u5E74",other:"\u5C06\u8FD1 {{count}} \u5E74"}},Hz=(t,n,e)=>{let i,r=Lhe[t];return typeof r=="string"?i=r:n===1?i=r.one:i=r.other.replace("{{count}}",String(n)),e?.addSuffix?e.comparison&&e.comparison>0?i+"\u5185":i+"\u524D":i};var Vhe={full:"y'\u5E74'M'\u6708'd'\u65E5' EEEE",long:"y'\u5E74'M'\u6708'd'\u65E5'",medium:"yyyy-MM-dd",short:"yy-MM-dd"},Bhe={full:"zzzz a h:mm:ss",long:"z a h:mm:ss",medium:"a h:mm:ss",short:"a h:mm"},jhe={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},zz={date:ia({formats:Vhe,defaultWidth:"full"}),time:ia({formats:Bhe,defaultWidth:"full"}),dateTime:ia({formats:jhe,defaultWidth:"full"})};function Uz(t,n,e){let i="eeee p";return zV(t,n,e)?i:t.getTime()>n.getTime()?"'\u4E0B\u4E2A'"+i:"'\u4E0A\u4E2A'"+i}var Hhe={lastWeek:Uz,yesterday:"'\u6628\u5929' p",today:"'\u4ECA\u5929' p",tomorrow:"'\u660E\u5929' p",nextWeek:Uz,other:"PP p"},$z=(t,n,e,i)=>{let r=Hhe[t];return typeof r=="function"?r(n,e,i):r};var zhe={narrow:["\u524D","\u516C\u5143"],abbreviated:["\u524D","\u516C\u5143"],wide:["\u516C\u5143\u524D","\u516C\u5143"]},Uhe={narrow:["1","2","3","4"],abbreviated:["\u7B2C\u4E00\u5B63","\u7B2C\u4E8C\u5B63","\u7B2C\u4E09\u5B63","\u7B2C\u56DB\u5B63"],wide:["\u7B2C\u4E00\u5B63\u5EA6","\u7B2C\u4E8C\u5B63\u5EA6","\u7B2C\u4E09\u5B63\u5EA6","\u7B2C\u56DB\u5B63\u5EA6"]},$he={narrow:["\u4E00","\u4E8C","\u4E09","\u56DB","\u4E94","\u516D","\u4E03","\u516B","\u4E5D","\u5341","\u5341\u4E00","\u5341\u4E8C"],abbreviated:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],wide:["\u4E00\u6708","\u4E8C\u6708","\u4E09\u6708","\u56DB\u6708","\u4E94\u6708","\u516D\u6708","\u4E03\u6708","\u516B\u6708","\u4E5D\u6708","\u5341\u6708","\u5341\u4E00\u6708","\u5341\u4E8C\u6708"]},Whe={narrow:["\u65E5","\u4E00","\u4E8C","\u4E09","\u56DB","\u4E94","\u516D"],short:["\u65E5","\u4E00","\u4E8C","\u4E09","\u56DB","\u4E94","\u516D"],abbreviated:["\u5468\u65E5","\u5468\u4E00","\u5468\u4E8C","\u5468\u4E09","\u5468\u56DB","\u5468\u4E94","\u5468\u516D"],wide:["\u661F\u671F\u65E5","\u661F\u671F\u4E00","\u661F\u671F\u4E8C","\u661F\u671F\u4E09","\u661F\u671F\u56DB","\u661F\u671F\u4E94","\u661F\u671F\u516D"]},Ghe={narrow:{am:"\u4E0A",pm:"\u4E0B",midnight:"\u51CC\u6668",noon:"\u5348",morning:"\u65E9",afternoon:"\u4E0B\u5348",evening:"\u665A",night:"\u591C"},abbreviated:{am:"\u4E0A\u5348",pm:"\u4E0B\u5348",midnight:"\u51CC\u6668",noon:"\u4E2D\u5348",morning:"\u65E9\u6668",afternoon:"\u4E2D\u5348",evening:"\u665A\u4E0A",night:"\u591C\u95F4"},wide:{am:"\u4E0A\u5348",pm:"\u4E0B\u5348",midnight:"\u51CC\u6668",noon:"\u4E2D\u5348",morning:"\u65E9\u6668",afternoon:"\u4E2D\u5348",evening:"\u665A\u4E0A",night:"\u591C\u95F4"}},qhe={narrow:{am:"\u4E0A",pm:"\u4E0B",midnight:"\u51CC\u6668",noon:"\u5348",morning:"\u65E9",afternoon:"\u4E0B\u5348",evening:"\u665A",night:"\u591C"},abbreviated:{am:"\u4E0A\u5348",pm:"\u4E0B\u5348",midnight:"\u51CC\u6668",noon:"\u4E2D\u5348",morning:"\u65E9\u6668",afternoon:"\u4E2D\u5348",evening:"\u665A\u4E0A",night:"\u591C\u95F4"},wide:{am:"\u4E0A\u5348",pm:"\u4E0B\u5348",midnight:"\u51CC\u6668",noon:"\u4E2D\u5348",morning:"\u65E9\u6668",afternoon:"\u4E2D\u5348",evening:"\u665A\u4E0A",night:"\u591C\u95F4"}},Yhe=(t,n)=>{let e=Number(t);switch(n?.unit){case"date":return e.toString()+"\u65E5";case"hour":return e.toString()+"\u65F6";case"minute":return e.toString()+"\u5206";case"second":return e.toString()+"\u79D2";default:return"\u7B2C "+e.toString()}},Wz={ordinalNumber:Yhe,era:Yn({values:zhe,defaultWidth:"wide"}),quarter:Yn({values:Uhe,defaultWidth:"wide",argumentCallback:t=>t-1}),month:Yn({values:$he,defaultWidth:"wide"}),day:Yn({values:Whe,defaultWidth:"wide"}),dayPeriod:Yn({values:Ghe,defaultWidth:"wide",formattingValues:qhe,defaultFormattingWidth:"wide"})};var Qhe=/^(第\s*)?\d+(日|时|分|秒)?/i,Khe=/\d+/i,Zhe={narrow:/^(前)/i,abbreviated:/^(前)/i,wide:/^(公元前|公元)/i},Xhe={any:[/^(前)/i,/^(公元)/i]},Jhe={narrow:/^[1234]/i,abbreviated:/^第[一二三四]刻/i,wide:/^第[一二三四]刻钟/i},epe={any:[/(1|一)/i,/(2|二)/i,/(3|三)/i,/(4|四)/i]},tpe={narrow:/^(一|二|三|四|五|六|七|八|九|十[二一])/i,abbreviated:/^(一|二|三|四|五|六|七|八|九|十[二一]|\d|1[12])月/i,wide:/^(一|二|三|四|五|六|七|八|九|十[二一])月/i},ipe={narrow:[/^一/i,/^二/i,/^三/i,/^四/i,/^五/i,/^六/i,/^七/i,/^八/i,/^九/i,/^十(?!(一|二))/i,/^十一/i,/^十二/i],any:[/^一|1/i,/^二|2/i,/^三|3/i,/^四|4/i,/^五|5/i,/^六|6/i,/^七|7/i,/^八|8/i,/^九|9/i,/^十(?!(一|二))|10/i,/^十一|11/i,/^十二|12/i]},npe={narrow:/^[一二三四五六日]/i,short:/^[一二三四五六日]/i,abbreviated:/^周[一二三四五六日]/i,wide:/^星期[一二三四五六日]/i},rpe={any:[/日/i,/一/i,/二/i,/三/i,/四/i,/五/i,/六/i]},ope={any:/^(上午?|下午?|午夜|[中正]午|早上?|下午|晚上?|凌晨|)/i},ape={any:{am:/^上午?/i,pm:/^下午?/i,midnight:/^午夜/i,noon:/^[中正]午/i,morning:/^早上/i,afternoon:/^下午/i,evening:/^晚上?/i,night:/^凌晨/i}},Gz={ordinalNumber:hh({matchPattern:Qhe,parsePattern:Khe,valueCallback:t=>parseInt(t,10)}),era:Qn({matchPatterns:Zhe,defaultMatchWidth:"wide",parsePatterns:Xhe,defaultParseWidth:"any"}),quarter:Qn({matchPatterns:Jhe,defaultMatchWidth:"wide",parsePatterns:epe,defaultParseWidth:"any",valueCallback:t=>t+1}),month:Qn({matchPatterns:tpe,defaultMatchWidth:"wide",parsePatterns:ipe,defaultParseWidth:"any"}),day:Qn({matchPatterns:npe,defaultMatchWidth:"wide",parsePatterns:rpe,defaultParseWidth:"any"}),dayPeriod:Qn({matchPatterns:ope,defaultMatchWidth:"any",parsePatterns:ape,defaultParseWidth:"any"})};var qz={code:"zh-CN",formatDistance:Hz,formatLong:zz,formatRelative:$z,localize:Wz,match:Gz,options:{weekStartsOn:1,firstWeekContainsDate:4}};var spe={lessThanXSeconds:{one:"\u5C11\u65BC 1 \u79D2",other:"\u5C11\u65BC {{count}} \u79D2"},xSeconds:{one:"1 \u79D2",other:"{{count}} \u79D2"},halfAMinute:"\u534A\u5206\u9418",lessThanXMinutes:{one:"\u5C11\u65BC 1 \u5206\u9418",other:"\u5C11\u65BC {{count}} \u5206\u9418"},xMinutes:{one:"1 \u5206\u9418",other:"{{count}} \u5206\u9418"},xHours:{one:"1 \u5C0F\u6642",other:"{{count}} \u5C0F\u6642"},aboutXHours:{one:"\u5927\u7D04 1 \u5C0F\u6642",other:"\u5927\u7D04 {{count}} \u5C0F\u6642"},xDays:{one:"1 \u5929",other:"{{count}} \u5929"},aboutXWeeks:{one:"\u5927\u7D04 1 \u500B\u661F\u671F",other:"\u5927\u7D04 {{count}} \u500B\u661F\u671F"},xWeeks:{one:"1 \u500B\u661F\u671F",other:"{{count}} \u500B\u661F\u671F"},aboutXMonths:{one:"\u5927\u7D04 1 \u500B\u6708",other:"\u5927\u7D04 {{count}} \u500B\u6708"},xMonths:{one:"1 \u500B\u6708",other:"{{count}} \u500B\u6708"},aboutXYears:{one:"\u5927\u7D04 1 \u5E74",other:"\u5927\u7D04 {{count}} \u5E74"},xYears:{one:"1 \u5E74",other:"{{count}} \u5E74"},overXYears:{one:"\u8D85\u904E 1 \u5E74",other:"\u8D85\u904E {{count}} \u5E74"},almostXYears:{one:"\u5C07\u8FD1 1 \u5E74",other:"\u5C07\u8FD1 {{count}} \u5E74"}},Yz=(t,n,e)=>{let i,r=spe[t];return typeof r=="string"?i=r:n===1?i=r.one:i=r.other.replace("{{count}}",String(n)),e?.addSuffix?e.comparison&&e.comparison>0?i+"\u5167":i+"\u524D":i};var lpe={full:"y'\u5E74'M'\u6708'd'\u65E5' EEEE",long:"y'\u5E74'M'\u6708'd'\u65E5'",medium:"yyyy-MM-dd",short:"yy-MM-dd"},cpe={full:"zzzz a h:mm:ss",long:"z a h:mm:ss",medium:"a h:mm:ss",short:"a h:mm"},dpe={full:"{{date}} {{time}}",long:"{{date}} {{time}}",medium:"{{date}} {{time}}",short:"{{date}} {{time}}"},Qz={date:ia({formats:lpe,defaultWidth:"full"}),time:ia({formats:cpe,defaultWidth:"full"}),dateTime:ia({formats:dpe,defaultWidth:"full"})};var upe={lastWeek:"'\u4E0A\u500B'eeee p",yesterday:"'\u6628\u5929' p",today:"'\u4ECA\u5929' p",tomorrow:"'\u660E\u5929' p",nextWeek:"'\u4E0B\u500B'eeee p",other:"P"},Kz=(t,n,e,i)=>upe[t];var mpe={narrow:["\u524D","\u516C\u5143"],abbreviated:["\u524D","\u516C\u5143"],wide:["\u516C\u5143\u524D","\u516C\u5143"]},hpe={narrow:["1","2","3","4"],abbreviated:["\u7B2C\u4E00\u523B","\u7B2C\u4E8C\u523B","\u7B2C\u4E09\u523B","\u7B2C\u56DB\u523B"],wide:["\u7B2C\u4E00\u523B\u9418","\u7B2C\u4E8C\u523B\u9418","\u7B2C\u4E09\u523B\u9418","\u7B2C\u56DB\u523B\u9418"]},ppe={narrow:["\u4E00","\u4E8C","\u4E09","\u56DB","\u4E94","\u516D","\u4E03","\u516B","\u4E5D","\u5341","\u5341\u4E00","\u5341\u4E8C"],abbreviated:["1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11\u6708","12\u6708"],wide:["\u4E00\u6708","\u4E8C\u6708","\u4E09\u6708","\u56DB\u6708","\u4E94\u6708","\u516D\u6708","\u4E03\u6708","\u516B\u6708","\u4E5D\u6708","\u5341\u6708","\u5341\u4E00\u6708","\u5341\u4E8C\u6708"]},fpe={narrow:["\u65E5","\u4E00","\u4E8C","\u4E09","\u56DB","\u4E94","\u516D"],short:["\u65E5","\u4E00","\u4E8C","\u4E09","\u56DB","\u4E94","\u516D"],abbreviated:["\u9031\u65E5","\u9031\u4E00","\u9031\u4E8C","\u9031\u4E09","\u9031\u56DB","\u9031\u4E94","\u9031\u516D"],wide:["\u661F\u671F\u65E5","\u661F\u671F\u4E00","\u661F\u671F\u4E8C","\u661F\u671F\u4E09","\u661F\u671F\u56DB","\u661F\u671F\u4E94","\u661F\u671F\u516D"]},gpe={narrow:{am:"\u4E0A",pm:"\u4E0B",midnight:"\u51CC\u6668",noon:"\u5348",morning:"\u65E9",afternoon:"\u4E0B\u5348",evening:"\u665A",night:"\u591C"},abbreviated:{am:"\u4E0A\u5348",pm:"\u4E0B\u5348",midnight:"\u51CC\u6668",noon:"\u4E2D\u5348",morning:"\u65E9\u6668",afternoon:"\u4E2D\u5348",evening:"\u665A\u4E0A",night:"\u591C\u9593"},wide:{am:"\u4E0A\u5348",pm:"\u4E0B\u5348",midnight:"\u51CC\u6668",noon:"\u4E2D\u5348",morning:"\u65E9\u6668",afternoon:"\u4E2D\u5348",evening:"\u665A\u4E0A",night:"\u591C\u9593"}},_pe={narrow:{am:"\u4E0A",pm:"\u4E0B",midnight:"\u51CC\u6668",noon:"\u5348",morning:"\u65E9",afternoon:"\u4E0B\u5348",evening:"\u665A",night:"\u591C"},abbreviated:{am:"\u4E0A\u5348",pm:"\u4E0B\u5348",midnight:"\u51CC\u6668",noon:"\u4E2D\u5348",morning:"\u65E9\u6668",afternoon:"\u4E2D\u5348",evening:"\u665A\u4E0A",night:"\u591C\u9593"},wide:{am:"\u4E0A\u5348",pm:"\u4E0B\u5348",midnight:"\u51CC\u6668",noon:"\u4E2D\u5348",morning:"\u65E9\u6668",afternoon:"\u4E2D\u5348",evening:"\u665A\u4E0A",night:"\u591C\u9593"}},bpe=(t,n)=>{let e=Number(t);switch(n?.unit){case"date":return e+"\u65E5";case"hour":return e+"\u6642";case"minute":return e+"\u5206";case"second":return e+"\u79D2";default:return"\u7B2C "+e}},Zz={ordinalNumber:bpe,era:Yn({values:mpe,defaultWidth:"wide"}),quarter:Yn({values:hpe,defaultWidth:"wide",argumentCallback:t=>t-1}),month:Yn({values:ppe,defaultWidth:"wide"}),day:Yn({values:fpe,defaultWidth:"wide"}),dayPeriod:Yn({values:gpe,defaultWidth:"wide",formattingValues:_pe,defaultFormattingWidth:"wide"})};var vpe=/^(第\s*)?\d+(日|時|分|秒)?/i,ype=/\d+/i,xpe={narrow:/^(前)/i,abbreviated:/^(前)/i,wide:/^(公元前|公元)/i},Cpe={any:[/^(前)/i,/^(公元)/i]},wpe={narrow:/^[1234]/i,abbreviated:/^第[一二三四]刻/i,wide:/^第[一二三四]刻鐘/i},Dpe={any:[/(1|一)/i,/(2|二)/i,/(3|三)/i,/(4|四)/i]},Mpe={narrow:/^(一|二|三|四|五|六|七|八|九|十[二一])/i,abbreviated:/^(一|二|三|四|五|六|七|八|九|十[二一]|\d|1[12])月/i,wide:/^(一|二|三|四|五|六|七|八|九|十[二一])月/i},Epe={narrow:[/^一/i,/^二/i,/^三/i,/^四/i,/^五/i,/^六/i,/^七/i,/^八/i,/^九/i,/^十(?!(一|二))/i,/^十一/i,/^十二/i],any:[/^一|1/i,/^二|2/i,/^三|3/i,/^四|4/i,/^五|5/i,/^六|6/i,/^七|7/i,/^八|8/i,/^九|9/i,/^十(?!(一|二))|10/i,/^十一|11/i,/^十二|12/i]},Spe={narrow:/^[一二三四五六日]/i,short:/^[一二三四五六日]/i,abbreviated:/^週[一二三四五六日]/i,wide:/^星期[一二三四五六日]/i},kpe={any:[/日/i,/一/i,/二/i,/三/i,/四/i,/五/i,/六/i]},Tpe={any:/^(上午?|下午?|午夜|[中正]午|早上?|下午|晚上?|凌晨)/i},Ipe={any:{am:/^上午?/i,pm:/^下午?/i,midnight:/^午夜/i,noon:/^[中正]午/i,morning:/^早上/i,afternoon:/^下午/i,evening:/^晚上?/i,night:/^凌晨/i}},Xz={ordinalNumber:hh({matchPattern:vpe,parsePattern:ype,valueCallback:t=>parseInt(t,10)}),era:Qn({matchPatterns:xpe,defaultMatchWidth:"wide",parsePatterns:Cpe,defaultParseWidth:"any"}),quarter:Qn({matchPatterns:wpe,defaultMatchWidth:"wide",parsePatterns:Dpe,defaultParseWidth:"any",valueCallback:t=>t+1}),month:Qn({matchPatterns:Mpe,defaultMatchWidth:"wide",parsePatterns:Epe,defaultParseWidth:"any"}),day:Qn({matchPatterns:Spe,defaultMatchWidth:"wide",parsePatterns:kpe,defaultParseWidth:"any"}),dayPeriod:Qn({matchPatterns:Tpe,defaultMatchWidth:"any",parsePatterns:Ipe,defaultParseWidth:"any"})};var Jz={code:"zh-TW",formatDistance:Yz,formatLong:Qz,formatRelative:Kz,localize:Zz,match:Xz,options:{weekStartsOn:1,firstWeekContainsDate:4}};var kI={navPos:"side",theme:"auto",dir:"ltr",showHeader:!0,headerPos:"fixed",showUserPanel:!0,sidenavOpened:!0,sidenavCollapsed:!1,language:"auto"};var ha=(()=>{let n=class n{get notify(){return this.notify$.asObservable()}constructor(){this.key="ng-matero-settings",this.document=u(_e),this.translate=u(ca),this.store=u(xz),this.mediaMatcher=u(Xm),this.dir=u(vz),this.notify$=new rt({}),this.htmlElement=this.document.querySelector("html"),this.storedOptions=this.store.get(this.key),this.options=Object.assign(kI,this.storedOptions),this.languages=["en-US","zh-CN","zh-TW"],this.localeMap={"en-US":cu,"zh-CN":qz,"zh-TW":Jz},this.translate.addLangs(this.languages)}reset(){this.store.remove(this.key)}setOptions(i){this.options=Object.assign(kI,this.options,i),this.store.set(this.key,this.options),this.notify$.next(this.options)}setDirection(i){i&&this.setOptions({dir:i}),this.dir.value=this.options.dir,this.htmlElement.dir=this.options.dir}getThemeColor(){return this.options.theme==="auto"&&this.mediaMatcher.matchMedia("(prefers-color-scheme)").media!=="not all"?this.mediaMatcher.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":this.options.theme}setTheme(i){i&&this.setOptions({theme:i}),this.getThemeColor()==="dark"?this.htmlElement.classList.add("theme-dark"):this.htmlElement.classList.remove("theme-dark")}getTranslateLang(){if(this.options.language==="auto"){let i=navigator.language;return this.languages.includes(i)?i:"en-US"}return this.options.language}setLanguage(i){i&&this.setOptions({language:i}),this.translate.use(this.getTranslateLang())}getLocale(){return this.localeMap[this.getTranslateLang()]}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var e8=(()=>{let n=class n{constructor(){this.http=u(kr),this.oidcAuth=u(jt),this.menuService=u(bo),this.permissonsService=u(wu),this.rolesService=u(Ph),this.oidcAuth.permissionsChange$.subscribe(()=>{console.log("StartupService: Permission change event received, refreshing permissions"),this.setPermissions()})}load(){return new Promise((i,r)=>{this.http.get("data/menu.json").pipe(He(o=>this.setMenu(o.menu))).subscribe({next:()=>{this.setPermissions(),i()},error:o=>{console.error("Error loading menu:",o),this.setPermissions(),i()}})})}setMenu(i){this.menuService.addNamespace(i,"menu"),this.menuService.set(i)}setPermissions(){let i=this.oidcAuth.getUserRoles();console.log("StartupService: User roles from token:",i);let r=["canAdd","canDelete","canEdit","canRead"];this.rolesService.flushRoles(),i.length>0?(this.permissonsService.loadPermissions(r),console.log("StartupService: Loaded permissions:",r),i.includes("HRAdmin")&&(this.rolesService.addRoles({HRAdmin:r}),console.log("StartupService: Added HRAdmin role with permissions:",r)),i.includes("Manager")&&(this.rolesService.addRoles({Manager:r}),console.log("StartupService: Added Manager role with permissions:",r)),i.includes("Employee")&&(this.rolesService.addRoles({Employee:["canRead"]}),console.log("StartupService: Added Employee role with canRead permission"))):(console.log("StartupService: No roles found - setting Guest permissions"),this.permissonsService.loadPermissions(["canRead"]),this.rolesService.addRoles({Guest:["canRead"]}))}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var t8=(()=>{let n=class n{constructor(){this.document=u(_e),this.selector="globalLoader"}getElement(){return this.document.getElementById(this.selector)}hide(){let i=this.getElement();i&&(i.addEventListener("transitionend",()=>{i.className="global-loader-hidden"}),i.classList.contains("global-loader-hidden")||(i.className+=" global-loader-fade-out"))}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var i8=(()=>{let n=class n{constructor(){this.translate=u(ca),this.settings=u(ha)}load(){return new Promise(i=>{let r=this.settings.getTranslateLang();this.translate.setFallbackLang(r),this.translate.use(r).subscribe({next:()=>console.log(`Successfully initialized '${r}' language.'`),error:()=>console.error(`Problem with '${r}' language initialization.'`),complete:()=>i()})})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();function n8(t,n){return n(t)}var TI=new O("BASE_URL");function Ape(t){return new RegExp("^http(s)?://","i").test(t)}function r8(t,n){let e=u(TI,{optional:!0}),i=o=>e&&Ape(o),r=o=>[e?.replace(/\/$/g,""),o.replace(/^\.?\//,"")].filter(a=>a).join("/");return i(t.url)===!1?n(t.clone({url:r(t.url)})):n(t)}function o8(t,n){let e=u(ha);return n(t.clone({headers:t.headers.append("Accept-Language",e.getTranslateLang())}))}function a8(t,n){let e=u(Vg);return t.url.includes("/api/")?n(t).pipe(Vt(i=>{if(i instanceof xl){let r=i.body;if(r&&"code"in r&&r.code!==0)return r.msg&&e.error(r.msg),er(()=>[])}return Q(i)})):n(t)}var b_=(function(t){return t[t.UNAUTHORIZED=401]="UNAUTHORIZED",t[t.FORBIDDEN=403]="FORBIDDEN",t[t.NOT_FOUND=404]="NOT_FOUND",t[t.INTERNAL_SERVER_ERROR=500]="INTERNAL_SERVER_ERROR",t})(b_||{});function s8(t,n){let e=u(Ae),i=u(Vg),r=[b_.FORBIDDEN,b_.NOT_FOUND,b_.INTERNAL_SERVER_ERROR],o=a=>a.error?.message?a.error.message:a.error?.msg?a.error.msg:a.status===0?"Network error - please check your connection":`${a.status} ${a.statusText}`;return n(t).pipe(ei(a=>a.status===0?(console.warn("Network error (possible CORS issue):",a),er(()=>a)):(!t.url.includes("/ai/")&&r.includes(a.status)?e.navigateByUrl(`/${a.status}`,{skipLocationChange:!0}):(console.error("ERROR",a),i.error(o(a)),a.status===b_.UNAUTHORIZED&&console.warn("Unauthorized access - authentication required")),er(()=>a))))}function l8(t,n){let e=u(yz),i=Date.now(),r;return n(t).pipe(He({next:o=>r=o instanceof xl?"succeeded":"",error:o=>r="failed"}),Xr(()=>{let o=Date.now()-i,a=`${t.method} "${t.urlWithParams}" ${r} in ${o} ms.`;e.add(a)}))}var c8=[n8,r8,o8,a8,s8,l8];var tw=["*"],Ope=["content"],Rpe=[[["mat-drawer"]],[["mat-drawer-content"]],"*"],Ppe=["mat-drawer","mat-drawer-content","*"];function Fpe(t,n){if(t&1){let e=q();m(0,"div",1),S("click",function(){k(e);let r=x();return T(r._onBackdropClicked())}),h()}if(t&2){let e=x();G("mat-drawer-shown",e._isShowingBackdrop())}}function Npe(t,n){t&1&&(m(0,"mat-drawer-content"),ne(1,2),h())}var Lpe=[[["mat-sidenav"]],[["mat-sidenav-content"]],"*"],Vpe=["mat-sidenav","mat-sidenav-content","*"];function Bpe(t,n){if(t&1){let e=q();m(0,"div",1),S("click",function(){k(e);let r=x();return T(r._onBackdropClicked())}),h()}if(t&2){let e=x();G("mat-drawer-shown",e._isShowingBackdrop())}}function jpe(t,n){t&1&&(m(0,"mat-sidenav-content"),ne(1,2),h())}var Hpe=`.mat-drawer-container{position:relative;z-index:1;color:var(--mat-sidenav-content-text-color, var(--mat-sys-on-background));background-color:var(--mat-sidenav-content-background-color, var(--mat-sys-background));box-sizing:border-box;display:block;overflow:hidden}.mat-drawer-container[fullscreen]{top:0;left:0;right:0;bottom:0;position:absolute}.mat-drawer-container[fullscreen].mat-drawer-container-has-open{overflow:hidden}.mat-drawer-container.mat-drawer-container-explicit-backdrop .mat-drawer-side{z-index:3}.mat-drawer-container.ng-animate-disabled .mat-drawer-backdrop,.mat-drawer-container.ng-animate-disabled .mat-drawer-content,.ng-animate-disabled .mat-drawer-container .mat-drawer-backdrop,.ng-animate-disabled .mat-drawer-container .mat-drawer-content{transition:none}.mat-drawer-backdrop{top:0;left:0;right:0;bottom:0;position:absolute;display:block;z-index:3;visibility:hidden}.mat-drawer-backdrop.mat-drawer-shown{visibility:visible;background-color:var(--mat-sidenav-scrim-color, color-mix(in srgb, var(--mat-sys-neutral-variant20) 40%, transparent))}.mat-drawer-transition .mat-drawer-backdrop{transition-duration:400ms;transition-timing-function:cubic-bezier(0.25, 0.8, 0.25, 1);transition-property:background-color,visibility}@media(forced-colors: active){.mat-drawer-backdrop{opacity:.5}}.mat-drawer-content{position:relative;z-index:1;display:block;height:100%;overflow:auto}.mat-drawer-content.mat-drawer-content-hidden{opacity:0}.mat-drawer-transition .mat-drawer-content{transition-duration:400ms;transition-timing-function:cubic-bezier(0.25, 0.8, 0.25, 1);transition-property:transform,margin-left,margin-right}.mat-drawer{position:relative;z-index:4;color:var(--mat-sidenav-container-text-color, var(--mat-sys-on-surface-variant));box-shadow:var(--mat-sidenav-container-elevation-shadow, none);background-color:var(--mat-sidenav-container-background-color, var(--mat-sys-surface));border-top-right-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-bottom-right-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));width:var(--mat-sidenav-container-width, 360px);display:block;position:absolute;top:0;bottom:0;z-index:3;outline:0;box-sizing:border-box;overflow-y:auto;transform:translate3d(-100%, 0, 0)}@media(forced-colors: active){.mat-drawer,[dir=rtl] .mat-drawer.mat-drawer-end{border-right:solid 1px currentColor}}@media(forced-colors: active){[dir=rtl] .mat-drawer,.mat-drawer.mat-drawer-end{border-left:solid 1px currentColor;border-right:none}}.mat-drawer.mat-drawer-side{z-index:2}.mat-drawer.mat-drawer-end{right:0;transform:translate3d(100%, 0, 0);border-top-left-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-bottom-left-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-top-right-radius:0;border-bottom-right-radius:0}[dir=rtl] .mat-drawer{border-top-left-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-bottom-left-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-top-right-radius:0;border-bottom-right-radius:0;transform:translate3d(100%, 0, 0)}[dir=rtl] .mat-drawer.mat-drawer-end{border-top-right-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-bottom-right-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-top-left-radius:0;border-bottom-left-radius:0;left:0;right:auto;transform:translate3d(-100%, 0, 0)}.mat-drawer-transition .mat-drawer{transition:transform 400ms cubic-bezier(0.25, 0.8, 0.25, 1)}.mat-drawer:not(.mat-drawer-opened):not(.mat-drawer-animating){visibility:hidden;box-shadow:none}.mat-drawer:not(.mat-drawer-opened):not(.mat-drawer-animating) .mat-drawer-inner-container{display:none}.mat-drawer.mat-drawer-opened.mat-drawer-opened{transform:none}.mat-drawer-side{box-shadow:none;border-right-color:var(--mat-sidenav-container-divider-color, transparent);border-right-width:1px;border-right-style:solid}.mat-drawer-side.mat-drawer-end{border-left-color:var(--mat-sidenav-container-divider-color, transparent);border-left-width:1px;border-left-style:solid;border-right:none}[dir=rtl] .mat-drawer-side{border-left-color:var(--mat-sidenav-container-divider-color, transparent);border-left-width:1px;border-left-style:solid;border-right:none}[dir=rtl] .mat-drawer-side.mat-drawer-end{border-right-color:var(--mat-sidenav-container-divider-color, transparent);border-right-width:1px;border-right-style:solid;border-left:none}.mat-drawer-inner-container{width:100%;height:100%;overflow:auto}.mat-sidenav-fixed{position:fixed} +`;var zpe=new O("MAT_DRAWER_DEFAULT_AUTOSIZE",{providedIn:"root",factory:Upe}),OI=new O("MAT_DRAWER_CONTAINER");function Upe(){return!1}var JC=(()=>{class t extends Xa{_platform=u(Ye);_changeDetectorRef=u(ye);_container=u(AI);constructor(){let e=u(Y),i=u(zs),r=u(ae);super(e,i,r)}ngAfterContentInit(){this._container._contentMarginChanges.subscribe(()=>{this._changeDetectorRef.markForCheck()})}_shouldBeHidden(){if(this._platform.isBrowser)return!1;let{start:e,end:i}=this._container;return e!=null&&e.mode!=="over"&&e.opened||i!=null&&i.mode!=="over"&&i.opened}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-drawer-content"]],hostAttrs:[1,"mat-drawer-content"],hostVars:6,hostBindings:function(i,r){i&2&&(At("margin-left",r._container._contentMargins.left,"px")("margin-right",r._container._contentMargins.right,"px"),G("mat-drawer-content-hidden",r._shouldBeHidden()))},features:[we([{provide:Xa,useExisting:t}]),le],ngContentSelectors:tw,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),ne(0))},encapsulation:2,changeDetection:0})}return t})(),II=(()=>{class t{_elementRef=u(Y);_focusTrapFactory=u(eh);_focusMonitor=u(oi);_platform=u(Ye);_ngZone=u(ae);_renderer=u(ze);_interactivityChecker=u(Dc);_doc=u(_e);_container=u(OI,{optional:!0});_focusTrap=null;_elementFocusedBeforeDrawerWasOpened=null;_eventCleanups;_isAttached;_anchor;get position(){return this._position}set position(e){e=e==="end"?"end":"start",e!==this._position&&(this._isAttached&&this._updatePositionInParent(e),this._position=e,this.onPositionChanged.emit())}_position="start";get mode(){return this._mode}set mode(e){this._mode=e,this._updateFocusTrapState(),this._modeChanged.next()}_mode="over";get disableClose(){return this._disableClose}set disableClose(e){this._disableClose=Vi(e)}_disableClose=!1;get autoFocus(){let e=this._autoFocus;return e??(this.mode==="side"?"dialog":"first-tabbable")}set autoFocus(e){(e==="true"||e==="false"||e==null)&&(e=Vi(e)),this._autoFocus=e}_autoFocus;get opened(){return this._opened()}set opened(e){this.toggle(Vi(e))}_opened=he(!1);_openedVia;_animationStarted=new z;_animationEnd=new z;openedChange=new U(!0);_openedStream=this.openedChange.pipe(ce(e=>e),se(()=>{}));openedStart=this._animationStarted.pipe(ce(()=>this.opened),Zu(void 0));_closedStream=this.openedChange.pipe(ce(e=>!e),se(()=>{}));closedStart=this._animationStarted.pipe(ce(()=>!this.opened),Zu(void 0));_destroyed=new z;onPositionChanged=new U;_content;_modeChanged=new z;_injector=u(de);_changeDetectorRef=u(ye);constructor(){this.openedChange.pipe(xe(this._destroyed)).subscribe(e=>{e?(this._elementFocusedBeforeDrawerWasOpened=this._doc.activeElement,this._takeFocus()):this._isFocusWithinDrawer()&&this._restoreFocus(this._openedVia||"program")}),this._ngZone.runOutsideAngular(()=>{let e=this._elementRef.nativeElement;ol(e,"keydown").pipe(ce(i=>i.keyCode===27&&!this.disableClose&&!Gt(i)),xe(this._destroyed)).subscribe(i=>this._ngZone.run(()=>{this.close(),i.stopPropagation(),i.preventDefault()})),this._eventCleanups=[this._renderer.listen(e,"transitionrun",this._handleTransitionEvent),this._renderer.listen(e,"transitionend",this._handleTransitionEvent),this._renderer.listen(e,"transitioncancel",this._handleTransitionEvent)]}),this._animationEnd.subscribe(()=>{this.openedChange.emit(this.opened)})}_forceFocus(e,i){this._interactivityChecker.isFocusable(e)||(e.tabIndex=-1,this._ngZone.runOutsideAngular(()=>{let r=()=>{o(),a(),e.removeAttribute("tabindex")},o=this._renderer.listen(e,"blur",r),a=this._renderer.listen(e,"mousedown",r)})),e.focus(i)}_focusByCssSelector(e,i){let r=this._elementRef.nativeElement.querySelector(e);r&&this._forceFocus(r,i)}_takeFocus(){if(!this._focusTrap)return;let e=this._elementRef.nativeElement;switch(this.autoFocus){case!1:case"dialog":return;case!0:case"first-tabbable":vt(()=>{!this._focusTrap.focusInitialElement()&&typeof e.focus=="function"&&e.focus()},{injector:this._injector});break;case"first-heading":this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]');break;default:this._focusByCssSelector(this.autoFocus);break}}_restoreFocus(e){this.autoFocus!=="dialog"&&(this._elementFocusedBeforeDrawerWasOpened?this._focusMonitor.focusVia(this._elementFocusedBeforeDrawerWasOpened,e):this._elementRef.nativeElement.blur(),this._elementFocusedBeforeDrawerWasOpened=null)}_isFocusWithinDrawer(){let e=this._doc.activeElement;return!!e&&this._elementRef.nativeElement.contains(e)}ngAfterViewInit(){this._isAttached=!0,this._position==="end"&&this._updatePositionInParent("end"),this._platform.isBrowser&&(this._focusTrap=this._focusTrapFactory.create(this._elementRef.nativeElement),this._updateFocusTrapState())}ngOnDestroy(){this._eventCleanups.forEach(e=>e()),this._focusTrap?.destroy(),this._anchor?.remove(),this._anchor=null,this._animationStarted.complete(),this._animationEnd.complete(),this._modeChanged.complete(),this._destroyed.next(),this._destroyed.complete()}open(e){return this.toggle(!0,e)}close(){return this.toggle(!1)}_closeViaBackdropClick(){return this._setOpen(!1,!0,"mouse")}toggle(e=!this.opened,i){e&&i&&(this._openedVia=i);let r=this._setOpen(e,!e&&this._isFocusWithinDrawer(),this._openedVia||"program");return e||(this._openedVia=null),r}_setOpen(e,i,r){return e===this.opened?Promise.resolve(e?"open":"close"):(this._opened.set(e),this._container?._transitionsEnabled?this._setIsAnimating(!0):setTimeout(()=>{this._animationStarted.next(),this._animationEnd.next()}),this._elementRef.nativeElement.classList.toggle("mat-drawer-opened",e),!e&&i&&this._restoreFocus(r),this._changeDetectorRef.markForCheck(),this._updateFocusTrapState(),new Promise(o=>{this.openedChange.pipe(mt(1)).subscribe(a=>o(a?"open":"close"))}))}_setIsAnimating(e){this._elementRef.nativeElement.classList.toggle("mat-drawer-animating",e)}_getWidth(){return this._elementRef.nativeElement.offsetWidth||0}_updateFocusTrapState(){this._focusTrap&&(this._focusTrap.enabled=!!this._container?.hasBackdrop&&this.opened)}_updatePositionInParent(e){if(!this._platform.isBrowser)return;let i=this._elementRef.nativeElement,r=i.parentNode;e==="end"?(this._anchor||(this._anchor=this._doc.createComment("mat-drawer-anchor"),r.insertBefore(this._anchor,i)),r.appendChild(i)):this._anchor&&this._anchor.parentNode.insertBefore(i,this._anchor)}_handleTransitionEvent=e=>{let i=this._elementRef.nativeElement;e.target===i&&this._ngZone.run(()=>{e.type==="transitionrun"?this._animationStarted.next(e):(e.type==="transitionend"&&this._setIsAnimating(!1),this._animationEnd.next(e))})};static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-drawer"]],viewQuery:function(i,r){if(i&1&&ie(Ope,5),i&2){let o;j(o=H())&&(r._content=o.first)}},hostAttrs:[1,"mat-drawer"],hostVars:12,hostBindings:function(i,r){i&2&&(X("align",null)("tabIndex",r.mode!=="side"?"-1":null),At("visibility",!r._container&&!r.opened?"hidden":null),G("mat-drawer-end",r.position==="end")("mat-drawer-over",r.mode==="over")("mat-drawer-push",r.mode==="push")("mat-drawer-side",r.mode==="side"))},inputs:{position:"position",mode:"mode",disableClose:"disableClose",autoFocus:"autoFocus",opened:"opened"},outputs:{openedChange:"openedChange",_openedStream:"opened",openedStart:"openedStart",_closedStream:"closed",closedStart:"closedStart",onPositionChanged:"positionChanged"},exportAs:["matDrawer"],ngContentSelectors:tw,decls:3,vars:0,consts:[["content",""],["cdkScrollable","",1,"mat-drawer-inner-container"]],template:function(i,r){i&1&&(Ee(),m(0,"div",1,0),ne(2),h())},dependencies:[Xa],encapsulation:2,changeDetection:0})}return t})(),AI=(()=>{class t{_dir=u(Yt,{optional:!0});_element=u(Y);_ngZone=u(ae);_changeDetectorRef=u(ye);_animationDisabled=Qe();_transitionsEnabled=!1;_allDrawers;_drawers=new Dr;_content;_userContent;get start(){return this._start}get end(){return this._end}get autosize(){return this._autosize}set autosize(e){this._autosize=Vi(e)}_autosize=u(zpe);get hasBackdrop(){return this._drawerHasBackdrop(this._start)||this._drawerHasBackdrop(this._end)}set hasBackdrop(e){this._backdropOverride=e==null?null:Vi(e)}_backdropOverride;backdropClick=new U;_start;_end;_left;_right;_destroyed=new z;_doCheckSubject=new z;_contentMargins={left:null,right:null};_contentMarginChanges=new z;get scrollable(){return this._userContent||this._content}_injector=u(de);constructor(){let e=u(Ye),i=u(sr);this._dir?.change.pipe(xe(this._destroyed)).subscribe(()=>{this._validateDrawers(),this.updateContentMargins()}),i.change().pipe(xe(this._destroyed)).subscribe(()=>this.updateContentMargins()),!this._animationDisabled&&e.isBrowser&&this._ngZone.runOutsideAngular(()=>{setTimeout(()=>{this._element.nativeElement.classList.add("mat-drawer-transition"),this._transitionsEnabled=!0},200)})}ngAfterContentInit(){this._allDrawers.changes.pipe(Ue(this._allDrawers),xe(this._destroyed)).subscribe(e=>{this._drawers.reset(e.filter(i=>!i._container||i._container===this)),this._drawers.notifyOnChanges()}),this._drawers.changes.pipe(Ue(null)).subscribe(()=>{this._validateDrawers(),this._drawers.forEach(e=>{this._watchDrawerToggle(e),this._watchDrawerPosition(e),this._watchDrawerMode(e)}),(!this._drawers.length||this._isDrawerOpen(this._start)||this._isDrawerOpen(this._end))&&this.updateContentMargins(),this._changeDetectorRef.markForCheck()}),this._ngZone.runOutsideAngular(()=>{this._doCheckSubject.pipe(Dt(10),xe(this._destroyed)).subscribe(()=>this.updateContentMargins())})}ngOnDestroy(){this._contentMarginChanges.complete(),this._doCheckSubject.complete(),this._drawers.destroy(),this._destroyed.next(),this._destroyed.complete()}open(){this._drawers.forEach(e=>e.open())}close(){this._drawers.forEach(e=>e.close())}updateContentMargins(){let e=0,i=0;if(this._left&&this._left.opened){if(this._left.mode=="side")e+=this._left._getWidth();else if(this._left.mode=="push"){let r=this._left._getWidth();e+=r,i-=r}}if(this._right&&this._right.opened){if(this._right.mode=="side")i+=this._right._getWidth();else if(this._right.mode=="push"){let r=this._right._getWidth();i+=r,e-=r}}e=e||null,i=i||null,(e!==this._contentMargins.left||i!==this._contentMargins.right)&&(this._contentMargins={left:e,right:i},this._ngZone.run(()=>this._contentMarginChanges.next(this._contentMargins)))}ngDoCheck(){this._autosize&&this._isPushed()&&this._ngZone.runOutsideAngular(()=>this._doCheckSubject.next())}_watchDrawerToggle(e){e._animationStarted.pipe(xe(this._drawers.changes)).subscribe(()=>{this.updateContentMargins(),this._changeDetectorRef.markForCheck()}),e.mode!=="side"&&e.openedChange.pipe(xe(this._drawers.changes)).subscribe(()=>this._setContainerClass(e.opened))}_watchDrawerPosition(e){e.onPositionChanged.pipe(xe(this._drawers.changes)).subscribe(()=>{vt({read:()=>this._validateDrawers()},{injector:this._injector})})}_watchDrawerMode(e){e._modeChanged.pipe(xe(it(this._drawers.changes,this._destroyed))).subscribe(()=>{this.updateContentMargins(),this._changeDetectorRef.markForCheck()})}_setContainerClass(e){let i=this._element.nativeElement.classList,r="mat-drawer-container-has-open";e?i.add(r):i.remove(r)}_validateDrawers(){this._start=this._end=null,this._drawers.forEach(e=>{e.position=="end"?(this._end!=null,this._end=e):(this._start!=null,this._start=e)}),this._right=this._left=null,this._dir&&this._dir.value==="rtl"?(this._left=this._end,this._right=this._start):(this._left=this._start,this._right=this._end)}_isPushed(){return this._isDrawerOpen(this._start)&&this._start.mode!="over"||this._isDrawerOpen(this._end)&&this._end.mode!="over"}_onBackdropClicked(){this.backdropClick.emit(),this._closeModalDrawersViaBackdrop()}_closeModalDrawersViaBackdrop(){[this._start,this._end].filter(e=>e&&!e.disableClose&&this._drawerHasBackdrop(e)).forEach(e=>e._closeViaBackdropClick())}_isShowingBackdrop(){return this._isDrawerOpen(this._start)&&this._drawerHasBackdrop(this._start)||this._isDrawerOpen(this._end)&&this._drawerHasBackdrop(this._end)}_isDrawerOpen(e){return e!=null&&e.opened}_drawerHasBackdrop(e){return this._backdropOverride==null?!!e&&e.mode!=="side":this._backdropOverride}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-drawer-container"]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,JC,5),Ce(o,II,5)),i&2){let a;j(a=H())&&(r._content=a.first),j(a=H())&&(r._allDrawers=a)}},viewQuery:function(i,r){if(i&1&&ie(JC,5),i&2){let o;j(o=H())&&(r._userContent=o.first)}},hostAttrs:[1,"mat-drawer-container"],hostVars:2,hostBindings:function(i,r){i&2&&G("mat-drawer-container-explicit-backdrop",r._backdropOverride)},inputs:{autosize:"autosize",hasBackdrop:"hasBackdrop"},outputs:{backdropClick:"backdropClick"},exportAs:["matDrawerContainer"],features:[we([{provide:OI,useExisting:t}])],ngContentSelectors:Ppe,decls:4,vars:2,consts:[[1,"mat-drawer-backdrop",3,"mat-drawer-shown"],[1,"mat-drawer-backdrop",3,"click"]],template:function(i,r){i&1&&(Ee(Rpe),L(0,Fpe,1,2,"div",0),ne(1),ne(2,1),L(3,Npe,2,0,"mat-drawer-content")),i&2&&(V(r.hasBackdrop?0:-1),g(3),V(r._content?-1:3))},dependencies:[JC],styles:[`.mat-drawer-container{position:relative;z-index:1;color:var(--mat-sidenav-content-text-color, var(--mat-sys-on-background));background-color:var(--mat-sidenav-content-background-color, var(--mat-sys-background));box-sizing:border-box;display:block;overflow:hidden}.mat-drawer-container[fullscreen]{top:0;left:0;right:0;bottom:0;position:absolute}.mat-drawer-container[fullscreen].mat-drawer-container-has-open{overflow:hidden}.mat-drawer-container.mat-drawer-container-explicit-backdrop .mat-drawer-side{z-index:3}.mat-drawer-container.ng-animate-disabled .mat-drawer-backdrop,.mat-drawer-container.ng-animate-disabled .mat-drawer-content,.ng-animate-disabled .mat-drawer-container .mat-drawer-backdrop,.ng-animate-disabled .mat-drawer-container .mat-drawer-content{transition:none}.mat-drawer-backdrop{top:0;left:0;right:0;bottom:0;position:absolute;display:block;z-index:3;visibility:hidden}.mat-drawer-backdrop.mat-drawer-shown{visibility:visible;background-color:var(--mat-sidenav-scrim-color, color-mix(in srgb, var(--mat-sys-neutral-variant20) 40%, transparent))}.mat-drawer-transition .mat-drawer-backdrop{transition-duration:400ms;transition-timing-function:cubic-bezier(0.25, 0.8, 0.25, 1);transition-property:background-color,visibility}@media(forced-colors: active){.mat-drawer-backdrop{opacity:.5}}.mat-drawer-content{position:relative;z-index:1;display:block;height:100%;overflow:auto}.mat-drawer-content.mat-drawer-content-hidden{opacity:0}.mat-drawer-transition .mat-drawer-content{transition-duration:400ms;transition-timing-function:cubic-bezier(0.25, 0.8, 0.25, 1);transition-property:transform,margin-left,margin-right}.mat-drawer{position:relative;z-index:4;color:var(--mat-sidenav-container-text-color, var(--mat-sys-on-surface-variant));box-shadow:var(--mat-sidenav-container-elevation-shadow, none);background-color:var(--mat-sidenav-container-background-color, var(--mat-sys-surface));border-top-right-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-bottom-right-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));width:var(--mat-sidenav-container-width, 360px);display:block;position:absolute;top:0;bottom:0;z-index:3;outline:0;box-sizing:border-box;overflow-y:auto;transform:translate3d(-100%, 0, 0)}@media(forced-colors: active){.mat-drawer,[dir=rtl] .mat-drawer.mat-drawer-end{border-right:solid 1px currentColor}}@media(forced-colors: active){[dir=rtl] .mat-drawer,.mat-drawer.mat-drawer-end{border-left:solid 1px currentColor;border-right:none}}.mat-drawer.mat-drawer-side{z-index:2}.mat-drawer.mat-drawer-end{right:0;transform:translate3d(100%, 0, 0);border-top-left-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-bottom-left-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-top-right-radius:0;border-bottom-right-radius:0}[dir=rtl] .mat-drawer{border-top-left-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-bottom-left-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-top-right-radius:0;border-bottom-right-radius:0;transform:translate3d(100%, 0, 0)}[dir=rtl] .mat-drawer.mat-drawer-end{border-top-right-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-bottom-right-radius:var(--mat-sidenav-container-shape, var(--mat-sys-corner-large));border-top-left-radius:0;border-bottom-left-radius:0;left:0;right:auto;transform:translate3d(-100%, 0, 0)}.mat-drawer-transition .mat-drawer{transition:transform 400ms cubic-bezier(0.25, 0.8, 0.25, 1)}.mat-drawer:not(.mat-drawer-opened):not(.mat-drawer-animating){visibility:hidden;box-shadow:none}.mat-drawer:not(.mat-drawer-opened):not(.mat-drawer-animating) .mat-drawer-inner-container{display:none}.mat-drawer.mat-drawer-opened.mat-drawer-opened{transform:none}.mat-drawer-side{box-shadow:none;border-right-color:var(--mat-sidenav-container-divider-color, transparent);border-right-width:1px;border-right-style:solid}.mat-drawer-side.mat-drawer-end{border-left-color:var(--mat-sidenav-container-divider-color, transparent);border-left-width:1px;border-left-style:solid;border-right:none}[dir=rtl] .mat-drawer-side{border-left-color:var(--mat-sidenav-container-divider-color, transparent);border-left-width:1px;border-left-style:solid;border-right:none}[dir=rtl] .mat-drawer-side.mat-drawer-end{border-right-color:var(--mat-sidenav-container-divider-color, transparent);border-right-width:1px;border-right-style:solid;border-left:none}.mat-drawer-inner-container{width:100%;height:100%;overflow:auto}.mat-sidenav-fixed{position:fixed} +`],encapsulation:2,changeDetection:0})}return t})(),ew=(()=>{class t extends JC{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-sidenav-content"]],hostAttrs:[1,"mat-drawer-content","mat-sidenav-content"],features:[we([{provide:Xa,useExisting:t}]),le],ngContentSelectors:tw,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),ne(0))},encapsulation:2,changeDetection:0})}return t})(),RI=(()=>{class t extends II{get fixedInViewport(){return this._fixedInViewport}set fixedInViewport(e){this._fixedInViewport=Vi(e)}_fixedInViewport=!1;get fixedTopGap(){return this._fixedTopGap}set fixedTopGap(e){this._fixedTopGap=Gn(e)}_fixedTopGap=0;get fixedBottomGap(){return this._fixedBottomGap}set fixedBottomGap(e){this._fixedBottomGap=Gn(e)}_fixedBottomGap=0;static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-sidenav"]],hostAttrs:[1,"mat-drawer","mat-sidenav"],hostVars:16,hostBindings:function(i,r){i&2&&(X("tabIndex",r.mode!=="side"?"-1":null)("align",null),At("top",r.fixedInViewport?r.fixedTopGap:null,"px")("bottom",r.fixedInViewport?r.fixedBottomGap:null,"px"),G("mat-drawer-end",r.position==="end")("mat-drawer-over",r.mode==="over")("mat-drawer-push",r.mode==="push")("mat-drawer-side",r.mode==="side")("mat-sidenav-fixed",r.fixedInViewport))},inputs:{fixedInViewport:"fixedInViewport",fixedTopGap:"fixedTopGap",fixedBottomGap:"fixedBottomGap"},exportAs:["matSidenav"],features:[we([{provide:II,useExisting:t}]),le],ngContentSelectors:tw,decls:3,vars:0,consts:[["content",""],["cdkScrollable","",1,"mat-drawer-inner-container"]],template:function(i,r){i&1&&(Ee(),m(0,"div",1,0),ne(2),h())},dependencies:[Xa],encapsulation:2,changeDetection:0})}return t})(),d8=(()=>{class t extends AI{_allDrawers=void 0;_content=void 0;static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-sidenav-container"]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,ew,5),Ce(o,RI,5)),i&2){let a;j(a=H())&&(r._content=a.first),j(a=H())&&(r._allDrawers=a)}},hostAttrs:[1,"mat-drawer-container","mat-sidenav-container"],hostVars:2,hostBindings:function(i,r){i&2&&G("mat-drawer-container-explicit-backdrop",r._backdropOverride)},exportAs:["matSidenavContainer"],features:[we([{provide:OI,useExisting:t},{provide:AI,useExisting:t}]),le],ngContentSelectors:Vpe,decls:4,vars:2,consts:[[1,"mat-drawer-backdrop",3,"mat-drawer-shown"],[1,"mat-drawer-backdrop",3,"click"]],template:function(i,r){i&1&&(Ee(Lpe),L(0,Bpe,1,2,"div",0),ne(1),ne(2,1),L(3,jpe,2,0,"mat-sidenav-content")),i&2&&(V(r.hasBackdrop?0:-1),g(3),V(r._content?-1:3))},dependencies:[ew],styles:[Hpe],encapsulation:2,changeDetection:0})}return t})(),u8=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,Tr,Tr,De]})}return t})();function Wpe(t,n){t&1&&ni(0,"div",3)}function Gpe(t,n){t&1&&(gt(0,"div",4),ni(1,"div",5),yt())}var qpe={min:8,max:100,speed:200,debounceTime:0,trickleSpeed:300,fadeOutSpeed:50,relative:!1,flat:!1,spinner:!1,direction:"ltr+",spinnerPosition:"right",trickleFunc:t=>t>=0&&t<20?10:t>=20&&t<50?4:t>=50&&t<80?2:t>=80&&t<99?.5:0},m8=new O("NG_PROGRESS_OPTIONS",{providedIn:"root",factory:()=>qpe});var iw=(function(t){return t.START="START",t.COMPLETE="COMPLETE",t})(iw||{});function Ype(t){let n=ht(t)||0;return n<100&&n>=0?n:0}function Qpe(t){let n=ht(t)||100;return n>0&&n<=100?n:100}var nw=(()=>{let n=class n{constructor(){this.defaultOptions=u(m8),this.min=re(this.defaultOptions.min,{transform:Ype}),this.max=re(this.defaultOptions.max,{transform:Qpe}),this.speed=re(this.defaultOptions.speed,{transform:ht}),this.trickleSpeed=re(this.defaultOptions.trickleSpeed,{transform:ht}),this.fadeOutSpeed=re(this.defaultOptions.fadeOutSpeed,{transform:ht}),this.debounceTime=re(this.defaultOptions.debounceTime,{transform:ht}),this.trickleFunc=re(this.defaultOptions.trickleFunc),this.config=ci(()=>({max:this.max(),min:this.min(),speed:this.speed(),trickleSpeed:this.trickleSpeed(),fadeOutSpeed:this.fadeOutSpeed(),trickleFunc:this.trickleFunc(),debounceTime:this.debounceTime()})),this._progress=he(0),this._active=he(!1),this.active=ci(()=>this._active()),this.progress=ci(()=>this._progress()),this.started=Ei(),this.completed=Ei(),this._trigger=new rt(null);let i;zr(r=>{let o=this.config();Ni(()=>{i=this._trigger.pipe(ce(a=>!!a),je(a=>a===iw.START?ds(o.debounceTime).pipe(je(()=>this.onTrickling(o))):this.onComplete(o))).subscribe(),r(()=>i?.unsubscribe())})})}start(){this.started.emit(),this._trigger.next(iw.START),this._active.set(!0)}complete(){this._trigger.next(iw.COMPLETE)}inc(i){let r=this.progress();this.active()?(typeof i!="number"&&(i=this.config().trickleFunc(r)),this.set(r+i)):this.start()}set(i){this._active.set(!0),this._progress.set(this.clamp(i))}clamp(i){return Math.max(this.config().min,Math.min(this.config().max,i))}onTrickling(i){return this.active()||this.set(i.min),ds(0,i.trickleSpeed).pipe(He(()=>this.inc()))}onComplete(i){return this.active()?(this.completed.emit(),Q({}).pipe(He(()=>this._progress.set(100)),Oa(i.speed+140),He(()=>this._active.set(!1)),Oa(i.fadeOutSpeed),Xr(()=>this._progress.set(0)),xe(wz(this.started)))):zi}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","ngProgressRef",""]],inputs:{min:[1,"min"],max:[1,"max"],speed:[1,"speed"],trickleSpeed:[1,"trickleSpeed"],fadeOutSpeed:[1,"fadeOutSpeed"],debounceTime:[1,"debounceTime"],trickleFunc:[1,"trickleFunc"]},outputs:{started:"started",completed:"completed"},exportAs:["ngProgressRef"]});let t=n;return t})(),h8=(()=>{let n=class n{constructor(){this.config=u(m8),this.progressRef=u(nw,{host:!0,self:!0}),this.flat=re(this.config.flat,{transform:B}),this.spinner=re(this.config.spinner,{transform:B}),this.relative=re(this.config.relative,{transform:B}),this.spinnerPosition=re(this.config.spinnerPosition),this.direction=re(this.config.direction),this.progressTransform=ci(()=>`translate3d(${this.progressRef.progress()}%,0,0)`)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["ng-progress"]],hostAttrs:["role","progressbar"],hostVars:12,hostBindings:function(r,o){r&2&&(X("spinnerPosition",o.spinnerPosition())("direction",o.direction()),At("--_ng-progress-speed",o.progressRef.speed()+"ms")("--_ng-progress-fade-out-speed",o.progressRef.fadeOutSpeed()+"ms"),G("ng-progress-bar",!0)("ng-progress-bar-active",o.progressRef.active())("ng-progress-bar-relative",o.relative()))},inputs:{flat:[1,"flat"],spinner:[1,"spinner"],relative:[1,"relative"],spinnerPosition:[1,"spinnerPosition"],direction:[1,"direction"]},exportAs:["ngProgress"],features:[Mm([{directive:nw,inputs:["min","min","max","max","speed","speed","trickleSpeed","trickleSpeed","fadeOutSpeed","fadeOutSpeed","debounceTime","debounceTime"],outputs:["started","started","completed","completed"]}])],decls:5,vars:4,consts:[[1,"ng-progress-bar-wrapper"],[1,"ng-bar-placeholder"],[1,"ng-bar"],[1,"ng-meteor"],[1,"ng-spinner"],[1,"ng-spinner-icon"]],template:function(r,o){r&1&&(gt(0,"div",0)(1,"div",1)(2,"div",2),L(3,Wpe,1,0,"div",3),yt()(),L(4,Gpe,2,0,"div",4),yt()),r&2&&(g(2),At("transform",o.progressTransform()),g(),V(o.flat()?-1:3),g(),V(o.spinner()?4:-1))},styles:['[_nghost-%COMP%]{--_ng-progress-thickness: var(--ng-progress-thickness, 2);--_ng-progress-thickness-px: calc(var(--_ng-progress-thickness) * 1px);--_ng-progress-thickness-add-one: calc(var(--_ng-progress-thickness) + 1);--_ng-progress-meteor-position-px: calc(var(--_ng-progress-thickness-add-one) * -1px);--_ng-progress-color: var(--ng-progress-color, #1B95E0);--_ng-progress-holder-color: var(--ng-progress-holder-color, transparent);--_ng-progress-ease: var(--ng-progress-ease, linear);--_ng-progress-spinner-thickness: var(--ng-progress-spinner-thickness, 2);--_ng-progress-spinner-thickness-px: calc(var(--_ng-progress-spinner-thickness) * 1px);--_ng-progress-spinner-spacing: var(--ng-progress-spinner-spacing, 15);--_ng-progress-spinner-spacing-px: calc(var(--_ng-progress-spinner-spacing) * 1px);--_ng-progress-spinner-size: var(--ng-progress-spinner-size, 18);--_ng-progress-spinner-size-px: calc(var(--_ng-progress-spinner-size) * 1px);--_ng-progress-spinner-speed: var(--ng-progress-spinner-speed, .25s);z-index:999999;pointer-events:none}.ng-progress-bar-active[_nghost-%COMP%] .ng-progress-bar-wrapper[_ngcontent-%COMP%]{filter:alpha(opacity=100);opacity:1;transition:none}.ng-progress-bar-active[_nghost-%COMP%] .ng-bar[_ngcontent-%COMP%]{transition:all var(--_ng-progress-speed) var(--_ng-progress-ease)}.ng-progress-bar-relative[_nghost-%COMP%] .ng-progress-bar-wrapper[_ngcontent-%COMP%]{position:relative}[direction="ltr+"][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%], [direction=ltr-][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%]{rotate:calc(var(--_ng-progress-thickness-add-one) * 1deg)}[direction="ltr+"][_nghost-%COMP%] .ng-bar[_ngcontent-%COMP%], [direction="rtl+"][_nghost-%COMP%] .ng-bar[_ngcontent-%COMP%]{margin-left:-100%}[direction="ltr+"][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%], [direction="rtl+"][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%]{right:0}[direction="ltr+"][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%], [direction=rtl-][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%]{top:var(--_ng-progress-meteor-position-px)}[direction=ltr-][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%], [direction="rtl+"][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%]{bottom:var(--_ng-progress-meteor-position-px)}[direction=ltr-][_nghost-%COMP%] .ng-bar-placeholder[_ngcontent-%COMP%], [direction="rtl+"][_nghost-%COMP%] .ng-bar-placeholder[_ngcontent-%COMP%]{transform:rotate(180deg)}[direction=ltr-][_nghost-%COMP%] .ng-spinner-icon[_ngcontent-%COMP%], [direction="rtl+"][_nghost-%COMP%] .ng-spinner-icon[_ngcontent-%COMP%]{animation-direction:reverse}[direction="rtl+"][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%], [direction=rtl-][_nghost-%COMP%] .ng-meteor[_ngcontent-%COMP%]{rotate:calc(var(--_ng-progress-thickness-add-one) * -1deg)}[_nghost-%COMP%] .ng-spinner[_ngcontent-%COMP%]{top:var(--_ng-progress-spinner-spacing-px)}[spinnerPosition=left][_nghost-%COMP%] .ng-spinner[_ngcontent-%COMP%]{left:var(--_ng-progress-spinner-spacing-px)}[spinnerPosition=right][_nghost-%COMP%] .ng-spinner[_ngcontent-%COMP%]{right:var(--_ng-progress-spinner-spacing-px)}.ng-progress-bar-wrapper[_ngcontent-%COMP%]{position:fixed;z-index:999999;top:0;left:0;width:100%;transform:scale(1);filter:alpha(opacity=0);opacity:0;transition:opacity var(--_ng-progress-fade-out-speed) linear}.ng-bar-placeholder[_ngcontent-%COMP%]{position:absolute;height:var(--_ng-progress-thickness-px);width:100%}.ng-bar[_ngcontent-%COMP%]{width:100%;height:100%;transform:translate(-100%,0,0);background:var(--_ng-progress-color)}.ng-meteor[_ngcontent-%COMP%]{display:block;position:absolute;width:100px;height:100%;opacity:1;box-shadow:0 0 10px var(--_ng-progress-color),0 0 5px var(--_ng-progress-color)}.ng-spinner[_ngcontent-%COMP%]{position:absolute;display:block;z-index:1031;top:10px}.ng-spinner-icon[_ngcontent-%COMP%]{box-sizing:border-box;animation:_ngcontent-%COMP%_spinner-animation var(--_ng-progress-spinner-speed) linear infinite;border-style:solid;border-color:transparent;border-radius:50%;border-top-color:var(--_ng-progress-color);border-left-color:var(--_ng-progress-color);width:var(--_ng-progress-spinner-size-px);height:var(--_ng-progress-spinner-size-px);border-width:var(--_ng-progress-spinner-thickness-px)}@keyframes _ngcontent-%COMP%_spinner-animation{0%{transform:rotate(0)}to{transform:rotate(360deg)}}'],changeDetection:0});let t=n;return t})();var Kpe={minDuration:0,startEvents:[As],completeEvents:[Si,ea,vc]},Zpe=new O("NG_PROGRESS_ROUTER_OPTIONS",{providedIn:"root",factory:()=>Kpe});function p8(t,n){return n.some(e=>t instanceof e)}var Xpe=(()=>{let n=class n{constructor(){this.router=u(Ae),this.config=u(Zpe),this.progressRef=u(nw,{host:!0,self:!0});let i;this.router.events.pipe(ce(r=>p8(r,[...this.config.startEvents,...this.config.completeEvents])),se(r=>p8(r,this.config.startEvents)),He(r=>{clearTimeout(i),r?this.progressRef.start():i=setTimeout(()=>{this.progressRef.complete()},this.config.minDuration)}),Ru()).subscribe()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n});let t=n;return t})(),f8=(()=>{let n=class n extends Xpe{};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275dir=P({type:n,selectors:[["ng-progress","ngProgressRouter",""]],features:[le]});let t=n;return t})();var Jpe=["*",[["mat-toolbar-row"]]],efe=["*","mat-toolbar-row"],tfe=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-toolbar-row"]],hostAttrs:[1,"mat-toolbar-row"],exportAs:["matToolbarRow"]})}return t})(),rw=(()=>{class t{_elementRef=u(Y);_platform=u(Ye);_document=u(_e);color;_toolbarRows;constructor(){}ngAfterViewInit(){this._platform.isBrowser&&(this._checkToolbarMixedModes(),this._toolbarRows.changes.subscribe(()=>this._checkToolbarMixedModes()))}_checkToolbarMixedModes(){this._toolbarRows.length}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-toolbar"]],contentQueries:function(i,r,o){if(i&1&&Ce(o,tfe,5),i&2){let a;j(a=H())&&(r._toolbarRows=a)}},hostAttrs:[1,"mat-toolbar"],hostVars:6,hostBindings:function(i,r){i&2&&(at(r.color?"mat-"+r.color:""),G("mat-toolbar-multiple-rows",r._toolbarRows.length>0)("mat-toolbar-single-row",r._toolbarRows.length===0))},inputs:{color:"color"},exportAs:["matToolbar"],ngContentSelectors:efe,decls:2,vars:0,template:function(i,r){i&1&&(Ee(Jpe),ne(0),ne(1,1))},styles:[`.mat-toolbar{background:var(--mat-toolbar-container-background-color, var(--mat-sys-surface));color:var(--mat-toolbar-container-text-color, var(--mat-sys-on-surface))}.mat-toolbar,.mat-toolbar h1,.mat-toolbar h2,.mat-toolbar h3,.mat-toolbar h4,.mat-toolbar h5,.mat-toolbar h6{font-family:var(--mat-toolbar-title-text-font, var(--mat-sys-title-large-font));font-size:var(--mat-toolbar-title-text-size, var(--mat-sys-title-large-size));line-height:var(--mat-toolbar-title-text-line-height, var(--mat-sys-title-large-line-height));font-weight:var(--mat-toolbar-title-text-weight, var(--mat-sys-title-large-weight));letter-spacing:var(--mat-toolbar-title-text-tracking, var(--mat-sys-title-large-tracking));margin:0}@media(forced-colors: active){.mat-toolbar{outline:solid 1px}}.mat-toolbar .mat-form-field-underline,.mat-toolbar .mat-form-field-ripple,.mat-toolbar .mat-focused .mat-form-field-ripple{background-color:currentColor}.mat-toolbar .mat-form-field-label,.mat-toolbar .mat-focused .mat-form-field-label,.mat-toolbar .mat-select-value,.mat-toolbar .mat-select-arrow,.mat-toolbar .mat-form-field.mat-focused .mat-select-arrow{color:inherit}.mat-toolbar .mat-input-element{caret-color:currentColor}.mat-toolbar .mat-mdc-button-base.mat-mdc-button-base.mat-unthemed{--mat-button-text-label-text-color: var(--mat-toolbar-container-text-color, var(--mat-sys-on-surface));--mat-button-outlined-label-text-color: var(--mat-toolbar-container-text-color, var(--mat-sys-on-surface))}.mat-toolbar-row,.mat-toolbar-single-row{display:flex;box-sizing:border-box;padding:0 16px;width:100%;flex-direction:row;align-items:center;white-space:nowrap;height:var(--mat-toolbar-standard-height, 64px)}@media(max-width: 599px){.mat-toolbar-row,.mat-toolbar-single-row{height:var(--mat-toolbar-mobile-height, 56px)}}.mat-toolbar-multiple-rows{display:flex;box-sizing:border-box;flex-direction:column;width:100%;min-height:var(--mat-toolbar-standard-height, 64px)}@media(max-width: 599px){.mat-toolbar-multiple-rows{min-height:var(--mat-toolbar-mobile-height, 56px)}} +`],encapsulation:2,changeDetection:0})}return t})();var ow=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,De]})}return t})();var _8=[["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror"],["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror"],["webkitRequestFullScreen","webkitCancelFullScreen","webkitCurrentFullScreenElement","webkitCancelFullScreen","webkitfullscreenchange","webkitfullscreenerror"],["mozRequestFullScreen","mozCancelFullScreen","mozFullScreenElement","mozFullScreenEnabled","mozfullscreenchange","mozfullscreenerror"],["msRequestFullscreen","msExitFullscreen","msFullscreenElement","msFullscreenEnabled","MSFullscreenChange","MSFullscreenError"]],jl=(()=>{if(typeof document>"u")return!1;let t=_8[0],n={};for(let e of _8)if(e?.[1]in document){for(let[r,o]of e.entries())n[t[r]]=o;return n}return!1})(),b8={change:jl.fullscreenchange,error:jl.fullscreenerror},Vo={request(t=document.documentElement,n){return new Promise((e,i)=>{let r=()=>{Vo.off("change",r),e()};Vo.on("change",r);let o=t[jl.requestFullscreen](n);o instanceof Promise&&o.then(r).catch(i)})},exit(){return new Promise((t,n)=>{if(!Vo.isFullscreen){t();return}let e=()=>{Vo.off("change",e),t()};Vo.on("change",e);let i=document[jl.exitFullscreen]();i instanceof Promise&&i.then(e).catch(n)})},toggle(t,n){return Vo.isFullscreen?Vo.exit():Vo.request(t,n)},onchange(t){Vo.on("change",t)},onerror(t){Vo.on("error",t)},on(t,n){let e=b8[t];e&&document.addEventListener(e,n,!1)},off(t,n){let e=b8[t];e&&document.removeEventListener(e,n,!1)},raw:jl};Object.defineProperties(Vo,{isFullscreen:{get:()=>!!document[jl.fullscreenElement]},element:{enumerable:!0,get:()=>document[jl.fullscreenElement]??void 0},isEnabled:{enumerable:!0,get:()=>!!document[jl.fullscreenEnabled]}});jl||(Vo={isEnabled:!1});var PI=Vo;function ife(t,n){t&1&&(gt(0,"span",2),f(1,"Talent Management"),yt())}var aw=(()=>{let n=class n{constructor(){this.showName=re(!0)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-branding"]],inputs:{showName:[1,"showName"]},decls:3,vars:1,consts:[["href","/",1,"branding"],["src","images/matero.png","alt","logo",1,"branding-logo"],[1,"branding-name"]],template:function(r,o){r&1&&(gt(0,"a",0),ni(1,"img",1),L(2,ife,2,0,"span",2),yt()),r&2&&(g(2),V(o.showName()?2:-1))},styles:[".branding[_ngcontent-%COMP%]{display:flex;align-items:center;margin:0 .5rem;text-decoration:none;white-space:nowrap;color:inherit;border-radius:50rem}.branding-logo[_ngcontent-%COMP%]{width:2rem;height:2rem;border-radius:50rem}.branding-name[_ngcontent-%COMP%]{margin:0 .5rem;font-size:1rem;font-weight:500}"]});let t=n;return t})();var v8=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-github-button"]],decls:3,vars:0,consts:[["matIconButton","","href","https://github.com/ng-matero/ng-matero","target","_blank"],["viewBox","0 0 16 16"],["fill","currentColor","d",`M7.999,0.431c-4.285,0-7.76,3.474-7.76,7.761 c0,3.428,2.223,6.337,5.307,7.363c0.388, + 0.071,0.53-0.168,0.53-0.374c0-0.184-0.007-0.672-0.01-1.32 c-2.159, + 0.469-2.614-1.04-2.614-1.04c-0.353-0.896-0.862-1.135-0.862-1.135c-0.705-0.481, + 0.053-0.472,0.053-0.472 c0.779,0.055,1.189,0.8,1.189,0.8c0.692,1.186,1.816,0.843,2.258, + 0.645c0.071-0.502,0.271-0.843,0.493-1.037 C4.86,11.425,3.049,10.76,3.049,7.786c0-0.847, + 0.302-1.54,0.799-2.082C3.768,5.507,3.501,4.718,3.924,3.65 c0,0,0.652-0.209, + 2.134,0.796C6.677,4.273,7.34,4.187,8,4.184c0.659,0.003,1.323,0.089,1.943, + 0.261 c1.482-1.004,2.132-0.796,2.132-0.796c0.423,1.068,0.157,1.857,0.077,2.054c0.497, + 0.542,0.798,1.235,0.798,2.082 c0,2.981-1.814,3.637-3.543,3.829c0.279,0.24,0.527,0.713, + 0.527,1.437c0,1.037-0.01,1.874-0.01,2.129 c0,0.208,0.14,0.449,0.534,0.373c3.081-1.028, + 5.302-3.935,5.302-7.362C15.76,3.906,12.285,0.431,7.999,0.431z`]],template:function(r,o){r&1&&(m(0,"a",0),ii(),m(1,"svg",1),M(2,"path",2),h()())},dependencies:[Fe,Ft],encapsulation:2});let t=n;return t})();var y8="mat-badge-content",nfe=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["ng-component"]],decls:0,vars:0,template:function(i,r){},styles:[`.mat-badge{position:relative}.mat-badge.mat-badge{overflow:visible}.mat-badge-content{position:absolute;text-align:center;display:inline-block;transition:transform 200ms ease-in-out;transform:scale(0.6);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;box-sizing:border-box;pointer-events:none;background-color:var(--mat-badge-background-color, var(--mat-sys-error));color:var(--mat-badge-text-color, var(--mat-sys-on-error));font-family:var(--mat-badge-text-font, var(--mat-sys-label-small-font));font-weight:var(--mat-badge-text-weight, var(--mat-sys-label-small-weight));border-radius:var(--mat-badge-container-shape, var(--mat-sys-corner-full))}.mat-badge-above .mat-badge-content{bottom:100%}.mat-badge-below .mat-badge-content{top:100%}.mat-badge-before .mat-badge-content{right:100%}[dir=rtl] .mat-badge-before .mat-badge-content{right:auto;left:100%}.mat-badge-after .mat-badge-content{left:100%}[dir=rtl] .mat-badge-after .mat-badge-content{left:auto;right:100%}@media(forced-colors: active){.mat-badge-content{outline:solid 1px;border-radius:0}}.mat-badge-disabled .mat-badge-content{background-color:var(--mat-badge-disabled-state-background-color, color-mix(in srgb, var(--mat-sys-error) 38%, transparent));color:var(--mat-badge-disabled-state-text-color, var(--mat-sys-on-error))}.mat-badge-hidden .mat-badge-content{display:none}.ng-animate-disabled .mat-badge-content,.mat-badge-content._mat-animation-noopable{transition:none}.mat-badge-content.mat-badge-active{transform:none}.mat-badge-small .mat-badge-content{width:var(--mat-badge-legacy-small-size-container-size, unset);height:var(--mat-badge-legacy-small-size-container-size, unset);min-width:var(--mat-badge-small-size-container-size, 6px);min-height:var(--mat-badge-small-size-container-size, 6px);line-height:var(--mat-badge-small-size-line-height, 6px);padding:var(--mat-badge-small-size-container-padding, 0);font-size:var(--mat-badge-small-size-text-size, 0);margin:var(--mat-badge-small-size-container-offset, -6px 0)}.mat-badge-small.mat-badge-overlap .mat-badge-content{margin:var(--mat-badge-small-size-container-overlap-offset, -6px)}.mat-badge-medium .mat-badge-content{width:var(--mat-badge-legacy-container-size, unset);height:var(--mat-badge-legacy-container-size, unset);min-width:var(--mat-badge-container-size, 16px);min-height:var(--mat-badge-container-size, 16px);line-height:var(--mat-badge-line-height, 16px);padding:var(--mat-badge-container-padding, 0 4px);font-size:var(--mat-badge-text-size, var(--mat-sys-label-small-size));margin:var(--mat-badge-container-offset, -12px 0)}.mat-badge-medium.mat-badge-overlap .mat-badge-content{margin:var(--mat-badge-container-overlap-offset, -12px)}.mat-badge-large .mat-badge-content{width:var(--mat-badge-legacy-large-size-container-size, unset);height:var(--mat-badge-legacy-large-size-container-size, unset);min-width:var(--mat-badge-large-size-container-size, 16px);min-height:var(--mat-badge-large-size-container-size, 16px);line-height:var(--mat-badge-large-size-line-height, 16px);padding:var(--mat-badge-large-size-container-padding, 0 4px);font-size:var(--mat-badge-large-size-text-size, var(--mat-sys-label-small-size));margin:var(--mat-badge-large-size-container-offset, -12px 0)}.mat-badge-large.mat-badge-overlap .mat-badge-content{margin:var(--mat-badge-large-size-container-overlap-offset, -12px)} +`],encapsulation:2,changeDetection:0})}return t})(),x8=(()=>{class t{_ngZone=u(ae);_elementRef=u(Y);_ariaDescriber=u(nh);_renderer=u(ze);_animationsDisabled=Qe();_idGenerator=u(et);get color(){return this._color}set color(e){this._setColor(e),this._color=e}_color="primary";overlap=!0;disabled;position="above after";get content(){return this._content}set content(e){this._updateRenderedContent(e)}_content;get description(){return this._description}set description(e){this._updateDescription(e)}_description;size="medium";hidden;_badgeElement;_inlineBadgeDescription;_isInitialized=!1;_interactivityChecker=u(Dc);_document=u(_e);constructor(){let e=u(ft);e.load(nfe),e.load(ro)}isAbove(){return this.position.indexOf("below")===-1}isAfter(){return this.position.indexOf("before")===-1}getBadgeElement(){return this._badgeElement}ngOnInit(){this._clearExistingBadges(),this.content&&!this._badgeElement&&(this._badgeElement=this._createBadgeElement(),this._updateRenderedContent(this.content)),this._isInitialized=!0}ngOnDestroy(){this._renderer.destroyNode&&(this._renderer.destroyNode(this._badgeElement),this._inlineBadgeDescription?.remove()),this._ariaDescriber.removeDescription(this._elementRef.nativeElement,this.description)}_isHostInteractive(){return this._interactivityChecker.isFocusable(this._elementRef.nativeElement,{ignoreVisibility:!0})}_createBadgeElement(){let e=this._renderer.createElement("span"),i="mat-badge-active";return e.setAttribute("id",this._idGenerator.getId("mat-badge-content-")),e.setAttribute("aria-hidden","true"),e.classList.add(y8),this._animationsDisabled&&e.classList.add("_mat-animation-noopable"),this._elementRef.nativeElement.appendChild(e),typeof requestAnimationFrame=="function"&&!this._animationsDisabled?this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>{e.classList.add(i)})}):e.classList.add(i),e}_updateRenderedContent(e){let i=`${e??""}`.trim();this._isInitialized&&i&&!this._badgeElement&&(this._badgeElement=this._createBadgeElement()),this._badgeElement&&(this._badgeElement.textContent=i),this._content=i}_updateDescription(e){this._ariaDescriber.removeDescription(this._elementRef.nativeElement,this.description),(!e||this._isHostInteractive())&&this._removeInlineDescription(),this._description=e,this._isHostInteractive()?this._ariaDescriber.describe(this._elementRef.nativeElement,e):this._updateInlineDescription()}_updateInlineDescription(){this._inlineBadgeDescription||(this._inlineBadgeDescription=this._document.createElement("span"),this._inlineBadgeDescription.classList.add("cdk-visually-hidden")),this._inlineBadgeDescription.textContent=this.description,this._badgeElement?.appendChild(this._inlineBadgeDescription)}_removeInlineDescription(){this._inlineBadgeDescription?.remove(),this._inlineBadgeDescription=void 0}_setColor(e){let i=this._elementRef.nativeElement.classList;i.remove(`mat-badge-${this._color}`),e&&i.add(`mat-badge-${e}`)}_clearExistingBadges(){let e=this._elementRef.nativeElement.querySelectorAll(`:scope > .${y8}`);for(let i of Array.from(e))i!==this._badgeElement&&i.remove()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matBadge",""]],hostAttrs:[1,"mat-badge"],hostVars:20,hostBindings:function(i,r){i&2&&G("mat-badge-overlap",r.overlap)("mat-badge-above",r.isAbove())("mat-badge-below",!r.isAbove())("mat-badge-before",!r.isAfter())("mat-badge-after",r.isAfter())("mat-badge-small",r.size==="small")("mat-badge-medium",r.size==="medium")("mat-badge-large",r.size==="large")("mat-badge-hidden",r.hidden||!r.content)("mat-badge-disabled",r.disabled)},inputs:{color:[0,"matBadgeColor","color"],overlap:[2,"matBadgeOverlap","overlap",B],disabled:[2,"matBadgeDisabled","disabled",B],position:[0,"matBadgePosition","position"],content:[0,"matBadge","content"],description:[0,"matBadgeDescription","description"],size:[0,"matBadgeSize","size"],hidden:[2,"matBadgeHidden","hidden",B]}})}return t})(),C8=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[El,De,De]})}return t})();var Kr=(()=>{class t{get vertical(){return this._vertical}set vertical(e){this._vertical=Vi(e)}_vertical=!1;get inset(){return this._inset}set inset(e){this._inset=Vi(e)}_inset=!1;static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-divider"]],hostAttrs:["role","separator",1,"mat-divider"],hostVars:7,hostBindings:function(i,r){i&2&&(X("aria-orientation",r.vertical?"vertical":"horizontal"),G("mat-divider-vertical",r.vertical)("mat-divider-horizontal",!r.vertical)("mat-divider-inset",r.inset))},inputs:{vertical:"vertical",inset:"inset"},decls:0,vars:0,template:function(i,r){},styles:[`.mat-divider{display:block;margin:0;border-top-style:solid;border-top-color:var(--mat-divider-color, var(--mat-sys-outline-variant));border-top-width:var(--mat-divider-width, 1px)}.mat-divider.mat-divider-vertical{border-top:0;border-right-style:solid;border-right-color:var(--mat-divider-color, var(--mat-sys-outline-variant));border-right-width:var(--mat-divider-width, 1px)}.mat-divider.mat-divider-inset{margin-left:80px}[dir=rtl] .mat-divider.mat-divider-inset{margin-left:auto;margin-right:80px} +`],encapsulation:2,changeDetection:0})}return t})(),Nr=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,De]})}return t})();var w8=["*"],D8=`.mdc-list{margin:0;padding:8px 0;list-style-type:none}.mdc-list:focus{outline:none}.mdc-list-item{display:flex;position:relative;justify-content:flex-start;overflow:hidden;padding:0;align-items:stretch;cursor:pointer;padding-left:16px;padding-right:16px;background-color:var(--mat-list-list-item-container-color, transparent);border-radius:var(--mat-list-list-item-container-shape, var(--mat-sys-corner-none))}.mdc-list-item.mdc-list-item--selected{background-color:var(--mat-list-list-item-selected-container-color)}.mdc-list-item:focus{outline:0}.mdc-list-item.mdc-list-item--disabled{cursor:auto}.mdc-list-item.mdc-list-item--with-one-line{height:var(--mat-list-list-item-one-line-container-height, 48px)}.mdc-list-item.mdc-list-item--with-one-line .mdc-list-item__start{align-self:center;margin-top:0}.mdc-list-item.mdc-list-item--with-one-line .mdc-list-item__end{align-self:center;margin-top:0}.mdc-list-item.mdc-list-item--with-two-lines{height:var(--mat-list-list-item-two-line-container-height, 64px)}.mdc-list-item.mdc-list-item--with-two-lines .mdc-list-item__start{align-self:flex-start;margin-top:16px}.mdc-list-item.mdc-list-item--with-two-lines .mdc-list-item__end{align-self:center;margin-top:0}.mdc-list-item.mdc-list-item--with-three-lines{height:var(--mat-list-list-item-three-line-container-height, 88px)}.mdc-list-item.mdc-list-item--with-three-lines .mdc-list-item__start{align-self:flex-start;margin-top:16px}.mdc-list-item.mdc-list-item--with-three-lines .mdc-list-item__end{align-self:flex-start;margin-top:16px}.mdc-list-item.mdc-list-item--selected::before,.mdc-list-item.mdc-list-item--selected:focus::before,.mdc-list-item:not(.mdc-list-item--selected):focus::before{position:absolute;box-sizing:border-box;width:100%;height:100%;top:0;left:0;content:"";pointer-events:none}a.mdc-list-item{color:inherit;text-decoration:none}.mdc-list-item__start{fill:currentColor;flex-shrink:0;pointer-events:none}.mdc-list-item--with-leading-icon .mdc-list-item__start{color:var(--mat-list-list-item-leading-icon-color, var(--mat-sys-on-surface-variant));width:var(--mat-list-list-item-leading-icon-size, 24px);height:var(--mat-list-list-item-leading-icon-size, 24px);margin-left:16px;margin-right:32px}[dir=rtl] .mdc-list-item--with-leading-icon .mdc-list-item__start{margin-left:32px;margin-right:16px}.mdc-list-item--with-leading-icon:hover .mdc-list-item__start{color:var(--mat-list-list-item-hover-leading-icon-color)}.mdc-list-item--with-leading-avatar .mdc-list-item__start{width:var(--mat-list-list-item-leading-avatar-size, 40px);height:var(--mat-list-list-item-leading-avatar-size, 40px);margin-left:16px;margin-right:16px;border-radius:50%}.mdc-list-item--with-leading-avatar .mdc-list-item__start,[dir=rtl] .mdc-list-item--with-leading-avatar .mdc-list-item__start{margin-left:16px;margin-right:16px;border-radius:50%}.mdc-list-item__end{flex-shrink:0;pointer-events:none}.mdc-list-item--with-trailing-meta .mdc-list-item__end{font-family:var(--mat-list-list-item-trailing-supporting-text-font, var(--mat-sys-label-small-font));line-height:var(--mat-list-list-item-trailing-supporting-text-line-height, var(--mat-sys-label-small-line-height));font-size:var(--mat-list-list-item-trailing-supporting-text-size, var(--mat-sys-label-small-size));font-weight:var(--mat-list-list-item-trailing-supporting-text-weight, var(--mat-sys-label-small-weight));letter-spacing:var(--mat-list-list-item-trailing-supporting-text-tracking, var(--mat-sys-label-small-tracking))}.mdc-list-item--with-trailing-icon .mdc-list-item__end{color:var(--mat-list-list-item-trailing-icon-color, var(--mat-sys-on-surface-variant));width:var(--mat-list-list-item-trailing-icon-size, 24px);height:var(--mat-list-list-item-trailing-icon-size, 24px)}.mdc-list-item--with-trailing-icon:hover .mdc-list-item__end{color:var(--mat-list-list-item-hover-trailing-icon-color)}.mdc-list-item.mdc-list-item--with-trailing-meta .mdc-list-item__end{color:var(--mat-list-list-item-trailing-supporting-text-color, var(--mat-sys-on-surface-variant))}.mdc-list-item--selected.mdc-list-item--with-trailing-icon .mdc-list-item__end{color:var(--mat-list-list-item-selected-trailing-icon-color, var(--mat-sys-primary))}.mdc-list-item__content{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;align-self:center;flex:1;pointer-events:none}.mdc-list-item--with-two-lines .mdc-list-item__content,.mdc-list-item--with-three-lines .mdc-list-item__content{align-self:stretch}.mdc-list-item__primary-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;color:var(--mat-list-list-item-label-text-color, var(--mat-sys-on-surface));font-family:var(--mat-list-list-item-label-text-font, var(--mat-sys-body-large-font));line-height:var(--mat-list-list-item-label-text-line-height, var(--mat-sys-body-large-line-height));font-size:var(--mat-list-list-item-label-text-size, var(--mat-sys-body-large-size));font-weight:var(--mat-list-list-item-label-text-weight, var(--mat-sys-body-large-weight));letter-spacing:var(--mat-list-list-item-label-text-tracking, var(--mat-sys-body-large-tracking))}.mdc-list-item:hover .mdc-list-item__primary-text{color:var(--mat-list-list-item-hover-label-text-color, var(--mat-sys-on-surface))}.mdc-list-item:focus .mdc-list-item__primary-text{color:var(--mat-list-list-item-focus-label-text-color, var(--mat-sys-on-surface))}.mdc-list-item--with-two-lines .mdc-list-item__primary-text,.mdc-list-item--with-three-lines .mdc-list-item__primary-text{display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before,.mdc-list-item--with-three-lines .mdc-list-item__primary-text::before{display:inline-block;width:0;height:28px;content:"";vertical-align:0}.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after,.mdc-list-item--with-three-lines .mdc-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-list-item__secondary-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;margin-top:0;color:var(--mat-list-list-item-supporting-text-color, var(--mat-sys-on-surface-variant));font-family:var(--mat-list-list-item-supporting-text-font, var(--mat-sys-body-medium-font));line-height:var(--mat-list-list-item-supporting-text-line-height, var(--mat-sys-body-medium-line-height));font-size:var(--mat-list-list-item-supporting-text-size, var(--mat-sys-body-medium-size));font-weight:var(--mat-list-list-item-supporting-text-weight, var(--mat-sys-body-medium-weight));letter-spacing:var(--mat-list-list-item-supporting-text-tracking, var(--mat-sys-body-medium-tracking))}.mdc-list-item__secondary-text::before{display:inline-block;width:0;height:20px;content:"";vertical-align:0}.mdc-list-item--with-three-lines .mdc-list-item__secondary-text{white-space:normal;line-height:20px}.mdc-list-item--with-overline .mdc-list-item__secondary-text{white-space:nowrap;line-height:auto}.mdc-list-item--with-leading-radio.mdc-list-item,.mdc-list-item--with-leading-checkbox.mdc-list-item,.mdc-list-item--with-leading-icon.mdc-list-item,.mdc-list-item--with-leading-avatar.mdc-list-item{padding-left:0;padding-right:16px}[dir=rtl] .mdc-list-item--with-leading-radio.mdc-list-item,[dir=rtl] .mdc-list-item--with-leading-checkbox.mdc-list-item,[dir=rtl] .mdc-list-item--with-leading-icon.mdc-list-item,[dir=rtl] .mdc-list-item--with-leading-avatar.mdc-list-item{padding-left:16px;padding-right:0}.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines .mdc-list-item__primary-text,.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines .mdc-list-item__primary-text,.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines .mdc-list-item__primary-text,.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines .mdc-list-item__primary-text{display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before,.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before,.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before,.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before{display:inline-block;width:0;height:32px;content:"";vertical-align:0}.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after,.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after,.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after,.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end,.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end,.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end,.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end{display:block;margin-top:0;line-height:normal}.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before,.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before,.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before,.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before{display:inline-block;width:0;height:32px;content:"";vertical-align:0}.mdc-list-item--with-trailing-icon.mdc-list-item,[dir=rtl] .mdc-list-item--with-trailing-icon.mdc-list-item{padding-left:0;padding-right:0}.mdc-list-item--with-trailing-icon .mdc-list-item__end{margin-left:16px;margin-right:16px}.mdc-list-item--with-trailing-meta.mdc-list-item{padding-left:16px;padding-right:0}[dir=rtl] .mdc-list-item--with-trailing-meta.mdc-list-item{padding-left:0;padding-right:16px}.mdc-list-item--with-trailing-meta .mdc-list-item__end{-webkit-user-select:none;user-select:none;margin-left:28px;margin-right:16px}[dir=rtl] .mdc-list-item--with-trailing-meta .mdc-list-item__end{margin-left:16px;margin-right:28px}.mdc-list-item--with-trailing-meta.mdc-list-item--with-three-lines .mdc-list-item__end,.mdc-list-item--with-trailing-meta.mdc-list-item--with-two-lines .mdc-list-item__end{display:block;line-height:normal;align-self:flex-start;margin-top:0}.mdc-list-item--with-trailing-meta.mdc-list-item--with-three-lines .mdc-list-item__end::before,.mdc-list-item--with-trailing-meta.mdc-list-item--with-two-lines .mdc-list-item__end::before{display:inline-block;width:0;height:28px;content:"";vertical-align:0}.mdc-list-item--with-leading-radio .mdc-list-item__start,.mdc-list-item--with-leading-checkbox .mdc-list-item__start{margin-left:8px;margin-right:24px}[dir=rtl] .mdc-list-item--with-leading-radio .mdc-list-item__start,[dir=rtl] .mdc-list-item--with-leading-checkbox .mdc-list-item__start{margin-left:24px;margin-right:8px}.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines .mdc-list-item__start,.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines .mdc-list-item__start{align-self:flex-start;margin-top:8px}.mdc-list-item--with-trailing-radio.mdc-list-item,.mdc-list-item--with-trailing-checkbox.mdc-list-item{padding-left:16px;padding-right:0}[dir=rtl] .mdc-list-item--with-trailing-radio.mdc-list-item,[dir=rtl] .mdc-list-item--with-trailing-checkbox.mdc-list-item{padding-left:0;padding-right:16px}.mdc-list-item--with-trailing-radio.mdc-list-item--with-leading-icon,.mdc-list-item--with-trailing-radio.mdc-list-item--with-leading-avatar,.mdc-list-item--with-trailing-checkbox.mdc-list-item--with-leading-icon,.mdc-list-item--with-trailing-checkbox.mdc-list-item--with-leading-avatar{padding-left:0}[dir=rtl] .mdc-list-item--with-trailing-radio.mdc-list-item--with-leading-icon,[dir=rtl] .mdc-list-item--with-trailing-radio.mdc-list-item--with-leading-avatar,[dir=rtl] .mdc-list-item--with-trailing-checkbox.mdc-list-item--with-leading-icon,[dir=rtl] .mdc-list-item--with-trailing-checkbox.mdc-list-item--with-leading-avatar{padding-right:0}.mdc-list-item--with-trailing-radio .mdc-list-item__end,.mdc-list-item--with-trailing-checkbox .mdc-list-item__end{margin-left:24px;margin-right:8px}[dir=rtl] .mdc-list-item--with-trailing-radio .mdc-list-item__end,[dir=rtl] .mdc-list-item--with-trailing-checkbox .mdc-list-item__end{margin-left:8px;margin-right:24px}.mdc-list-item--with-trailing-radio.mdc-list-item--with-three-lines .mdc-list-item__end,.mdc-list-item--with-trailing-checkbox.mdc-list-item--with-three-lines .mdc-list-item__end{align-self:flex-start;margin-top:8px}.mdc-list-group__subheader{margin:.75rem 16px}.mdc-list-item--disabled .mdc-list-item__start,.mdc-list-item--disabled .mdc-list-item__content,.mdc-list-item--disabled .mdc-list-item__end{opacity:1}.mdc-list-item--disabled .mdc-list-item__primary-text,.mdc-list-item--disabled .mdc-list-item__secondary-text{opacity:var(--mat-list-list-item-disabled-label-text-opacity, 0.3)}.mdc-list-item--disabled.mdc-list-item--with-leading-icon .mdc-list-item__start{color:var(--mat-list-list-item-disabled-leading-icon-color, var(--mat-sys-on-surface));opacity:var(--mat-list-list-item-disabled-leading-icon-opacity, 0.38)}.mdc-list-item--disabled.mdc-list-item--with-trailing-icon .mdc-list-item__end{color:var(--mat-list-list-item-disabled-trailing-icon-color, var(--mat-sys-on-surface));opacity:var(--mat-list-list-item-disabled-trailing-icon-opacity, 0.38)}.mat-mdc-list-item.mat-mdc-list-item-both-leading-and-trailing,[dir=rtl] .mat-mdc-list-item.mat-mdc-list-item-both-leading-and-trailing{padding-left:0;padding-right:0}.mdc-list-item.mdc-list-item--disabled .mdc-list-item__primary-text{color:var(--mat-list-list-item-disabled-label-text-color, var(--mat-sys-on-surface))}.mdc-list-item:hover::before{background-color:var(--mat-list-list-item-hover-state-layer-color, var(--mat-sys-on-surface));opacity:var(--mat-list-list-item-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mdc-list-item.mdc-list-item--disabled::before{background-color:var(--mat-list-list-item-disabled-state-layer-color, var(--mat-sys-on-surface));opacity:var(--mat-list-list-item-disabled-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mdc-list-item:focus::before{background-color:var(--mat-list-list-item-focus-state-layer-color, var(--mat-sys-on-surface));opacity:var(--mat-list-list-item-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mdc-list-item--disabled .mdc-radio,.mdc-list-item--disabled .mdc-checkbox{opacity:var(--mat-list-list-item-disabled-label-text-opacity, 0.3)}.mdc-list-item--with-leading-avatar .mat-mdc-list-item-avatar{border-radius:var(--mat-list-list-item-leading-avatar-shape, var(--mat-sys-corner-full));background-color:var(--mat-list-list-item-leading-avatar-color, var(--mat-sys-primary-container))}.mat-mdc-list-item-icon{font-size:var(--mat-list-list-item-leading-icon-size, 24px)}@media(forced-colors: active){a.mdc-list-item--activated::after{content:"";position:absolute;top:50%;right:16px;transform:translateY(-50%);width:10px;height:0;border-bottom:solid 10px;border-radius:10px}a.mdc-list-item--activated [dir=rtl]::after{right:auto;left:16px}}.mat-mdc-list-base{display:block}.mat-mdc-list-base .mdc-list-item__start,.mat-mdc-list-base .mdc-list-item__end,.mat-mdc-list-base .mdc-list-item__content{pointer-events:auto}.mat-mdc-list-item,.mat-mdc-list-option{width:100%;box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-list-item:not(.mat-mdc-list-item-interactive),.mat-mdc-list-option:not(.mat-mdc-list-item-interactive){cursor:default}.mat-mdc-list-item .mat-divider-inset,.mat-mdc-list-option .mat-divider-inset{position:absolute;left:0;right:0;bottom:0}.mat-mdc-list-item .mat-mdc-list-item-avatar~.mat-divider-inset,.mat-mdc-list-option .mat-mdc-list-item-avatar~.mat-divider-inset{margin-left:72px}[dir=rtl] .mat-mdc-list-item .mat-mdc-list-item-avatar~.mat-divider-inset,[dir=rtl] .mat-mdc-list-option .mat-mdc-list-item-avatar~.mat-divider-inset{margin-right:72px}.mat-mdc-list-item-interactive::before{top:0;left:0;right:0;bottom:0;position:absolute;content:"";opacity:0;pointer-events:none;border-radius:inherit}.mat-mdc-list-item>.mat-focus-indicator{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none}.mat-mdc-list-item:focus>.mat-focus-indicator::before{content:""}.mat-mdc-list-item.mdc-list-item--with-three-lines .mat-mdc-list-item-line.mdc-list-item__secondary-text{white-space:nowrap;line-height:normal}.mat-mdc-list-item.mdc-list-item--with-three-lines .mat-mdc-list-item-unscoped-content.mdc-list-item__secondary-text{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}mat-action-list button{background:none;color:inherit;border:none;font:inherit;outline:inherit;-webkit-tap-highlight-color:rgba(0,0,0,0);text-align:start}mat-action-list button::-moz-focus-inner{border:0}.mdc-list-item--with-leading-icon .mdc-list-item__start{margin-inline-start:var(--mat-list-list-item-leading-icon-start-space, 16px);margin-inline-end:var(--mat-list-list-item-leading-icon-end-space, 16px)}.mat-mdc-nav-list .mat-mdc-list-item{border-radius:var(--mat-list-active-indicator-shape, var(--mat-sys-corner-full));--mat-focus-indicator-border-radius: var(--mat-list-active-indicator-shape, var(--mat-sys-corner-full))}.mat-mdc-nav-list .mat-mdc-list-item.mdc-list-item--activated{background-color:var(--mat-list-active-indicator-color, var(--mat-sys-secondary-container))} +`,ofe=["unscopedContent"],afe=["text"],sfe=[[["","matListItemAvatar",""],["","matListItemIcon",""]],[["","matListItemTitle",""]],[["","matListItemLine",""]],"*",[["","matListItemMeta",""]],[["mat-divider"]]],lfe=["[matListItemAvatar],[matListItemIcon]","[matListItemTitle]","[matListItemLine]","*","[matListItemMeta]","mat-divider"];var cfe=new O("ListOption"),Hl=(()=>{class t{_elementRef=u(Y);constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matListItemTitle",""]],hostAttrs:[1,"mat-mdc-list-item-title","mdc-list-item__primary-text"]})}return t})(),Fu=(()=>{class t{_elementRef=u(Y);constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matListItemLine",""]],hostAttrs:[1,"mat-mdc-list-item-line","mdc-list-item__secondary-text"]})}return t})(),FI=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matListItemMeta",""]],hostAttrs:[1,"mat-mdc-list-item-meta","mdc-list-item__end"]})}return t})(),M8=(()=>{class t{_listOption=u(cfe,{optional:!0});constructor(){}_isAlignedAtStart(){return!this._listOption||this._listOption?._getTogglePosition()==="after"}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,hostVars:4,hostBindings:function(i,r){i&2&&G("mdc-list-item__start",r._isAlignedAtStart())("mdc-list-item__end",!r._isAlignedAtStart())}})}return t})(),dfe=(()=>{class t extends M8{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","matListItemAvatar",""]],hostAttrs:[1,"mat-mdc-list-item-avatar"],features:[le]})}return t})(),y_=(()=>{class t extends M8{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","matListItemIcon",""]],hostAttrs:[1,"mat-mdc-list-item-icon"],features:[le]})}return t})(),ufe=new O("MAT_LIST_CONFIG"),v_=(()=>{class t{_isNonInteractive=!0;get disableRipple(){return this._disableRipple}set disableRipple(e){this._disableRipple=Vi(e)}_disableRipple=!1;get disabled(){return this._disabled()}set disabled(e){this._disabled.set(Vi(e))}_disabled=he(!1);_defaultOptions=u(ufe,{optional:!0});static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,hostVars:1,hostBindings:function(i,r){i&2&&X("aria-disabled",r.disabled)},inputs:{disableRipple:"disableRipple",disabled:"disabled"}})}return t})(),mfe=(()=>{class t{_elementRef=u(Y);_ngZone=u(ae);_listBase=u(v_,{optional:!0});_platform=u(Ye);_hostElement;_isButtonElement;_noopAnimations=Qe();_avatars;_icons;set lines(e){this._explicitLines=Gn(e,null),this._updateItemLines(!1)}_explicitLines=null;get disableRipple(){return this.disabled||this._disableRipple||this._noopAnimations||!!this._listBase?.disableRipple}set disableRipple(e){this._disableRipple=Vi(e)}_disableRipple=!1;get disabled(){return this._disabled()||!!this._listBase?.disabled}set disabled(e){this._disabled.set(Vi(e))}_disabled=he(!1);_subscriptions=new ke;_rippleRenderer=null;_hasUnscopedTextContent=!1;rippleConfig;get rippleDisabled(){return this.disableRipple||!!this.rippleConfig.disabled}constructor(){u(ft).load(Oi);let e=u(Bs,{optional:!0});this.rippleConfig=e||{},this._hostElement=this._elementRef.nativeElement,this._isButtonElement=this._hostElement.nodeName.toLowerCase()==="button",this._listBase&&!this._listBase._isNonInteractive&&this._initInteractiveListItem(),this._isButtonElement&&!this._hostElement.hasAttribute("type")&&this._hostElement.setAttribute("type","button")}ngAfterViewInit(){this._monitorProjectedLinesAndTitle(),this._updateItemLines(!0)}ngOnDestroy(){this._subscriptions.unsubscribe(),this._rippleRenderer!==null&&this._rippleRenderer._removeTriggerEvents()}_hasIconOrAvatar(){return!!(this._avatars.length||this._icons.length)}_initInteractiveListItem(){this._hostElement.classList.add("mat-mdc-list-item-interactive"),this._rippleRenderer=new Ec(this,this._ngZone,this._hostElement,this._platform,u(de)),this._rippleRenderer.setupTriggerEvents(this._hostElement)}_monitorProjectedLinesAndTitle(){this._ngZone.runOutsideAngular(()=>{this._subscriptions.add(it(this._lines.changes,this._titles.changes).subscribe(()=>this._updateItemLines(!1)))})}_updateItemLines(e){if(!this._lines||!this._titles||!this._unscopedContent)return;e&&this._checkDomForUnscopedTextContent();let i=this._explicitLines??this._inferLinesFromContent(),r=this._unscopedContent.nativeElement;if(this._hostElement.classList.toggle("mat-mdc-list-item-single-line",i<=1),this._hostElement.classList.toggle("mdc-list-item--with-one-line",i<=1),this._hostElement.classList.toggle("mdc-list-item--with-two-lines",i===2),this._hostElement.classList.toggle("mdc-list-item--with-three-lines",i===3),this._hasUnscopedTextContent){let o=this._titles.length===0&&i===1;r.classList.toggle("mdc-list-item__primary-text",o),r.classList.toggle("mdc-list-item__secondary-text",!o)}else r.classList.remove("mdc-list-item__primary-text"),r.classList.remove("mdc-list-item__secondary-text")}_inferLinesFromContent(){let e=this._titles.length+this._lines.length;return this._hasUnscopedTextContent&&(e+=1),e}_checkDomForUnscopedTextContent(){this._hasUnscopedTextContent=Array.from(this._unscopedContent.nativeElement.childNodes).filter(e=>e.nodeType!==e.COMMENT_NODE).some(e=>!!(e.textContent&&e.textContent.trim()))}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,contentQueries:function(i,r,o){if(i&1&&(Ce(o,dfe,4),Ce(o,y_,4)),i&2){let a;j(a=H())&&(r._avatars=a),j(a=H())&&(r._icons=a)}},hostVars:4,hostBindings:function(i,r){i&2&&(X("aria-disabled",r.disabled)("disabled",r._isButtonElement&&r.disabled||null),G("mdc-list-item--disabled",r.disabled))},inputs:{lines:"lines",disableRipple:"disableRipple",disabled:"disabled"}})}return t})();var Js=(()=>{class t extends v_{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-list"]],hostAttrs:[1,"mat-mdc-list","mat-mdc-list-base","mdc-list"],exportAs:["matList"],features:[we([{provide:v_,useExisting:t}]),le],ngContentSelectors:w8,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),ne(0))},styles:[D8],encapsulation:2,changeDetection:0})}return t})(),pa=(()=>{class t extends mfe{_lines;_titles;_meta;_unscopedContent;_itemText;get activated(){return this._activated}set activated(e){this._activated=Vi(e)}_activated=!1;_getAriaCurrent(){return this._hostElement.nodeName==="A"&&this._activated?"page":null}_hasBothLeadingAndTrailing(){return this._meta.length!==0&&(this._avatars.length!==0||this._icons.length!==0)}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-list-item"],["a","mat-list-item",""],["button","mat-list-item",""]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,Fu,5),Ce(o,Hl,5),Ce(o,FI,5)),i&2){let a;j(a=H())&&(r._lines=a),j(a=H())&&(r._titles=a),j(a=H())&&(r._meta=a)}},viewQuery:function(i,r){if(i&1&&(ie(ofe,5),ie(afe,5)),i&2){let o;j(o=H())&&(r._unscopedContent=o.first),j(o=H())&&(r._itemText=o.first)}},hostAttrs:[1,"mat-mdc-list-item","mdc-list-item"],hostVars:13,hostBindings:function(i,r){i&2&&(X("aria-current",r._getAriaCurrent()),G("mdc-list-item--activated",r.activated)("mdc-list-item--with-leading-avatar",r._avatars.length!==0)("mdc-list-item--with-leading-icon",r._icons.length!==0)("mdc-list-item--with-trailing-meta",r._meta.length!==0)("mat-mdc-list-item-both-leading-and-trailing",r._hasBothLeadingAndTrailing())("_mat-animation-noopable",r._noopAnimations))},inputs:{activated:"activated"},exportAs:["matListItem"],features:[le],ngContentSelectors:lfe,decls:10,vars:0,consts:[["unscopedContent",""],[1,"mdc-list-item__content"],[1,"mat-mdc-list-item-unscoped-content",3,"cdkObserveContent"],[1,"mat-focus-indicator"]],template:function(i,r){if(i&1){let o=q();Ee(sfe),ne(0),m(1,"span",1),ne(2,1),ne(3,2),m(4,"span",2,0),S("cdkObserveContent",function(){return k(o),T(r._updateItemLines(!0))}),ne(6,3),h()(),ne(7,4),ne(8,5),M(9,"div",3)}},dependencies:[Zf],encapsulation:2,changeDetection:0})}return t})();var E8=(()=>{class t extends v_{_isNonInteractive=!1;static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-nav-list"]],hostAttrs:["role","navigation",1,"mat-mdc-nav-list","mat-mdc-list-base","mdc-list"],exportAs:["matNavList"],features:[we([{provide:v_,useExisting:t}]),le],ngContentSelectors:w8,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),ne(0))},styles:[D8],encapsulation:2,changeDetection:0})}return t})();var fa=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[Jm,De,Io,ey,Nr]})}return t})();var hfe=["mat-menu-item",""],pfe=[[["mat-icon"],["","matMenuItemIcon",""]],"*"],ffe=["mat-icon, [matMenuItemIcon]","*"];function gfe(t,n){t&1&&(ii(),m(0,"svg",2),M(1,"polygon",3),h())}var _fe=["*"];function bfe(t,n){if(t&1){let e=q();gt(0,"div",0),O0("click",function(){k(e);let r=x();return T(r.closed.emit("click"))})("animationstart",function(r){k(e);let o=x();return T(o._onAnimationStart(r.animationName))})("animationend",function(r){k(e);let o=x();return T(o._onAnimationDone(r.animationName))})("animationcancel",function(r){k(e);let o=x();return T(o._onAnimationDone(r.animationName))}),gt(1,"div",1),ne(2),yt()()}if(t&2){let e=x();at(e._classList),G("mat-menu-panel-animations-disabled",e._animationsDisabled)("mat-menu-panel-exit-animation",e._panelAnimationState==="void")("mat-menu-panel-animating",e._isAnimating()),pi("id",e.panelId),X("aria-label",e.ariaLabel||null)("aria-labelledby",e.ariaLabelledby||null)("aria-describedby",e.ariaDescribedby||null)}}var LI=new O("MAT_MENU_PANEL"),Ul=(()=>{class t{_elementRef=u(Y);_document=u(_e);_focusMonitor=u(oi);_parentMenu=u(LI,{optional:!0});_changeDetectorRef=u(ye);role="menuitem";disabled=!1;disableRipple=!1;_hovered=new z;_focused=new z;_highlighted=!1;_triggersSubmenu=!1;constructor(){u(ft).load(Oi),this._parentMenu?.addItem?.(this)}focus(e,i){this._focusMonitor&&e?this._focusMonitor.focusVia(this._getHostElement(),e,i):this._getHostElement().focus(i),this._focused.next(this)}ngAfterViewInit(){this._focusMonitor&&this._focusMonitor.monitor(this._elementRef,!1)}ngOnDestroy(){this._focusMonitor&&this._focusMonitor.stopMonitoring(this._elementRef),this._parentMenu&&this._parentMenu.removeItem&&this._parentMenu.removeItem(this),this._hovered.complete(),this._focused.complete()}_getTabIndex(){return this.disabled?"-1":"0"}_getHostElement(){return this._elementRef.nativeElement}_checkDisabled(e){this.disabled&&(e.preventDefault(),e.stopPropagation())}_handleMouseEnter(){this._hovered.next(this)}getLabel(){let e=this._elementRef.nativeElement.cloneNode(!0),i=e.querySelectorAll("mat-icon, .material-icons");for(let r=0;r{class t{_elementRef=u(Y);_changeDetectorRef=u(ye);_injector=u(de);_keyManager;_xPosition;_yPosition;_firstItemFocusRef;_exitFallbackTimeout;_animationsDisabled=Qe();_allItems;_directDescendantItems=new Dr;_classList={};_panelAnimationState="void";_animationDone=new z;_isAnimating=he(!1);parentMenu;direction;overlayPanelClass;backdropClass;ariaLabel;ariaLabelledby;ariaDescribedby;get xPosition(){return this._xPosition}set xPosition(e){this._xPosition=e,this.setPositionClasses()}get yPosition(){return this._yPosition}set yPosition(e){this._yPosition=e,this.setPositionClasses()}templateRef;items;lazyContent;overlapTrigger;hasBackdrop;set panelClass(e){let i=this._previousPanelClass,r=I({},this._classList);i&&i.length&&i.split(" ").forEach(o=>{r[o]=!1}),this._previousPanelClass=e,e&&e.length&&(e.split(" ").forEach(o=>{r[o]=!0}),this._elementRef.nativeElement.className=""),this._classList=r}_previousPanelClass;get classList(){return this.panelClass}set classList(e){this.panelClass=e}closed=new U;close=this.closed;panelId=u(et).getId("mat-menu-panel-");constructor(){let e=u(yfe);this.overlayPanelClass=e.overlayPanelClass||"",this._xPosition=e.xPosition,this._yPosition=e.yPosition,this.backdropClass=e.backdropClass,this.overlapTrigger=e.overlapTrigger,this.hasBackdrop=e.hasBackdrop}ngOnInit(){this.setPositionClasses()}ngAfterContentInit(){this._updateDirectDescendants(),this._keyManager=new Fs(this._directDescendantItems).withWrap().withTypeAhead().withHomeAndEnd(),this._keyManager.tabOut.subscribe(()=>this.closed.emit("tab")),this._directDescendantItems.changes.pipe(Ue(this._directDescendantItems),je(e=>it(...e.map(i=>i._focused)))).subscribe(e=>this._keyManager.updateActiveItem(e)),this._directDescendantItems.changes.subscribe(e=>{let i=this._keyManager;if(this._panelAnimationState==="enter"&&i.activeItem?._hasFocus()){let r=e.toArray(),o=Math.max(0,Math.min(r.length-1,i.activeItemIndex||0));r[o]&&!r[o].disabled?i.setActiveItem(o):i.setNextItemActive()}})}ngOnDestroy(){this._keyManager?.destroy(),this._directDescendantItems.destroy(),this.closed.complete(),this._firstItemFocusRef?.destroy(),clearTimeout(this._exitFallbackTimeout)}_hovered(){return this._directDescendantItems.changes.pipe(Ue(this._directDescendantItems),je(i=>it(...i.map(r=>r._hovered))))}addItem(e){}removeItem(e){}_handleKeydown(e){let i=e.keyCode,r=this._keyManager;switch(i){case 27:Gt(e)||(e.preventDefault(),this.closed.emit("keydown"));break;case 37:this.parentMenu&&this.direction==="ltr"&&this.closed.emit("keydown");break;case 39:this.parentMenu&&this.direction==="rtl"&&this.closed.emit("keydown");break;default:(i===38||i===40)&&r.setFocusOrigin("keyboard"),r.onKeydown(e);return}}focusFirstItem(e="program"){this._firstItemFocusRef?.destroy(),this._firstItemFocusRef=vt(()=>{let i=this._resolvePanel();if(!i||!i.contains(document.activeElement)){let r=this._keyManager;r.setFocusOrigin(e).setFirstItemActive(),!r.activeItem&&i&&i.focus()}},{injector:this._injector})}resetActiveItem(){this._keyManager.setActiveItem(-1)}setElevation(e){}setPositionClasses(e=this.xPosition,i=this.yPosition){this._classList=Me(I({},this._classList),{"mat-menu-before":e==="before","mat-menu-after":e==="after","mat-menu-above":i==="above","mat-menu-below":i==="below"}),this._changeDetectorRef.markForCheck()}_onAnimationDone(e){let i=e===sw;(i||e===NI)&&(i&&(clearTimeout(this._exitFallbackTimeout),this._exitFallbackTimeout=void 0),this._animationDone.next(i?"void":"enter"),this._isAnimating.set(!1))}_onAnimationStart(e){(e===NI||e===sw)&&this._isAnimating.set(!0)}_setIsOpen(e){if(this._panelAnimationState=e?"enter":"void",e){if(this._keyManager.activeItemIndex===0){let i=this._resolvePanel();i&&(i.scrollTop=0)}}else this._animationsDisabled||(this._exitFallbackTimeout=setTimeout(()=>this._onAnimationDone(sw),200));this._animationsDisabled&&setTimeout(()=>{this._onAnimationDone(e?NI:sw)}),this._changeDetectorRef.markForCheck()}_updateDirectDescendants(){this._allItems.changes.pipe(Ue(this._allItems)).subscribe(e=>{this._directDescendantItems.reset(e.filter(i=>i._parentMenu===this)),this._directDescendantItems.notifyOnChanges()})}_resolvePanel(){let e=null;return this._directDescendantItems.length&&(e=this._directDescendantItems.first._getHostElement().closest('[role="menu"]')),e}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-menu"]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,vfe,5),Ce(o,Ul,5),Ce(o,Ul,4)),i&2){let a;j(a=H())&&(r.lazyContent=a.first),j(a=H())&&(r._allItems=a),j(a=H())&&(r.items=a)}},viewQuery:function(i,r){if(i&1&&ie(te,5),i&2){let o;j(o=H())&&(r.templateRef=o.first)}},hostVars:3,hostBindings:function(i,r){i&2&&X("aria-label",null)("aria-labelledby",null)("aria-describedby",null)},inputs:{backdropClass:"backdropClass",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],ariaDescribedby:[0,"aria-describedby","ariaDescribedby"],xPosition:"xPosition",yPosition:"yPosition",overlapTrigger:[2,"overlapTrigger","overlapTrigger",B],hasBackdrop:[2,"hasBackdrop","hasBackdrop",e=>e==null?null:B(e)],panelClass:[0,"class","panelClass"],classList:"classList"},outputs:{closed:"closed",close:"close"},exportAs:["matMenu"],features:[we([{provide:LI,useExisting:t}])],ngContentSelectors:_fe,decls:1,vars:0,consts:[["tabindex","-1","role","menu",1,"mat-mdc-menu-panel",3,"click","animationstart","animationend","animationcancel","id"],[1,"mat-mdc-menu-content"]],template:function(i,r){i&1&&(Ee(),Ba(0,bfe,3,12,"ng-template"))},styles:[`mat-menu{display:none}.mat-mdc-menu-content{margin:0;padding:8px 0;outline:0}.mat-mdc-menu-content,.mat-mdc-menu-content .mat-mdc-menu-item .mat-mdc-menu-item-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;flex:1;white-space:normal;font-family:var(--mat-menu-item-label-text-font, var(--mat-sys-label-large-font));line-height:var(--mat-menu-item-label-text-line-height, var(--mat-sys-label-large-line-height));font-size:var(--mat-menu-item-label-text-size, var(--mat-sys-label-large-size));letter-spacing:var(--mat-menu-item-label-text-tracking, var(--mat-sys-label-large-tracking));font-weight:var(--mat-menu-item-label-text-weight, var(--mat-sys-label-large-weight))}@keyframes _mat-menu-enter{from{opacity:0;transform:scale(0.8)}to{opacity:1;transform:none}}@keyframes _mat-menu-exit{from{opacity:1}to{opacity:0}}.mat-mdc-menu-panel{min-width:112px;max-width:280px;overflow:auto;box-sizing:border-box;outline:0;animation:_mat-menu-enter 120ms cubic-bezier(0, 0, 0.2, 1);border-radius:var(--mat-menu-container-shape, var(--mat-sys-corner-extra-small));background-color:var(--mat-menu-container-color, var(--mat-sys-surface-container));box-shadow:var(--mat-menu-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));will-change:transform,opacity}.mat-mdc-menu-panel.mat-menu-panel-exit-animation{animation:_mat-menu-exit 100ms 25ms linear forwards}.mat-mdc-menu-panel.mat-menu-panel-animations-disabled{animation:none}.mat-mdc-menu-panel.mat-menu-panel-animating{pointer-events:none}.mat-mdc-menu-panel.mat-menu-panel-animating:has(.mat-mdc-menu-content:empty){display:none}@media(forced-colors: active){.mat-mdc-menu-panel{outline:solid 1px}}.mat-mdc-menu-panel .mat-divider{color:var(--mat-menu-divider-color, var(--mat-sys-surface-variant));margin-bottom:var(--mat-menu-divider-bottom-spacing, 8px);margin-top:var(--mat-menu-divider-top-spacing, 8px)}.mat-mdc-menu-item{display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;padding:0;cursor:pointer;width:100%;text-align:left;box-sizing:border-box;color:inherit;font-size:inherit;background:none;text-decoration:none;margin:0;min-height:48px;padding-left:var(--mat-menu-item-leading-spacing, 12px);padding-right:var(--mat-menu-item-trailing-spacing, 12px);-webkit-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-menu-item::-moz-focus-inner{border:0}[dir=rtl] .mat-mdc-menu-item{padding-left:var(--mat-menu-item-trailing-spacing, 12px);padding-right:var(--mat-menu-item-leading-spacing, 12px)}.mat-mdc-menu-item:has(.material-icons,mat-icon,[matButtonIcon]){padding-left:var(--mat-menu-item-with-icon-leading-spacing, 12px);padding-right:var(--mat-menu-item-with-icon-trailing-spacing, 12px)}[dir=rtl] .mat-mdc-menu-item:has(.material-icons,mat-icon,[matButtonIcon]){padding-left:var(--mat-menu-item-with-icon-trailing-spacing, 12px);padding-right:var(--mat-menu-item-with-icon-leading-spacing, 12px)}.mat-mdc-menu-item,.mat-mdc-menu-item:visited,.mat-mdc-menu-item:link{color:var(--mat-menu-item-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-menu-item .mat-icon-no-color,.mat-mdc-menu-item .mat-mdc-menu-submenu-icon{color:var(--mat-menu-item-icon-color, var(--mat-sys-on-surface-variant))}.mat-mdc-menu-item[disabled]{cursor:default;opacity:.38}.mat-mdc-menu-item[disabled]::after{display:block;position:absolute;content:"";top:0;left:0;bottom:0;right:0}.mat-mdc-menu-item:focus{outline:0}.mat-mdc-menu-item .mat-icon{flex-shrink:0;margin-right:var(--mat-menu-item-spacing, 12px);height:var(--mat-menu-item-icon-size, 24px);width:var(--mat-menu-item-icon-size, 24px)}[dir=rtl] .mat-mdc-menu-item{text-align:right}[dir=rtl] .mat-mdc-menu-item .mat-icon{margin-right:0;margin-left:var(--mat-menu-item-spacing, 12px)}.mat-mdc-menu-item:not([disabled]):hover{background-color:var(--mat-menu-item-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}.mat-mdc-menu-item:not([disabled]).cdk-program-focused,.mat-mdc-menu-item:not([disabled]).cdk-keyboard-focused,.mat-mdc-menu-item:not([disabled]).mat-mdc-menu-item-highlighted{background-color:var(--mat-menu-item-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent))}@media(forced-colors: active){.mat-mdc-menu-item{margin-top:1px}}.mat-mdc-menu-submenu-icon{width:var(--mat-menu-item-icon-size, 24px);height:10px;fill:currentColor;padding-left:var(--mat-menu-item-spacing, 12px)}[dir=rtl] .mat-mdc-menu-submenu-icon{padding-right:var(--mat-menu-item-spacing, 12px);padding-left:0}[dir=rtl] .mat-mdc-menu-submenu-icon polygon{transform:scaleX(-1);transform-origin:center}@media(forced-colors: active){.mat-mdc-menu-submenu-icon{fill:CanvasText}}.mat-mdc-menu-item .mat-mdc-menu-ripple{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none} +`],encapsulation:2,changeDetection:0})}return t})(),S8=new O("mat-menu-scroll-strategy",{providedIn:"root",factory:()=>{let t=u(de);return()=>Tn(t)}});function Cfe(t){let n=u(de);return()=>Tn(n)}var wfe={provide:S8,deps:[],useFactory:Cfe};var sp=new WeakMap,Dfe=(()=>{class t{_canHaveBackdrop;_element=u(Y);_viewContainerRef=u(st);_menuItemInstance=u(Ul,{optional:!0,self:!0});_dir=u(Yt,{optional:!0});_focusMonitor=u(oi);_ngZone=u(ae);_injector=u(de);_scrollStrategy=u(S8);_changeDetectorRef=u(ye);_animationsDisabled=Qe();_portal;_overlayRef=null;_menuOpen=!1;_closingActionsSubscription=ke.EMPTY;_menuCloseSubscription=ke.EMPTY;_pendingRemoval;_parentMaterialMenu;_parentInnerPadding;_openedBy=void 0;get _menu(){return this._menuInternal}set _menu(e){e!==this._menuInternal&&(this._menuInternal=e,this._menuCloseSubscription.unsubscribe(),e&&(this._parentMaterialMenu,this._menuCloseSubscription=e.close.subscribe(i=>{this._destroyMenu(i),(i==="click"||i==="tab")&&this._parentMaterialMenu&&this._parentMaterialMenu.closed.emit(i)})),this._menuItemInstance?._setTriggersSubmenu(this._triggersSubmenu()))}_menuInternal;constructor(e){this._canHaveBackdrop=e;let i=u(LI,{optional:!0});this._parentMaterialMenu=i instanceof Zr?i:void 0}ngOnDestroy(){this._menu&&this._ownsMenu(this._menu)&&sp.delete(this._menu),this._pendingRemoval?.unsubscribe(),this._menuCloseSubscription.unsubscribe(),this._closingActionsSubscription.unsubscribe(),this._overlayRef&&(this._overlayRef.dispose(),this._overlayRef=null)}get menuOpen(){return this._menuOpen}get dir(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}_triggersSubmenu(){return!!(this._menuItemInstance&&this._parentMaterialMenu&&this._menu)}_closeMenu(){this._menu?.close.emit()}_openMenu(e){let i=this._menu;if(this._menuOpen||!i)return;this._pendingRemoval?.unsubscribe();let r=sp.get(i);sp.set(i,this),r&&r!==this&&r._closeMenu();let o=this._createOverlay(i),a=o.getConfig(),s=a.positionStrategy;this._setPosition(i,s),this._canHaveBackdrop?a.hasBackdrop=i.hasBackdrop==null?!this._triggersSubmenu():i.hasBackdrop:a.hasBackdrop=!1,o.hasAttached()||(o.attach(this._getPortal(i)),i.lazyContent?.attach(this.menuData)),this._closingActionsSubscription=this._menuClosingActions().subscribe(()=>this._closeMenu()),i.parentMenu=this._triggersSubmenu()?this._parentMaterialMenu:void 0,i.direction=this.dir,e&&i.focusFirstItem(this._openedBy||"program"),this._setIsMenuOpen(!0),i instanceof Zr&&(i._setIsOpen(!0),i._directDescendantItems.changes.pipe(xe(i.close)).subscribe(()=>{s.withLockedPosition(!1).reapplyLastPosition(),s.withLockedPosition(!0)}))}focus(e,i){this._focusMonitor&&e?this._focusMonitor.focusVia(this._element,e,i):this._element.nativeElement.focus(i)}_destroyMenu(e){let i=this._overlayRef,r=this._menu;!i||!this.menuOpen||(this._closingActionsSubscription.unsubscribe(),this._pendingRemoval?.unsubscribe(),r instanceof Zr&&this._ownsMenu(r)?(this._pendingRemoval=r._animationDone.pipe(mt(1)).subscribe(()=>{i.detach(),sp.has(r)||r.lazyContent?.detach()}),r._setIsOpen(!1)):(i.detach(),r?.lazyContent?.detach()),r&&this._ownsMenu(r)&&sp.delete(r),this.restoreFocus&&(e==="keydown"||!this._openedBy||!this._triggersSubmenu())&&this.focus(this._openedBy),this._openedBy=void 0,this._setIsMenuOpen(!1))}_setIsMenuOpen(e){e!==this._menuOpen&&(this._menuOpen=e,this._menuOpen?this.menuOpened.emit():this.menuClosed.emit(),this._triggersSubmenu()&&this._menuItemInstance._setHighlighted(e),this._changeDetectorRef.markForCheck())}_createOverlay(e){if(!this._overlayRef){let i=this._getOverlayConfig(e);this._subscribeToPositions(e,i.positionStrategy),this._overlayRef=qr(this._injector,i),this._overlayRef.keydownEvents().subscribe(r=>{this._menu instanceof Zr&&this._menu._handleKeydown(r)})}return this._overlayRef}_getOverlayConfig(e){return new Gr({positionStrategy:Ja(this._injector,this._getOverlayOrigin()).withLockedPosition().withGrowAfterOpen().withTransformOriginOn(".mat-menu-panel, .mat-mdc-menu-panel"),backdropClass:e.backdropClass||"cdk-overlay-transparent-backdrop",panelClass:e.overlayPanelClass,scrollStrategy:this._scrollStrategy(),direction:this._dir||"ltr",disableAnimations:this._animationsDisabled})}_subscribeToPositions(e,i){e.setPositionClasses&&i.positionChanges.subscribe(r=>{this._ngZone.run(()=>{let o=r.connectionPair.overlayX==="start"?"after":"before",a=r.connectionPair.overlayY==="top"?"below":"above";e.setPositionClasses(o,a)})})}_setPosition(e,i){let[r,o]=e.xPosition==="before"?["end","start"]:["start","end"],[a,s]=e.yPosition==="above"?["bottom","top"]:["top","bottom"],[l,c]=[a,s],[d,p]=[r,o],_=0;if(this._triggersSubmenu()){if(p=r=e.xPosition==="before"?"start":"end",o=d=r==="end"?"start":"end",this._parentMaterialMenu){if(this._parentInnerPadding==null){let b=this._parentMaterialMenu.items.first;this._parentInnerPadding=b?b._getHostElement().offsetTop:0}_=a==="bottom"?this._parentInnerPadding:-this._parentInnerPadding}}else e.overlapTrigger||(l=a==="top"?"bottom":"top",c=s==="top"?"bottom":"top");i.withPositions([{originX:r,originY:l,overlayX:d,overlayY:a,offsetY:_},{originX:o,originY:l,overlayX:p,overlayY:a,offsetY:_},{originX:r,originY:c,overlayX:d,overlayY:s,offsetY:-_},{originX:o,originY:c,overlayX:p,overlayY:s,offsetY:-_}])}_menuClosingActions(){let e=this._getOutsideClickStream(this._overlayRef),i=this._overlayRef.detachments(),r=this._parentMaterialMenu?this._parentMaterialMenu.closed:Q(),o=this._parentMaterialMenu?this._parentMaterialMenu._hovered().pipe(ce(a=>this._menuOpen&&a!==this._menuItemInstance)):Q();return it(e,r,o,i)}_getPortal(e){return(!this._portal||this._portal.templateRef!==e.templateRef)&&(this._portal=new kn(e.templateRef,this._viewContainerRef)),this._portal}_ownsMenu(e){return sp.get(e)===this}static \u0275fac=function(i){jd()};static \u0275dir=P({type:t})}return t})(),el=(()=>{class t extends Dfe{_cleanupTouchstart;_hoverSubscription=ke.EMPTY;get _deprecatedMatMenuTriggerFor(){return this.menu}set _deprecatedMatMenuTriggerFor(e){this.menu=e}get menu(){return this._menu}set menu(e){this._menu=e}menuData;restoreFocus=!0;menuOpened=new U;onMenuOpen=this.menuOpened;menuClosed=new U;onMenuClose=this.menuClosed;constructor(){super(!0);let e=u(ze);this._cleanupTouchstart=e.listen(this._element.nativeElement,"touchstart",i=>{eu(i)||(this._openedBy="touch")},{passive:!0})}triggersSubmenu(){return super._triggersSubmenu()}toggleMenu(){return this.menuOpen?this.closeMenu():this.openMenu()}openMenu(){this._openMenu(!0)}closeMenu(){this._closeMenu()}updatePosition(){this._overlayRef?.updatePosition()}ngAfterContentInit(){this._handleHover()}ngOnDestroy(){super.ngOnDestroy(),this._cleanupTouchstart(),this._hoverSubscription.unsubscribe()}_getOverlayOrigin(){return this._element}_getOutsideClickStream(e){return e.backdropClick()}_handleMousedown(e){Jd(e)||(this._openedBy=e.button===0?"mouse":void 0,this.triggersSubmenu()&&e.preventDefault())}_handleKeydown(e){let i=e.keyCode;(i===13||i===32)&&(this._openedBy="keyboard"),this.triggersSubmenu()&&(i===39&&this.dir==="ltr"||i===37&&this.dir==="rtl")&&(this._openedBy="keyboard",this.openMenu())}_handleClick(e){this.triggersSubmenu()?(e.stopPropagation(),this.openMenu()):this.toggleMenu()}_handleHover(){this.triggersSubmenu()&&this._parentMaterialMenu&&(this._hoverSubscription=this._parentMaterialMenu._hovered().subscribe(e=>{e===this._menuItemInstance&&!e.disabled&&this._parentMaterialMenu?._panelAnimationState!=="void"&&(this._openedBy="mouse",this._openMenu(!1))}))}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","mat-menu-trigger-for",""],["","matMenuTriggerFor",""]],hostAttrs:[1,"mat-mdc-menu-trigger"],hostVars:3,hostBindings:function(i,r){i&1&&S("click",function(a){return r._handleClick(a)})("mousedown",function(a){return r._handleMousedown(a)})("keydown",function(a){return r._handleKeydown(a)}),i&2&&X("aria-haspopup",r.menu?"menu":null)("aria-expanded",r.menuOpen)("aria-controls",r.menuOpen?r.menu==null?null:r.menu.panelId:null)},inputs:{_deprecatedMatMenuTriggerFor:[0,"mat-menu-trigger-for","_deprecatedMatMenuTriggerFor"],menu:[0,"matMenuTriggerFor","menu"],menuData:[0,"matMenuTriggerData","menuData"],restoreFocus:[0,"matMenuTriggerRestoreFocus","restoreFocus"]},outputs:{menuOpened:"menuOpened",onMenuOpen:"onMenuOpen",menuClosed:"menuClosed",onMenuClose:"onMenuClose"},exportAs:["matMenuTrigger"],features:[le]})}return t})();var tl=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[wfe],imports:[Io,De,cr,Tr,De]})}return t})(),k8={transformMenu:{type:7,name:"transformMenu",definitions:[{type:0,name:"void",styles:{type:6,styles:{opacity:0,transform:"scale(0.8)"},offset:null}},{type:1,expr:"void => enter",animation:{type:4,styles:{type:6,styles:{opacity:1,transform:"scale(1)"},offset:null},timings:"120ms cubic-bezier(0, 0, 0.2, 1)"},options:null},{type:1,expr:"* => void",animation:{type:4,styles:{type:6,styles:{opacity:0},offset:null},timings:"100ms 25ms linear"},options:null}],options:{}},fadeInItems:{type:7,name:"fadeInItems",definitions:[{type:0,name:"showing",styles:{type:6,styles:{opacity:1},offset:null}},{type:1,expr:"void => *",animation:[{type:6,styles:{opacity:0},offset:null},{type:4,styles:null,timings:"400ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)"}],options:null}],options:{}}},Ict=k8.fadeInItems,Act=k8.transformMenu;function Mfe(t,n){if(t&1&&(m(0,"mat-list-item")(1,"mat-icon",3),f(2,"info"),h(),m(3,"a",4),f(4),h()()),t&2){let e=n.$implicit;g(4),N(e)}}var T8=(()=>{let n=class n{constructor(){this.messages=["Server Error Reports 1","Server Error Reports 2","Server Error Reports 3"]}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-notification"]],decls:8,vars:1,consts:[["menu","matMenu"],["matIconButton","",3,"matMenuTriggerFor"],["matBadge","5","matBadgeColor","warn","aria-hidden","false"],["matListItemIcon","",1,"m-x-16"],["matListItemTitle","","href","#"]],template:function(r,o){if(r&1&&(m(0,"button",1)(1,"mat-icon",2),f(2,"notifications"),h()(),m(3,"mat-menu",null,0)(5,"mat-nav-list"),Mt(6,Mfe,5,1,"mat-list-item",null,qi),h()()),r&2){let a=Te(4);v("matMenuTriggerFor",a),g(6),Et(o.messages)}},dependencies:[C8,x8,Fe,Ft,Ge,Ze,fa,E8,pa,y_,Hl,tl,Zr,el],styles:["[_nghost-%COMP%] .mat-badge-content{--mat-badge-background-color: #ef0000;--mat-badge-text-color: #fff}"]});let t=n;return t})();var Efe=(t,n)=>n.value;function Sfe(t,n){t&1&&M(0,"mat-pseudo-checkbox",5)}function kfe(t,n){if(t&1){let e=q();m(0,"button",3),S("click",function(){let r=k(e).$implicit,o=x();return T(o.changeLang(r.value))}),m(1,"span",4),f(2),me(3,"translate"),L(4,Sfe,1,0,"mat-pseudo-checkbox",5),h()()}if(t&2){let e=n.$implicit,i=x();g(2),fe(" ",Re(3,2,e.name)," "),g(2),V(e.value===i.options.language?4:-1)}}var I8=(()=>{let n=class n{constructor(){this.settings=u(ha),this.options=this.settings.options,this.langs=[{value:"en-US",name:"en_us"},{value:"zh-CN",name:"zh_cn"},{value:"zh-TW",name:"zh_tw"},{value:"auto",name:"system"}]}changeLang(i){this.settings.setLanguage(i)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-translate"]],decls:7,vars:1,consts:[["menu","matMenu"],["matIconButton","",3,"matMenuTriggerFor"],["mat-menu-item",""],["mat-menu-item","",3,"click"],[1,"d-flex","justify-content-between","gap-8"],["state","checked","appearance","minimal"]],template:function(r,o){if(r&1&&(m(0,"button",1)(1,"mat-icon"),f(2,"translate"),h()(),m(3,"mat-menu",null,0),Mt(5,kfe,5,4,"button",2,Efe),h()),r&2){let a=Te(4);v("matMenuTriggerFor",a),g(5),Et(o.langs)}},dependencies:[Fe,Ft,Ge,Ze,tl,Zr,Ul,el,nu,Or],encapsulation:2});let t=n;return t})();function Tfe(t,n){t&1&&(m(0,"button",8)(1,"mat-icon"),f(2,"account_circle"),h(),m(3,"span"),f(4),me(5,"translate"),h()(),m(6,"button",9)(7,"mat-icon"),f(8,"edit"),h(),m(9,"span"),f(10),me(11,"translate"),h()()),t&2&&(g(4),N(Re(5,2,"profile")),g(6),N(Re(11,4,"edit_profile")))}function Ife(t,n){if(t&1){let e=q();m(0,"button",6),S("click",function(){k(e);let r=x();return T(r.logout())}),m(1,"mat-icon"),f(2,"exit_to_app"),h(),m(3,"span"),f(4),me(5,"translate"),h()()}t&2&&(g(4),N(Re(5,1,"logout")))}function Afe(t,n){if(t&1){let e=q();m(0,"button",6),S("click",function(){k(e);let r=x();return T(r.login())}),m(1,"mat-icon"),f(2,"login"),h(),m(3,"span"),f(4),me(5,"translate"),h()()}t&2&&(g(4),N(Re(5,1,"login")))}var A8=(()=>{let n=class n{constructor(){this.oidcAuth=u(jt),this.router=u(Ae),this.settings=u(ha),this.userName="Guest",this.userEmail="",this.userRoles="Anonymous User"}ngOnInit(){this.updateUserInfo(),this.authSubscription=this.oidcAuth.isAuthenticated$.subscribe(()=>{this.updateUserInfo()})}ngOnDestroy(){this.authSubscription?.unsubscribe()}updateUserInfo(){if(!this.oidcAuth.isAuthenticated()){this.userName="Guest",this.userEmail="",this.userRoles="Anonymous User";return}let i=this.oidcAuth.getUserInfo();console.log("UserButton: User info:",i),console.log("UserButton: Available claims:",i?Object.keys(i):"null"),this.userName=i?.name||i?.preferred_username||i?.given_name||i?.["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"]||i?.sub||"User",this.userEmail=i?.email||i?.["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]||"";let r=this.oidcAuth.getUserRoles();this.userRoles=r.length>0?r.join(", "):"No roles",console.log("UserButton: Set userName to:",this.userName,"email to:",this.userEmail,"roles:",this.userRoles)}isAuthenticated(){return this.oidcAuth.isAuthenticated()}login(){this.oidcAuth.login()}logout(){this.oidcAuth.logout(),this.router.navigateByUrl("/dashboard")}restore(){this.settings.reset(),window.location.reload()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-user"]],decls:22,vars:9,consts:[["menu","matMenu"],["matIconButton","",1,"user-button",3,"matMenuTriggerFor"],[1,"user-info"],[1,"user-name"],[1,"user-email"],[1,"user-roles"],["mat-menu-item","",3,"click"],["mat-menu-item",""],["routerLink","/profile/overview","mat-menu-item",""],["routerLink","/profile/settings","mat-menu-item",""]],template:function(r,o){if(r&1){let a=q();m(0,"button",1)(1,"mat-icon"),f(2,"account_circle"),h()(),m(3,"mat-menu",null,0)(5,"div",2)(6,"div",3),f(7),h(),m(8,"div",4),f(9),h(),m(10,"div",5),f(11),h()(),M(12,"mat-divider"),L(13,Tfe,12,6),m(14,"button",6),S("click",function(){return k(a),T(o.restore())}),m(15,"mat-icon"),f(16,"restore"),h(),m(17,"span"),f(18),me(19,"translate"),h()(),L(20,Ife,6,3,"button",7)(21,Afe,6,3,"button",7),h()}if(r&2){let a=Te(4);v("matMenuTriggerFor",a),g(7),N(o.userName),g(2),N(o.userEmail),g(2),N(o.userRoles),g(2),V(o.isAuthenticated()?13:-1),g(5),N(Re(19,7,"restore_defaults")),g(2),V(o.isAuthenticated()?20:21)}},dependencies:[Je,Wn,Fe,Ft,Ge,Ze,tl,Zr,Ul,el,Nr,Kr,Rr,Or],styles:["[_nghost-%COMP%]{display:inline-block}.user-button[_ngcontent-%COMP%]{display:inline-flex!important}.user-info[_ngcontent-%COMP%]{padding:16px;max-width:250px}.user-info[_ngcontent-%COMP%] .user-name[_ngcontent-%COMP%]{font-weight:500;font-size:14px;margin-bottom:4px}.user-info[_ngcontent-%COMP%] .user-email[_ngcontent-%COMP%]{font-size:12px;color:#0009;margin-bottom:4px}.user-info[_ngcontent-%COMP%] .user-roles[_ngcontent-%COMP%]{font-size:11px;color:#00000080;font-style:italic}"]});let t=n;return t})();function Ofe(t,n){if(t&1){let e=q();m(0,"button",4),S("click",function(){k(e);let r=x();return T(r.toggleSidenav.emit())}),m(1,"mat-icon"),f(2,"menu"),h()()}}function Rfe(t,n){t&1&&M(0,"app-branding")}var O8=(()=>{let n=class n{constructor(){this.showToggle=re(!0),this.showBranding=re(!1),this.toggleSidenav=Ei(),this.toggleSidenavNotice=Ei()}toggleFullscreen(){PI.isEnabled&&PI.toggle()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-header"]],hostAttrs:[1,"matero-header"],inputs:{showToggle:[1,"showToggle"],showBranding:[1,"showBranding"]},outputs:{toggleSidenav:"toggleSidenav",toggleSidenavNotice:"toggleSidenavNotice"},decls:17,vars:2,consts:[["matIconButton",""],[1,"flex-fill"],["matIconButton","",1,"hide-small",3,"click"],[1,"hide-small"],["matIconButton","",3,"click"]],template:function(r,o){r&1&&(m(0,"mat-toolbar"),L(1,Ofe,3,0,"button",0),L(2,Rfe,1,0,"app-branding"),M(3,"span",1)(4,"app-github-button"),m(5,"button",0)(6,"mat-icon"),f(7,"search"),h()(),M(8,"app-translate"),m(9,"button",2),S("click",function(){return o.toggleFullscreen()}),m(10,"mat-icon"),f(11,"fullscreen"),h()(),M(12,"app-notification",3)(13,"app-user"),m(14,"button",2),S("click",function(){return o.toggleSidenavNotice.emit()}),m(15,"mat-icon"),f(16,"list"),h()()()),r&2&&(g(),V(o.showToggle()?1:-1),g(),V(o.showBranding()?2:-1))},dependencies:[ow,rw,Fe,Ft,Ge,Ze,aw,v8,T8,I8,A8],styles:[`.matero-header{--mat-toolbar-container-background-color: transparent;position:relative;z-index:200;display:block;background-color:var(--header-background-color);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)} +`],encapsulation:2});let t=n;return t})();var lp=["*"];function Pfe(t,n){t&1&&ne(0)}var N8=["tabListContainer"],L8=["tabList"],V8=["tabListInner"],B8=["nextPaginator"],j8=["previousPaginator"],Ffe=["content"];function Nfe(t,n){}var Lfe=["tabBodyWrapper"],Vfe=["tabHeader"];function Bfe(t,n){}function jfe(t,n){if(t&1&&A(0,Bfe,0,0,"ng-template",12),t&2){let e=x().$implicit;v("cdkPortalOutlet",e.templateLabel)}}function Hfe(t,n){if(t&1&&f(0),t&2){let e=x().$implicit;N(e.textLabel)}}function zfe(t,n){if(t&1){let e=q();m(0,"div",7,2),S("click",function(){let r=k(e),o=r.$implicit,a=r.$index,s=x(),l=Te(1);return T(s._handleClick(o,l,a))})("cdkFocusChange",function(r){let o=k(e).$index,a=x();return T(a._tabFocusChanged(r,o))}),M(2,"span",8)(3,"div",9),m(4,"span",10)(5,"span",11),L(6,jfe,1,1,null,12)(7,Hfe,1,1),h()()()}if(t&2){let e=n.$implicit,i=n.$index,r=Te(1),o=x();at(e.labelClass),G("mdc-tab--active",o.selectedIndex===i),v("id",o._getTabLabelId(e,i))("disabled",e.disabled)("fitInkBarToContent",o.fitInkBarToContent),X("tabIndex",o._getTabIndex(i))("aria-posinset",i+1)("aria-setsize",o._tabs.length)("aria-controls",o._getTabContentId(i))("aria-selected",o.selectedIndex===i)("aria-label",e.ariaLabel||null)("aria-labelledby",!e.ariaLabel&&e.ariaLabelledby?e.ariaLabelledby:null),g(3),v("matRippleTrigger",r)("matRippleDisabled",e.disabled||o.disableRipple),g(3),V(e.templateLabel?6:7)}}function Ufe(t,n){t&1&&ne(0)}function $fe(t,n){if(t&1){let e=q();m(0,"mat-tab-body",13),S("_onCentered",function(){k(e);let r=x();return T(r._removeTabBodyWrapperHeight())})("_onCentering",function(r){k(e);let o=x();return T(o._setTabBodyWrapperHeight(r))})("_beforeCentering",function(r){k(e);let o=x();return T(o._bodyCentered(r))}),h()}if(t&2){let e=n.$implicit,i=n.$index,r=x();at(e.bodyClass),v("id",r._getTabContentId(i))("content",e.content)("position",e.position)("animationDuration",r.animationDuration)("preserveContent",r.preserveContent),X("tabindex",r.contentTabIndex!=null&&r.selectedIndex===i?r.contentTabIndex:null)("aria-labelledby",r._getTabLabelId(e,i))("aria-hidden",r.selectedIndex!==i)}}var Wfe=["mat-tab-nav-bar",""],Gfe=["mat-tab-link",""],qfe=new O("MatTabContent"),Yfe=(()=>{class t{template=u(te);constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matTabContent",""]],features:[we([{provide:qfe,useExisting:t}])]})}return t})(),Qfe=new O("MatTabLabel"),H8=new O("MAT_TAB"),Kfe=(()=>{class t extends oB{_closestTab=u(H8,{optional:!0});static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","mat-tab-label",""],["","matTabLabel",""]],features:[we([{provide:Qfe,useExisting:t}]),le]})}return t})(),z8=new O("MAT_TAB_GROUP"),C_=(()=>{class t{_viewContainerRef=u(st);_closestTabGroup=u(z8,{optional:!0});disabled=!1;get templateLabel(){return this._templateLabel}set templateLabel(e){this._setTemplateLabelInput(e)}_templateLabel;_explicitContent=void 0;_implicitContent;textLabel="";ariaLabel;ariaLabelledby;labelClass;bodyClass;id=null;_contentPortal=null;get content(){return this._contentPortal}_stateChanges=new z;position=null;origin=null;isActive=!1;constructor(){u(ft).load(Oi)}ngOnChanges(e){(e.hasOwnProperty("textLabel")||e.hasOwnProperty("disabled"))&&this._stateChanges.next()}ngOnDestroy(){this._stateChanges.complete()}ngOnInit(){this._contentPortal=new kn(this._explicitContent||this._implicitContent,this._viewContainerRef)}_setTemplateLabelInput(e){e&&e._closestTab===this&&(this._templateLabel=e)}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-tab"]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,Kfe,5),Ce(o,Yfe,7,te)),i&2){let a;j(a=H())&&(r.templateLabel=a.first),j(a=H())&&(r._explicitContent=a.first)}},viewQuery:function(i,r){if(i&1&&ie(te,7),i&2){let o;j(o=H())&&(r._implicitContent=o.first)}},hostAttrs:["hidden",""],hostVars:1,hostBindings:function(i,r){i&2&&X("id",null)},inputs:{disabled:[2,"disabled","disabled",B],textLabel:[0,"label","textLabel"],ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],labelClass:"labelClass",bodyClass:"bodyClass",id:"id"},exportAs:["matTab"],features:[we([{provide:H8,useExisting:t}]),Oe],ngContentSelectors:lp,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),Ba(0,Pfe,1,0,"ng-template"))},encapsulation:2})}return t})(),VI="mdc-tab-indicator--active",R8="mdc-tab-indicator--no-transition",lw=class{_items;_currentItem;constructor(n){this._items=n}hide(){this._items.forEach(n=>n.deactivateInkBar()),this._currentItem=void 0}alignToElement(n){let e=this._items.find(r=>r.elementRef.nativeElement===n),i=this._currentItem;if(e!==i&&(i?.deactivateInkBar(),e)){let r=i?.elementRef.nativeElement.getBoundingClientRect?.();e.activateInkBar(r),this._currentItem=e}}},U8=(()=>{class t{_elementRef=u(Y);_inkBarElement;_inkBarContentElement;_fitToContent=!1;get fitInkBarToContent(){return this._fitToContent}set fitInkBarToContent(e){this._fitToContent!==e&&(this._fitToContent=e,this._inkBarElement&&this._appendInkBarElement())}activateInkBar(e){let i=this._elementRef.nativeElement;if(!e||!i.getBoundingClientRect||!this._inkBarContentElement){i.classList.add(VI);return}let r=i.getBoundingClientRect(),o=e.width/r.width,a=e.left-r.left;i.classList.add(R8),this._inkBarContentElement.style.setProperty("transform",`translateX(${a}px) scaleX(${o})`),i.getBoundingClientRect(),i.classList.remove(R8),i.classList.add(VI),this._inkBarContentElement.style.setProperty("transform","")}deactivateInkBar(){this._elementRef.nativeElement.classList.remove(VI)}ngOnInit(){this._createInkBarElement()}ngOnDestroy(){this._inkBarElement?.remove(),this._inkBarElement=this._inkBarContentElement=null}_createInkBarElement(){let e=this._elementRef.nativeElement.ownerDocument||document,i=this._inkBarElement=e.createElement("span"),r=this._inkBarContentElement=e.createElement("span");i.className="mdc-tab-indicator",r.className="mdc-tab-indicator__content mdc-tab-indicator__content--underline",i.appendChild(this._inkBarContentElement),this._appendInkBarElement()}_appendInkBarElement(){this._inkBarElement;let e=this._fitToContent?this._elementRef.nativeElement.querySelector(".mdc-tab__content"):this._elementRef.nativeElement;e.appendChild(this._inkBarElement)}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,inputs:{fitInkBarToContent:[2,"fitInkBarToContent","fitInkBarToContent",B]}})}return t})();var $8=(()=>{class t extends U8{elementRef=u(Y);disabled=!1;focus(){this.elementRef.nativeElement.focus()}getOffsetLeft(){return this.elementRef.nativeElement.offsetLeft}getOffsetWidth(){return this.elementRef.nativeElement.offsetWidth}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","matTabLabelWrapper",""]],hostVars:3,hostBindings:function(i,r){i&2&&(X("aria-disabled",!!r.disabled),G("mat-mdc-tab-disabled",r.disabled))},inputs:{disabled:[2,"disabled","disabled",B]},features:[le]})}return t})(),P8={passive:!0},Zfe=650,Xfe=100,W8=(()=>{class t{_elementRef=u(Y);_changeDetectorRef=u(ye);_viewportRuler=u(sr);_dir=u(Yt,{optional:!0});_ngZone=u(ae);_platform=u(Ye);_sharedResizeObserver=u(Jy);_injector=u(de);_renderer=u(ze);_animationsDisabled=Qe();_eventCleanups;_scrollDistance=0;_selectedIndexChanged=!1;_destroyed=new z;_showPaginationControls=!1;_disableScrollAfter=!0;_disableScrollBefore=!0;_tabLabelCount;_scrollDistanceChanged;_keyManager;_currentTextContent;_stopScrolling=new z;disablePagination=!1;get selectedIndex(){return this._selectedIndex}set selectedIndex(e){let i=isNaN(e)?0:e;this._selectedIndex!=i&&(this._selectedIndexChanged=!0,this._selectedIndex=i,this._keyManager&&this._keyManager.updateActiveItem(i))}_selectedIndex=0;selectFocusedIndex=new U;indexFocused=new U;constructor(){this._eventCleanups=this._ngZone.runOutsideAngular(()=>[this._renderer.listen(this._elementRef.nativeElement,"mouseleave",()=>this._stopInterval())])}ngAfterViewInit(){this._eventCleanups.push(this._renderer.listen(this._previousPaginator.nativeElement,"touchstart",()=>this._handlePaginatorPress("before"),P8),this._renderer.listen(this._nextPaginator.nativeElement,"touchstart",()=>this._handlePaginatorPress("after"),P8))}ngAfterContentInit(){let e=this._dir?this._dir.change:Q("ltr"),i=this._sharedResizeObserver.observe(this._elementRef.nativeElement).pipe(Dt(32),xe(this._destroyed)),r=this._viewportRuler.change(150).pipe(xe(this._destroyed)),o=()=>{this.updatePagination(),this._alignInkBarToSelectedTab()};this._keyManager=new Fs(this._items).withHorizontalOrientation(this._getLayoutDirection()).withHomeAndEnd().withWrap().skipPredicate(()=>!1),this._keyManager.updateActiveItem(Math.max(this._selectedIndex,0)),vt(o,{injector:this._injector}),it(e,r,i,this._items.changes,this._itemsResized()).pipe(xe(this._destroyed)).subscribe(()=>{this._ngZone.run(()=>{Promise.resolve().then(()=>{this._scrollDistance=Math.max(0,Math.min(this._getMaxScrollDistance(),this._scrollDistance)),o()})}),this._keyManager?.withHorizontalOrientation(this._getLayoutDirection())}),this._keyManager.change.subscribe(a=>{this.indexFocused.emit(a),this._setTabFocus(a)})}_itemsResized(){return typeof ResizeObserver!="function"?zi:this._items.changes.pipe(Ue(this._items),je(e=>new Ne(i=>this._ngZone.runOutsideAngular(()=>{let r=new ResizeObserver(o=>i.next(o));return e.forEach(o=>r.observe(o.elementRef.nativeElement)),()=>{r.disconnect()}}))),us(1),ce(e=>e.some(i=>i.contentRect.width>0&&i.contentRect.height>0)))}ngAfterContentChecked(){this._tabLabelCount!=this._items.length&&(this.updatePagination(),this._tabLabelCount=this._items.length,this._changeDetectorRef.markForCheck()),this._selectedIndexChanged&&(this._scrollToLabel(this._selectedIndex),this._checkScrollingControls(),this._alignInkBarToSelectedTab(),this._selectedIndexChanged=!1,this._changeDetectorRef.markForCheck()),this._scrollDistanceChanged&&(this._updateTabScrollPosition(),this._scrollDistanceChanged=!1,this._changeDetectorRef.markForCheck())}ngOnDestroy(){this._eventCleanups.forEach(e=>e()),this._keyManager?.destroy(),this._destroyed.next(),this._destroyed.complete(),this._stopScrolling.complete()}_handleKeydown(e){if(!Gt(e))switch(e.keyCode){case 13:case 32:if(this.focusIndex!==this.selectedIndex){let i=this._items.get(this.focusIndex);i&&!i.disabled&&(this.selectFocusedIndex.emit(this.focusIndex),this._itemSelected(e))}break;default:this._keyManager?.onKeydown(e)}}_onContentChanges(){let e=this._elementRef.nativeElement.textContent;e!==this._currentTextContent&&(this._currentTextContent=e||"",this._ngZone.run(()=>{this.updatePagination(),this._alignInkBarToSelectedTab(),this._changeDetectorRef.markForCheck()}))}updatePagination(){this._checkPaginationEnabled(),this._checkScrollingControls(),this._updateTabScrollPosition()}get focusIndex(){return this._keyManager?this._keyManager.activeItemIndex:0}set focusIndex(e){!this._isValidIndex(e)||this.focusIndex===e||!this._keyManager||this._keyManager.setActiveItem(e)}_isValidIndex(e){return this._items?!!this._items.toArray()[e]:!0}_setTabFocus(e){if(this._showPaginationControls&&this._scrollToLabel(e),this._items&&this._items.length){this._items.toArray()[e].focus();let i=this._tabListContainer.nativeElement;this._getLayoutDirection()=="ltr"?i.scrollLeft=0:i.scrollLeft=i.scrollWidth-i.offsetWidth}}_getLayoutDirection(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}_updateTabScrollPosition(){if(this.disablePagination)return;let e=this.scrollDistance,i=this._getLayoutDirection()==="ltr"?-e:e;this._tabList.nativeElement.style.transform=`translateX(${Math.round(i)}px)`,(this._platform.TRIDENT||this._platform.EDGE)&&(this._tabListContainer.nativeElement.scrollLeft=0)}get scrollDistance(){return this._scrollDistance}set scrollDistance(e){this._scrollTo(e)}_scrollHeader(e){let i=this._tabListContainer.nativeElement.offsetWidth,r=(e=="before"?-1:1)*i/3;return this._scrollTo(this._scrollDistance+r)}_handlePaginatorClick(e){this._stopInterval(),this._scrollHeader(e)}_scrollToLabel(e){if(this.disablePagination)return;let i=this._items?this._items.toArray()[e]:null;if(!i)return;let r=this._tabListContainer.nativeElement.offsetWidth,{offsetLeft:o,offsetWidth:a}=i.elementRef.nativeElement,s,l;this._getLayoutDirection()=="ltr"?(s=o,l=s+a):(l=this._tabListInner.nativeElement.offsetWidth-o,s=l-a);let c=this.scrollDistance,d=this.scrollDistance+r;sd&&(this.scrollDistance+=Math.min(l-d,s-c))}_checkPaginationEnabled(){if(this.disablePagination)this._showPaginationControls=!1;else{let e=this._tabListInner.nativeElement.scrollWidth,i=this._elementRef.nativeElement.offsetWidth,r=e-i>=5;r||(this.scrollDistance=0),r!==this._showPaginationControls&&(this._showPaginationControls=r,this._changeDetectorRef.markForCheck())}}_checkScrollingControls(){this.disablePagination?this._disableScrollAfter=this._disableScrollBefore=!0:(this._disableScrollBefore=this.scrollDistance==0,this._disableScrollAfter=this.scrollDistance==this._getMaxScrollDistance(),this._changeDetectorRef.markForCheck())}_getMaxScrollDistance(){let e=this._tabListInner.nativeElement.scrollWidth,i=this._tabListContainer.nativeElement.offsetWidth;return e-i||0}_alignInkBarToSelectedTab(){let e=this._items&&this._items.length?this._items.toArray()[this.selectedIndex]:null,i=e?e.elementRef.nativeElement:null;i?this._inkBar.alignToElement(i):this._inkBar.hide()}_stopInterval(){this._stopScrolling.next()}_handlePaginatorPress(e,i){i&&i.button!=null&&i.button!==0||(this._stopInterval(),ds(Zfe,Xfe).pipe(xe(it(this._stopScrolling,this._destroyed))).subscribe(()=>{let{maxScrollDistance:r,distance:o}=this._scrollHeader(e);(o===0||o>=r)&&this._stopInterval()}))}_scrollTo(e){if(this.disablePagination)return{maxScrollDistance:0,distance:0};let i=this._getMaxScrollDistance();return this._scrollDistance=Math.max(0,Math.min(i,e)),this._scrollDistanceChanged=!0,this._checkScrollingControls(),{maxScrollDistance:i,distance:this._scrollDistance}}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,inputs:{disablePagination:[2,"disablePagination","disablePagination",B],selectedIndex:[2,"selectedIndex","selectedIndex",ht]},outputs:{selectFocusedIndex:"selectFocusedIndex",indexFocused:"indexFocused"}})}return t})(),Jfe=(()=>{class t extends W8{_items;_tabListContainer;_tabList;_tabListInner;_nextPaginator;_previousPaginator;_inkBar;ariaLabel;ariaLabelledby;disableRipple=!1;ngAfterContentInit(){this._inkBar=new lw(this._items),super.ngAfterContentInit()}_itemSelected(e){e.preventDefault()}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-tab-header"]],contentQueries:function(i,r,o){if(i&1&&Ce(o,$8,4),i&2){let a;j(a=H())&&(r._items=a)}},viewQuery:function(i,r){if(i&1&&(ie(N8,7),ie(L8,7),ie(V8,7),ie(B8,5),ie(j8,5)),i&2){let o;j(o=H())&&(r._tabListContainer=o.first),j(o=H())&&(r._tabList=o.first),j(o=H())&&(r._tabListInner=o.first),j(o=H())&&(r._nextPaginator=o.first),j(o=H())&&(r._previousPaginator=o.first)}},hostAttrs:[1,"mat-mdc-tab-header"],hostVars:4,hostBindings:function(i,r){i&2&&G("mat-mdc-tab-header-pagination-controls-enabled",r._showPaginationControls)("mat-mdc-tab-header-rtl",r._getLayoutDirection()=="rtl")},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],disableRipple:[2,"disableRipple","disableRipple",B]},features:[le],ngContentSelectors:lp,decls:13,vars:10,consts:[["previousPaginator",""],["tabListContainer",""],["tabList",""],["tabListInner",""],["nextPaginator",""],["mat-ripple","",1,"mat-mdc-tab-header-pagination","mat-mdc-tab-header-pagination-before",3,"click","mousedown","touchend","matRippleDisabled"],[1,"mat-mdc-tab-header-pagination-chevron"],[1,"mat-mdc-tab-label-container",3,"keydown"],["role","tablist",1,"mat-mdc-tab-list",3,"cdkObserveContent"],[1,"mat-mdc-tab-labels"],["mat-ripple","",1,"mat-mdc-tab-header-pagination","mat-mdc-tab-header-pagination-after",3,"mousedown","click","touchend","matRippleDisabled"]],template:function(i,r){if(i&1){let o=q();Ee(),m(0,"div",5,0),S("click",function(){return k(o),T(r._handlePaginatorClick("before"))})("mousedown",function(s){return k(o),T(r._handlePaginatorPress("before",s))})("touchend",function(){return k(o),T(r._stopInterval())}),M(2,"div",6),h(),m(3,"div",7,1),S("keydown",function(s){return k(o),T(r._handleKeydown(s))}),m(5,"div",8,2),S("cdkObserveContent",function(){return k(o),T(r._onContentChanges())}),m(7,"div",9,3),ne(9),h()()(),m(10,"div",10,4),S("mousedown",function(s){return k(o),T(r._handlePaginatorPress("after",s))})("click",function(){return k(o),T(r._handlePaginatorClick("after"))})("touchend",function(){return k(o),T(r._stopInterval())}),M(12,"div",6),h()}i&2&&(G("mat-mdc-tab-header-pagination-disabled",r._disableScrollBefore),v("matRippleDisabled",r._disableScrollBefore||r.disableRipple),g(3),G("_mat-animation-noopable",r._animationsDisabled),g(2),X("aria-label",r.ariaLabel||null)("aria-labelledby",r.ariaLabelledby||null),g(5),G("mat-mdc-tab-header-pagination-disabled",r._disableScrollAfter),v("matRippleDisabled",r._disableScrollAfter||r.disableRipple))},dependencies:[qn,Zf],styles:[`.mat-mdc-tab-header{display:flex;overflow:hidden;position:relative;flex-shrink:0}.mdc-tab-indicator .mdc-tab-indicator__content{transition-duration:var(--mat-tab-animation-duration, 250ms)}.mat-mdc-tab-header-pagination{-webkit-user-select:none;user-select:none;position:relative;display:none;justify-content:center;align-items:center;min-width:32px;cursor:pointer;z-index:2;-webkit-tap-highlight-color:rgba(0,0,0,0);touch-action:none;box-sizing:content-box;outline:0}.mat-mdc-tab-header-pagination::-moz-focus-inner{border:0}.mat-mdc-tab-header-pagination .mat-ripple-element{opacity:.12;background-color:var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-tab-header-pagination-controls-enabled .mat-mdc-tab-header-pagination{display:flex}.mat-mdc-tab-header-pagination-before,.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-after{padding-left:4px}.mat-mdc-tab-header-pagination-before .mat-mdc-tab-header-pagination-chevron,.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-after .mat-mdc-tab-header-pagination-chevron{transform:rotate(-135deg)}.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-before,.mat-mdc-tab-header-pagination-after{padding-right:4px}.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-before .mat-mdc-tab-header-pagination-chevron,.mat-mdc-tab-header-pagination-after .mat-mdc-tab-header-pagination-chevron{transform:rotate(45deg)}.mat-mdc-tab-header-pagination-chevron{border-style:solid;border-width:2px 2px 0 0;height:8px;width:8px;border-color:var(--mat-tab-pagination-icon-color, var(--mat-sys-on-surface))}.mat-mdc-tab-header-pagination-disabled{box-shadow:none;cursor:default;pointer-events:none}.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron{opacity:.4}.mat-mdc-tab-list{flex-grow:1;position:relative;transition:transform 500ms cubic-bezier(0.35, 0, 0.25, 1)}._mat-animation-noopable .mat-mdc-tab-list{transition:none}.mat-mdc-tab-label-container{display:flex;flex-grow:1;overflow:hidden;z-index:1;border-bottom-style:solid;border-bottom-width:var(--mat-tab-divider-height, 1px);border-bottom-color:var(--mat-tab-divider-color, var(--mat-sys-surface-variant))}.mat-mdc-tab-group-inverted-header .mat-mdc-tab-label-container{border-bottom:none;border-top-style:solid;border-top-width:var(--mat-tab-divider-height, 1px);border-top-color:var(--mat-tab-divider-color, var(--mat-sys-surface-variant))}.mat-mdc-tab-labels{display:flex;flex:1 0 auto}[mat-align-tabs=center]>.mat-mdc-tab-header .mat-mdc-tab-labels{justify-content:center}[mat-align-tabs=end]>.mat-mdc-tab-header .mat-mdc-tab-labels{justify-content:flex-end}.cdk-drop-list .mat-mdc-tab-labels,.mat-mdc-tab-labels.cdk-drop-list{min-height:var(--mat-tab-container-height, 48px)}.mat-mdc-tab::before{margin:5px}@media(forced-colors: active){.mat-mdc-tab[aria-disabled=true]{color:GrayText}} +`],encapsulation:2})}return t})(),G8=new O("MAT_TABS_CONFIG"),F8=(()=>{class t extends Ir{_host=u(BI);_ngZone=u(ae);_centeringSub=ke.EMPTY;_leavingSub=ke.EMPTY;constructor(){super()}ngOnInit(){super.ngOnInit(),this._centeringSub=this._host._beforeCentering.pipe(Ue(this._host._isCenterPosition())).subscribe(e=>{this._host._content&&e&&!this.hasAttached()&&this._ngZone.run(()=>{Promise.resolve().then(),this.attach(this._host._content)})}),this._leavingSub=this._host._afterLeavingCenter.subscribe(()=>{this._host.preserveContent||this._ngZone.run(()=>this.detach())})}ngOnDestroy(){super.ngOnDestroy(),this._centeringSub.unsubscribe(),this._leavingSub.unsubscribe()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matTabBodyHost",""]],features:[le]})}return t})(),BI=(()=>{class t{_elementRef=u(Y);_dir=u(Yt,{optional:!0});_ngZone=u(ae);_injector=u(de);_renderer=u(ze);_diAnimationsDisabled=Qe();_eventCleanups;_initialized;_fallbackTimer;_positionIndex;_dirChangeSubscription=ke.EMPTY;_position;_previousPosition;_onCentering=new U;_beforeCentering=new U;_afterLeavingCenter=new U;_onCentered=new U(!0);_portalHost;_contentElement;_content;animationDuration="500ms";preserveContent=!1;set position(e){this._positionIndex=e,this._computePositionAnimationState()}constructor(){if(this._dir){let e=u(ye);this._dirChangeSubscription=this._dir.change.subscribe(i=>{this._computePositionAnimationState(i),e.markForCheck()})}}ngOnInit(){this._bindTransitionEvents(),this._position==="center"&&(this._setActiveClass(!0),vt(()=>this._onCentering.emit(this._elementRef.nativeElement.clientHeight),{injector:this._injector})),this._initialized=!0}ngOnDestroy(){clearTimeout(this._fallbackTimer),this._eventCleanups?.forEach(e=>e()),this._dirChangeSubscription.unsubscribe()}_bindTransitionEvents(){this._ngZone.runOutsideAngular(()=>{let e=this._elementRef.nativeElement,i=r=>{r.target===this._contentElement?.nativeElement&&(this._elementRef.nativeElement.classList.remove("mat-tab-body-animating"),r.type==="transitionend"&&this._transitionDone())};this._eventCleanups=[this._renderer.listen(e,"transitionstart",r=>{r.target===this._contentElement?.nativeElement&&(this._elementRef.nativeElement.classList.add("mat-tab-body-animating"),this._transitionStarted())}),this._renderer.listen(e,"transitionend",i),this._renderer.listen(e,"transitioncancel",i)]})}_transitionStarted(){clearTimeout(this._fallbackTimer);let e=this._position==="center";this._beforeCentering.emit(e),e&&this._onCentering.emit(this._elementRef.nativeElement.clientHeight)}_transitionDone(){this._position==="center"?this._onCentered.emit():this._previousPosition==="center"&&this._afterLeavingCenter.emit()}_setActiveClass(e){this._elementRef.nativeElement.classList.toggle("mat-mdc-tab-body-active",e)}_getLayoutDirection(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}_isCenterPosition(){return this._positionIndex===0}_computePositionAnimationState(e=this._getLayoutDirection()){this._previousPosition=this._position,this._positionIndex<0?this._position=e=="ltr"?"left":"right":this._positionIndex>0?this._position=e=="ltr"?"right":"left":this._position="center",this._animationsDisabled()?this._simulateTransitionEvents():this._initialized&&(this._position==="center"||this._previousPosition==="center")&&(clearTimeout(this._fallbackTimer),this._fallbackTimer=this._ngZone.runOutsideAngular(()=>setTimeout(()=>this._simulateTransitionEvents(),100)))}_simulateTransitionEvents(){this._transitionStarted(),vt(()=>this._transitionDone(),{injector:this._injector})}_animationsDisabled(){return this._diAnimationsDisabled||this.animationDuration==="0ms"||this.animationDuration==="0s"}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-tab-body"]],viewQuery:function(i,r){if(i&1&&(ie(F8,5),ie(Ffe,5)),i&2){let o;j(o=H())&&(r._portalHost=o.first),j(o=H())&&(r._contentElement=o.first)}},hostAttrs:[1,"mat-mdc-tab-body"],hostVars:1,hostBindings:function(i,r){i&2&&X("inert",r._position==="center"?null:"")},inputs:{_content:[0,"content","_content"],animationDuration:"animationDuration",preserveContent:"preserveContent",position:"position"},outputs:{_onCentering:"_onCentering",_beforeCentering:"_beforeCentering",_onCentered:"_onCentered"},decls:3,vars:6,consts:[["content",""],["cdkScrollable","",1,"mat-mdc-tab-body-content"],["matTabBodyHost",""]],template:function(i,r){i&1&&(m(0,"div",1,0),A(2,Nfe,0,0,"ng-template",2),h()),i&2&&G("mat-tab-body-content-left",r._position==="left")("mat-tab-body-content-right",r._position==="right")("mat-tab-body-content-can-animate",r._position==="center"||r._previousPosition==="center")},dependencies:[F8,Xa],styles:[`.mat-mdc-tab-body{top:0;left:0;right:0;bottom:0;position:absolute;display:block;overflow:hidden;outline:0;flex-basis:100%}.mat-mdc-tab-body.mat-mdc-tab-body-active{position:relative;overflow-x:hidden;overflow-y:auto;z-index:1;flex-grow:1}.mat-mdc-tab-group.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body.mat-mdc-tab-body-active{overflow-y:hidden}.mat-mdc-tab-body-content{height:100%;overflow:auto;transform:none;visibility:hidden}.mat-tab-body-animating>.mat-mdc-tab-body-content,.mat-mdc-tab-body-active>.mat-mdc-tab-body-content{visibility:visible}.mat-tab-body-animating>.mat-mdc-tab-body-content{min-height:1px}.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body-content{overflow:hidden}.mat-tab-body-content-can-animate{transition:transform var(--mat-tab-animation-duration) 1ms cubic-bezier(0.35, 0, 0.25, 1)}.mat-mdc-tab-body-wrapper._mat-animation-noopable .mat-tab-body-content-can-animate{transition:none}.mat-tab-body-content-left{transform:translate3d(-100%, 0, 0)}.mat-tab-body-content-right{transform:translate3d(100%, 0, 0)} +`],encapsulation:2})}return t})(),cw=(()=>{class t{_elementRef=u(Y);_changeDetectorRef=u(ye);_ngZone=u(ae);_tabsSubscription=ke.EMPTY;_tabLabelSubscription=ke.EMPTY;_tabBodySubscription=ke.EMPTY;_diAnimationsDisabled=Qe();_allTabs;_tabBodies;_tabBodyWrapper;_tabHeader;_tabs=new Dr;_indexToSelect=0;_lastFocusedTabIndex=null;_tabBodyWrapperHeight=0;color;get fitInkBarToContent(){return this._fitInkBarToContent}set fitInkBarToContent(e){this._fitInkBarToContent=e,this._changeDetectorRef.markForCheck()}_fitInkBarToContent=!1;stretchTabs=!0;alignTabs=null;dynamicHeight=!1;get selectedIndex(){return this._selectedIndex}set selectedIndex(e){this._indexToSelect=isNaN(e)?null:e}_selectedIndex=null;headerPosition="above";get animationDuration(){return this._animationDuration}set animationDuration(e){let i=e+"";this._animationDuration=/^\d+$/.test(i)?e+"ms":i}_animationDuration;get contentTabIndex(){return this._contentTabIndex}set contentTabIndex(e){this._contentTabIndex=isNaN(e)?null:e}_contentTabIndex;disablePagination=!1;disableRipple=!1;preserveContent=!1;get backgroundColor(){return this._backgroundColor}set backgroundColor(e){let i=this._elementRef.nativeElement.classList;i.remove("mat-tabs-with-background",`mat-background-${this.backgroundColor}`),e&&i.add("mat-tabs-with-background",`mat-background-${e}`),this._backgroundColor=e}_backgroundColor;ariaLabel;ariaLabelledby;selectedIndexChange=new U;focusChange=new U;animationDone=new U;selectedTabChange=new U(!0);_groupId;_isServer=!u(Ye).isBrowser;constructor(){let e=u(G8,{optional:!0});this._groupId=u(et).getId("mat-tab-group-"),this.animationDuration=e&&e.animationDuration?e.animationDuration:"500ms",this.disablePagination=e&&e.disablePagination!=null?e.disablePagination:!1,this.dynamicHeight=e&&e.dynamicHeight!=null?e.dynamicHeight:!1,e?.contentTabIndex!=null&&(this.contentTabIndex=e.contentTabIndex),this.preserveContent=!!e?.preserveContent,this.fitInkBarToContent=e&&e.fitInkBarToContent!=null?e.fitInkBarToContent:!1,this.stretchTabs=e&&e.stretchTabs!=null?e.stretchTabs:!0,this.alignTabs=e&&e.alignTabs!=null?e.alignTabs:null}ngAfterContentChecked(){let e=this._indexToSelect=this._clampTabIndex(this._indexToSelect);if(this._selectedIndex!=e){let i=this._selectedIndex==null;if(!i){this.selectedTabChange.emit(this._createChangeEvent(e));let r=this._tabBodyWrapper.nativeElement;r.style.minHeight=r.clientHeight+"px"}Promise.resolve().then(()=>{this._tabs.forEach((r,o)=>r.isActive=o===e),i||(this.selectedIndexChange.emit(e),this._tabBodyWrapper.nativeElement.style.minHeight="")})}this._tabs.forEach((i,r)=>{i.position=r-e,this._selectedIndex!=null&&i.position==0&&!i.origin&&(i.origin=e-this._selectedIndex)}),this._selectedIndex!==e&&(this._selectedIndex=e,this._lastFocusedTabIndex=null,this._changeDetectorRef.markForCheck())}ngAfterContentInit(){this._subscribeToAllTabChanges(),this._subscribeToTabLabels(),this._tabsSubscription=this._tabs.changes.subscribe(()=>{let e=this._clampTabIndex(this._indexToSelect);if(e===this._selectedIndex){let i=this._tabs.toArray(),r;for(let o=0;o{i[e].isActive=!0,this.selectedTabChange.emit(this._createChangeEvent(e))})}this._changeDetectorRef.markForCheck()})}ngAfterViewInit(){this._tabBodySubscription=this._tabBodies.changes.subscribe(()=>this._bodyCentered(!0))}_subscribeToAllTabChanges(){this._allTabs.changes.pipe(Ue(this._allTabs)).subscribe(e=>{this._tabs.reset(e.filter(i=>i._closestTabGroup===this||!i._closestTabGroup)),this._tabs.notifyOnChanges()})}ngOnDestroy(){this._tabs.destroy(),this._tabsSubscription.unsubscribe(),this._tabLabelSubscription.unsubscribe(),this._tabBodySubscription.unsubscribe()}realignInkBar(){this._tabHeader&&this._tabHeader._alignInkBarToSelectedTab()}updatePagination(){this._tabHeader&&this._tabHeader.updatePagination()}focusTab(e){let i=this._tabHeader;i&&(i.focusIndex=e)}_focusChanged(e){this._lastFocusedTabIndex=e,this.focusChange.emit(this._createChangeEvent(e))}_createChangeEvent(e){let i=new jI;return i.index=e,this._tabs&&this._tabs.length&&(i.tab=this._tabs.toArray()[e]),i}_subscribeToTabLabels(){this._tabLabelSubscription&&this._tabLabelSubscription.unsubscribe(),this._tabLabelSubscription=it(...this._tabs.map(e=>e._stateChanges)).subscribe(()=>this._changeDetectorRef.markForCheck())}_clampTabIndex(e){return Math.min(this._tabs.length-1,Math.max(e||0,0))}_getTabLabelId(e,i){return e.id||`${this._groupId}-label-${i}`}_getTabContentId(e){return`${this._groupId}-content-${e}`}_setTabBodyWrapperHeight(e){if(!this.dynamicHeight||!this._tabBodyWrapperHeight){this._tabBodyWrapperHeight=e;return}let i=this._tabBodyWrapper.nativeElement;i.style.height=this._tabBodyWrapperHeight+"px",this._tabBodyWrapper.nativeElement.offsetHeight&&(i.style.height=e+"px")}_removeTabBodyWrapperHeight(){let e=this._tabBodyWrapper.nativeElement;this._tabBodyWrapperHeight=e.clientHeight,e.style.height="",this._ngZone.run(()=>this.animationDone.emit())}_handleClick(e,i,r){i.focusIndex=r,e.disabled||(this.selectedIndex=r)}_getTabIndex(e){let i=this._lastFocusedTabIndex??this.selectedIndex;return e===i?0:-1}_tabFocusChanged(e,i){e&&e!=="mouse"&&e!=="touch"&&(this._tabHeader.focusIndex=i)}_bodyCentered(e){e&&this._tabBodies?.forEach((i,r)=>i._setActiveClass(r===this._selectedIndex))}_animationsDisabled(){return this._diAnimationsDisabled||this.animationDuration==="0"||this.animationDuration==="0ms"}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-tab-group"]],contentQueries:function(i,r,o){if(i&1&&Ce(o,C_,5),i&2){let a;j(a=H())&&(r._allTabs=a)}},viewQuery:function(i,r){if(i&1&&(ie(Lfe,5),ie(Vfe,5),ie(BI,5)),i&2){let o;j(o=H())&&(r._tabBodyWrapper=o.first),j(o=H())&&(r._tabHeader=o.first),j(o=H())&&(r._tabBodies=o)}},hostAttrs:[1,"mat-mdc-tab-group"],hostVars:11,hostBindings:function(i,r){i&2&&(X("mat-align-tabs",r.alignTabs),at("mat-"+(r.color||"primary")),At("--mat-tab-animation-duration",r.animationDuration),G("mat-mdc-tab-group-dynamic-height",r.dynamicHeight)("mat-mdc-tab-group-inverted-header",r.headerPosition==="below")("mat-mdc-tab-group-stretch-tabs",r.stretchTabs))},inputs:{color:"color",fitInkBarToContent:[2,"fitInkBarToContent","fitInkBarToContent",B],stretchTabs:[2,"mat-stretch-tabs","stretchTabs",B],alignTabs:[0,"mat-align-tabs","alignTabs"],dynamicHeight:[2,"dynamicHeight","dynamicHeight",B],selectedIndex:[2,"selectedIndex","selectedIndex",ht],headerPosition:"headerPosition",animationDuration:"animationDuration",contentTabIndex:[2,"contentTabIndex","contentTabIndex",ht],disablePagination:[2,"disablePagination","disablePagination",B],disableRipple:[2,"disableRipple","disableRipple",B],preserveContent:[2,"preserveContent","preserveContent",B],backgroundColor:"backgroundColor",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"]},outputs:{selectedIndexChange:"selectedIndexChange",focusChange:"focusChange",animationDone:"animationDone",selectedTabChange:"selectedTabChange"},exportAs:["matTabGroup"],features:[we([{provide:z8,useExisting:t}])],ngContentSelectors:lp,decls:9,vars:8,consts:[["tabHeader",""],["tabBodyWrapper",""],["tabNode",""],[3,"indexFocused","selectFocusedIndex","selectedIndex","disableRipple","disablePagination","aria-label","aria-labelledby"],["role","tab","matTabLabelWrapper","","cdkMonitorElementFocus","",1,"mdc-tab","mat-mdc-tab","mat-focus-indicator",3,"id","mdc-tab--active","class","disabled","fitInkBarToContent"],[1,"mat-mdc-tab-body-wrapper"],["role","tabpanel",3,"id","class","content","position","animationDuration","preserveContent"],["role","tab","matTabLabelWrapper","","cdkMonitorElementFocus","",1,"mdc-tab","mat-mdc-tab","mat-focus-indicator",3,"click","cdkFocusChange","id","disabled","fitInkBarToContent"],[1,"mdc-tab__ripple"],["mat-ripple","",1,"mat-mdc-tab-ripple",3,"matRippleTrigger","matRippleDisabled"],[1,"mdc-tab__content"],[1,"mdc-tab__text-label"],[3,"cdkPortalOutlet"],["role","tabpanel",3,"_onCentered","_onCentering","_beforeCentering","id","content","position","animationDuration","preserveContent"]],template:function(i,r){if(i&1){let o=q();Ee(),m(0,"mat-tab-header",3,0),S("indexFocused",function(s){return k(o),T(r._focusChanged(s))})("selectFocusedIndex",function(s){return k(o),T(r.selectedIndex=s)}),Mt(2,zfe,8,17,"div",4,Em),h(),L(4,Ufe,1,0),m(5,"div",5,1),Mt(7,$fe,1,10,"mat-tab-body",6,Em),h()}i&2&&(v("selectedIndex",r.selectedIndex||0)("disableRipple",r.disableRipple)("disablePagination",r.disablePagination),pc("aria-label",r.ariaLabel)("aria-labelledby",r.ariaLabelledby),g(2),Et(r._tabs),g(2),V(r._isServer?4:-1),g(),G("_mat-animation-noopable",r._animationsDisabled()),g(2),Et(r._tabs))},dependencies:[Jfe,$8,Kf,qn,Ir,BI],styles:[`.mdc-tab{min-width:90px;padding:0 24px;display:flex;flex:1 0 auto;justify-content:center;box-sizing:border-box;border:none;outline:none;text-align:center;white-space:nowrap;cursor:pointer;z-index:1;touch-action:manipulation}.mdc-tab__content{display:flex;align-items:center;justify-content:center;height:inherit;pointer-events:none}.mdc-tab__text-label{transition:150ms color linear;display:inline-block;line-height:1;z-index:2}.mdc-tab--active .mdc-tab__text-label{transition-delay:100ms}._mat-animation-noopable .mdc-tab__text-label{transition:none}.mdc-tab-indicator{display:flex;position:absolute;top:0;left:0;justify-content:center;width:100%;height:100%;pointer-events:none;z-index:1}.mdc-tab-indicator__content{transition:var(--mat-tab-animation-duration, 250ms) transform cubic-bezier(0.4, 0, 0.2, 1);transform-origin:left;opacity:0}.mdc-tab-indicator__content--underline{align-self:flex-end;box-sizing:border-box;width:100%;border-top-style:solid}.mdc-tab-indicator--active .mdc-tab-indicator__content{opacity:1}._mat-animation-noopable .mdc-tab-indicator__content,.mdc-tab-indicator--no-transition .mdc-tab-indicator__content{transition:none}.mat-mdc-tab-ripple.mat-mdc-tab-ripple{position:absolute;top:0;left:0;bottom:0;right:0;pointer-events:none}.mat-mdc-tab{-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none;background:none;height:var(--mat-tab-container-height, 48px);font-family:var(--mat-tab-label-text-font, var(--mat-sys-title-small-font));font-size:var(--mat-tab-label-text-size, var(--mat-sys-title-small-size));letter-spacing:var(--mat-tab-label-text-tracking, var(--mat-sys-title-small-tracking));line-height:var(--mat-tab-label-text-line-height, var(--mat-sys-title-small-line-height));font-weight:var(--mat-tab-label-text-weight, var(--mat-sys-title-small-weight))}.mat-mdc-tab.mdc-tab{flex-grow:0}.mat-mdc-tab .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-active-indicator-color, var(--mat-sys-primary));border-top-width:var(--mat-tab-active-indicator-height, 2px);border-radius:var(--mat-tab-active-indicator-shape, 0)}.mat-mdc-tab:hover .mdc-tab__text-label{color:var(--mat-tab-inactive-hover-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab:focus .mdc-tab__text-label{color:var(--mat-tab-inactive-focus-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab.mdc-tab--active .mdc-tab__text-label{color:var(--mat-tab-active-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab.mdc-tab--active .mdc-tab__ripple::before,.mat-mdc-tab.mdc-tab--active .mat-ripple-element{background-color:var(--mat-tab-active-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-tab.mdc-tab--active:hover .mdc-tab__text-label{color:var(--mat-tab-active-hover-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab.mdc-tab--active:hover .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-active-hover-indicator-color, var(--mat-sys-primary))}.mat-mdc-tab.mdc-tab--active:focus .mdc-tab__text-label{color:var(--mat-tab-active-focus-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab.mdc-tab--active:focus .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-active-focus-indicator-color, var(--mat-sys-primary))}.mat-mdc-tab.mat-mdc-tab-disabled{opacity:.4;pointer-events:none}.mat-mdc-tab.mat-mdc-tab-disabled .mdc-tab__content{pointer-events:none}.mat-mdc-tab.mat-mdc-tab-disabled .mdc-tab__ripple::before,.mat-mdc-tab.mat-mdc-tab-disabled .mat-ripple-element{background-color:var(--mat-tab-disabled-ripple-color, var(--mat-sys-on-surface-variant))}.mat-mdc-tab .mdc-tab__ripple::before{content:"";display:block;position:absolute;top:0;left:0;right:0;bottom:0;opacity:0;pointer-events:none;background-color:var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-tab .mdc-tab__text-label{color:var(--mat-tab-inactive-label-text-color, var(--mat-sys-on-surface));display:inline-flex;align-items:center}.mat-mdc-tab .mdc-tab__content{position:relative;pointer-events:auto}.mat-mdc-tab:hover .mdc-tab__ripple::before{opacity:.04}.mat-mdc-tab.cdk-program-focused .mdc-tab__ripple::before,.mat-mdc-tab.cdk-keyboard-focused .mdc-tab__ripple::before{opacity:.12}.mat-mdc-tab .mat-ripple-element{opacity:.12;background-color:var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-tab-group.mat-mdc-tab-group-stretch-tabs>.mat-mdc-tab-header .mat-mdc-tab{flex-grow:1}.mat-mdc-tab-group{display:flex;flex-direction:column;max-width:100%}.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header,.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header-pagination{background-color:var(--mat-tab-background-color)}.mat-mdc-tab-group.mat-tabs-with-background.mat-primary>.mat-mdc-tab-header .mat-mdc-tab .mdc-tab__text-label{color:var(--mat-tab-foreground-color)}.mat-mdc-tab-group.mat-tabs-with-background.mat-primary>.mat-mdc-tab-header .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-foreground-color)}.mat-mdc-tab-group.mat-tabs-with-background:not(.mat-primary)>.mat-mdc-tab-header .mat-mdc-tab:not(.mdc-tab--active) .mdc-tab__text-label{color:var(--mat-tab-foreground-color)}.mat-mdc-tab-group.mat-tabs-with-background:not(.mat-primary)>.mat-mdc-tab-header .mat-mdc-tab:not(.mdc-tab--active) .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-foreground-color)}.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header .mat-mdc-tab-header-pagination-chevron,.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header .mat-focus-indicator::before,.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron,.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mat-focus-indicator::before{border-color:var(--mat-tab-foreground-color)}.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header .mat-ripple-element,.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header .mdc-tab__ripple::before,.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mat-ripple-element,.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mdc-tab__ripple::before{background-color:var(--mat-tab-foreground-color)}.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header .mat-mdc-tab-header-pagination-chevron,.mat-mdc-tab-group.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron{color:var(--mat-tab-foreground-color)}.mat-mdc-tab-group.mat-mdc-tab-group-inverted-header{flex-direction:column-reverse}.mat-mdc-tab-group.mat-mdc-tab-group-inverted-header .mdc-tab-indicator__content--underline{align-self:flex-start}.mat-mdc-tab-body-wrapper{position:relative;overflow:hidden;display:flex;transition:height 500ms cubic-bezier(0.35, 0, 0.25, 1)}.mat-mdc-tab-body-wrapper._mat-animation-noopable{transition:none !important;animation:none !important} +`],encapsulation:2})}return t})(),jI=class{index;tab},HI=(()=>{class t extends W8{_focusedItem=he(null);get fitInkBarToContent(){return this._fitInkBarToContent.value}set fitInkBarToContent(e){this._fitInkBarToContent.next(e),this._changeDetectorRef.markForCheck()}_fitInkBarToContent=new rt(!1);stretchTabs=!0;get animationDuration(){return this._animationDuration}set animationDuration(e){let i=e+"";this._animationDuration=/^\d+$/.test(i)?e+"ms":i}_animationDuration;_items;get backgroundColor(){return this._backgroundColor}set backgroundColor(e){let i=this._elementRef.nativeElement.classList;i.remove("mat-tabs-with-background",`mat-background-${this.backgroundColor}`),e&&i.add("mat-tabs-with-background",`mat-background-${e}`),this._backgroundColor=e}_backgroundColor;get disableRipple(){return this._disableRipple()}set disableRipple(e){this._disableRipple.set(e)}_disableRipple=he(!1);color="primary";tabPanel;_tabListContainer;_tabList;_tabListInner;_nextPaginator;_previousPaginator;_inkBar;constructor(){let e=u(G8,{optional:!0});super(),this.disablePagination=e&&e.disablePagination!=null?e.disablePagination:!1,this.fitInkBarToContent=e&&e.fitInkBarToContent!=null?e.fitInkBarToContent:!1,this.stretchTabs=e&&e.stretchTabs!=null?e.stretchTabs:!0}_itemSelected(){}ngAfterContentInit(){this._inkBar=new lw(this._items),this._items.changes.pipe(Ue(null),xe(this._destroyed)).subscribe(()=>this.updateActiveLink()),super.ngAfterContentInit(),this._keyManager.change.pipe(Ue(null),xe(this._destroyed)).subscribe(()=>this._focusedItem.set(this._keyManager?.activeItem||null))}ngAfterViewInit(){this.tabPanel,super.ngAfterViewInit()}updateActiveLink(){if(!this._items)return;let e=this._items.toArray();for(let i=0;i.mat-mdc-tab-link-container .mat-mdc-tab-links{justify-content:center}[mat-align-tabs=end]>.mat-mdc-tab-link-container .mat-mdc-tab-links{justify-content:flex-end}.cdk-drop-list .mat-mdc-tab-links,.mat-mdc-tab-links.cdk-drop-list{min-height:var(--mat-tab-container-height, 48px)}.mat-mdc-tab-link-container{display:flex;flex-grow:1;overflow:hidden;z-index:1;border-bottom-style:solid;border-bottom-width:var(--mat-tab-divider-height, 1px);border-bottom-color:var(--mat-tab-divider-color, var(--mat-sys-surface-variant))}.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-link-container,.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-header-pagination{background-color:var(--mat-tab-background-color)}.mat-mdc-tab-nav-bar.mat-tabs-with-background.mat-primary>.mat-mdc-tab-link-container .mat-mdc-tab-link .mdc-tab__text-label{color:var(--mat-tab-foreground-color)}.mat-mdc-tab-nav-bar.mat-tabs-with-background.mat-primary>.mat-mdc-tab-link-container .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-foreground-color)}.mat-mdc-tab-nav-bar.mat-tabs-with-background:not(.mat-primary)>.mat-mdc-tab-link-container .mat-mdc-tab-link:not(.mdc-tab--active) .mdc-tab__text-label{color:var(--mat-tab-foreground-color)}.mat-mdc-tab-nav-bar.mat-tabs-with-background:not(.mat-primary)>.mat-mdc-tab-link-container .mat-mdc-tab-link:not(.mdc-tab--active) .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-foreground-color)}.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-link-container .mat-mdc-tab-header-pagination-chevron,.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-link-container .mat-focus-indicator::before,.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron,.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mat-focus-indicator::before{border-color:var(--mat-tab-foreground-color)}.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-link-container .mat-ripple-element,.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-link-container .mdc-tab__ripple::before,.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mat-ripple-element,.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mdc-tab__ripple::before{background-color:var(--mat-tab-foreground-color)}.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-link-container .mat-mdc-tab-header-pagination-chevron,.mat-mdc-tab-nav-bar.mat-tabs-with-background>.mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron{color:var(--mat-tab-foreground-color)} +`],encapsulation:2})}return t})(),ege=(()=>{class t extends U8{_tabNavBar=u(HI);elementRef=u(Y);_focusMonitor=u(oi);_destroyed=new z;_isActive=!1;_tabIndex=ci(()=>this._tabNavBar._focusedItem()===this?this.tabIndex:-1);get active(){return this._isActive}set active(e){e!==this._isActive&&(this._isActive=e,this._tabNavBar.updateActiveLink())}disabled=!1;get disableRipple(){return this._disableRipple()}set disableRipple(e){this._disableRipple.set(e)}_disableRipple=he(!1);tabIndex=0;rippleConfig;get rippleDisabled(){return this.disabled||this.disableRipple||this._tabNavBar.disableRipple||!!this.rippleConfig.disabled}id=u(et).getId("mat-tab-link-");constructor(){super(),u(ft).load(Oi);let e=u(Bs,{optional:!0}),i=u(new Li("tabindex"),{optional:!0});this.rippleConfig=e||{},this.tabIndex=i==null?0:parseInt(i)||0,Qe()&&(this.rippleConfig.animation={enterDuration:0,exitDuration:0}),this._tabNavBar._fitInkBarToContent.pipe(xe(this._destroyed)).subscribe(r=>{this.fitInkBarToContent=r})}focus(){this.elementRef.nativeElement.focus()}ngAfterViewInit(){this._focusMonitor.monitor(this.elementRef)}ngOnDestroy(){this._destroyed.next(),this._destroyed.complete(),super.ngOnDestroy(),this._focusMonitor.stopMonitoring(this.elementRef)}_handleFocus(){this._tabNavBar.focusIndex=this._tabNavBar._items.toArray().indexOf(this)}_handleKeydown(e){(e.keyCode===32||e.keyCode===13)&&(this.disabled?e.preventDefault():this._tabNavBar.tabPanel&&(e.keyCode===32&&e.preventDefault(),this.elementRef.nativeElement.click()))}_getAriaControls(){return this._tabNavBar.tabPanel?this._tabNavBar.tabPanel?.id:this.elementRef.nativeElement.getAttribute("aria-controls")}_getAriaSelected(){return this._tabNavBar.tabPanel?this.active?"true":"false":this.elementRef.nativeElement.getAttribute("aria-selected")}_getAriaCurrent(){return this.active&&!this._tabNavBar.tabPanel?"page":null}_getRole(){return this._tabNavBar.tabPanel?"tab":this.elementRef.nativeElement.getAttribute("role")}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["","mat-tab-link",""],["","matTabLink",""]],hostAttrs:[1,"mdc-tab","mat-mdc-tab-link","mat-focus-indicator"],hostVars:11,hostBindings:function(i,r){i&1&&S("focus",function(){return r._handleFocus()})("keydown",function(a){return r._handleKeydown(a)}),i&2&&(X("aria-controls",r._getAriaControls())("aria-current",r._getAriaCurrent())("aria-disabled",r.disabled)("aria-selected",r._getAriaSelected())("id",r.id)("tabIndex",r._tabIndex())("role",r._getRole()),G("mat-mdc-tab-disabled",r.disabled)("mdc-tab--active",r.active))},inputs:{active:[2,"active","active",B],disabled:[2,"disabled","disabled",B],disableRipple:[2,"disableRipple","disableRipple",B],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:ht(e)],id:"id"},exportAs:["matTabLink"],features:[le],attrs:Gfe,ngContentSelectors:lp,decls:5,vars:2,consts:[[1,"mdc-tab__ripple"],["mat-ripple","",1,"mat-mdc-tab-ripple",3,"matRippleTrigger","matRippleDisabled"],[1,"mdc-tab__content"],[1,"mdc-tab__text-label"]],template:function(i,r){i&1&&(Ee(),M(0,"span",0)(1,"div",1),m(2,"span",2)(3,"span",3),ne(4),h()()),i&2&&(g(),v("matRippleTrigger",r.elementRef.nativeElement)("matRippleDisabled",r.rippleDisabled))},dependencies:[qn],styles:[`.mat-mdc-tab-link{-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-decoration:none;background:none;height:var(--mat-tab-container-height, 48px);font-family:var(--mat-tab-label-text-font, var(--mat-sys-title-small-font));font-size:var(--mat-tab-label-text-size, var(--mat-sys-title-small-size));letter-spacing:var(--mat-tab-label-text-tracking, var(--mat-sys-title-small-tracking));line-height:var(--mat-tab-label-text-line-height, var(--mat-sys-title-small-line-height));font-weight:var(--mat-tab-label-text-weight, var(--mat-sys-title-small-weight))}.mat-mdc-tab-link.mdc-tab{flex-grow:0}.mat-mdc-tab-link .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-active-indicator-color, var(--mat-sys-primary));border-top-width:var(--mat-tab-active-indicator-height, 2px);border-radius:var(--mat-tab-active-indicator-shape, 0)}.mat-mdc-tab-link:hover .mdc-tab__text-label{color:var(--mat-tab-inactive-hover-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab-link:focus .mdc-tab__text-label{color:var(--mat-tab-inactive-focus-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab-link.mdc-tab--active .mdc-tab__text-label{color:var(--mat-tab-active-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab-link.mdc-tab--active .mdc-tab__ripple::before,.mat-mdc-tab-link.mdc-tab--active .mat-ripple-element{background-color:var(--mat-tab-active-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-tab-link.mdc-tab--active:hover .mdc-tab__text-label{color:var(--mat-tab-active-hover-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab-link.mdc-tab--active:hover .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-active-hover-indicator-color, var(--mat-sys-primary))}.mat-mdc-tab-link.mdc-tab--active:focus .mdc-tab__text-label{color:var(--mat-tab-active-focus-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-tab-link.mdc-tab--active:focus .mdc-tab-indicator__content--underline{border-color:var(--mat-tab-active-focus-indicator-color, var(--mat-sys-primary))}.mat-mdc-tab-link.mat-mdc-tab-disabled{opacity:.4;pointer-events:none}.mat-mdc-tab-link.mat-mdc-tab-disabled .mdc-tab__content{pointer-events:none}.mat-mdc-tab-link.mat-mdc-tab-disabled .mdc-tab__ripple::before,.mat-mdc-tab-link.mat-mdc-tab-disabled .mat-ripple-element{background-color:var(--mat-tab-disabled-ripple-color, var(--mat-sys-on-surface-variant))}.mat-mdc-tab-link .mdc-tab__ripple::before{content:"";display:block;position:absolute;top:0;left:0;right:0;bottom:0;opacity:0;pointer-events:none;background-color:var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-tab-link .mdc-tab__text-label{color:var(--mat-tab-inactive-label-text-color, var(--mat-sys-on-surface));display:inline-flex;align-items:center}.mat-mdc-tab-link .mdc-tab__content{position:relative;pointer-events:auto}.mat-mdc-tab-link:hover .mdc-tab__ripple::before{opacity:.04}.mat-mdc-tab-link.cdk-program-focused .mdc-tab__ripple::before,.mat-mdc-tab-link.cdk-keyboard-focused .mdc-tab__ripple::before{opacity:.12}.mat-mdc-tab-link .mat-ripple-element{opacity:.12;background-color:var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface))}.mat-mdc-tab-header.mat-mdc-tab-nav-bar-stretch-tabs .mat-mdc-tab-link{flex-grow:1}.mat-mdc-tab-link::before{margin:5px}@media(max-width: 599px){.mat-mdc-tab-link{min-width:72px}} +`],encapsulation:2,changeDetection:0})}return t})(),q8=(()=>{class t{id=u(et).getId("mat-tab-nav-panel-");_activeTabId;static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-tab-nav-panel"]],hostAttrs:["role","tabpanel",1,"mat-mdc-tab-nav-panel"],hostVars:2,hostBindings:function(i,r){i&2&&X("aria-labelledby",r._activeTabId)("id",r.id)},inputs:{id:"id"},exportAs:["matTabNavPanel"],ngContentSelectors:lp,decls:1,vars:0,template:function(i,r){i&1&&(Ee(),ne(0))},encapsulation:2,changeDetection:0})}return t})(),cp=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,De]})}return t})();function tge(t,n){if(t&1&&(m(0,"div",1)(1,"div"),f(2),h(),m(3,"div",2)(4,"div",3),f(5),h(),m(6,"div",4),f(7),h()()()),t&2){let e=n.$implicit;g(),at(Zo("d-flex align-items-center justify-content-center r-12 ",e.color)),At("width",3,"rem")("height",3,"rem"),g(),fe(" ",e.icon," "),g(3),N(e.title),g(2),N(e.content)}}function ige(t,n){if(t&1&&(m(0,"mat-tab",0),Mt(1,tge,8,10,"div",1,qi),h()),t&2){let e=n.$implicit;v("label",e.label),g(),Et(e.messages)}}var Y8=(()=>{let n=class n{constructor(){this.tabs=[{label:"Today",messages:[{icon:"\u{1F514}",color:"bg-red-95",title:"General Meeting for update",content:"You can use the Dashboard to explore how many new users download reports daily and monthly."},{icon:"\u{1F4E2}",color:"bg-azure-95",title:"Widgets update",content:"We've made some updates to the emendable widget which we think you are going to love."},{icon:"\u23F3",color:"bg-violet-95",title:"Coming soon new features",content:"More new features are coming soon, so stay patient!"}]},{label:"Notifications",messages:[{icon:"\u{1F4E9}",color:"bg-magenta-95",title:"Weekly reports are available",content:"Please go to the notification center to check your reports."}]}]}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-sidebar-notice"]],hostAttrs:[1,"matero-sidebar-notice"],decls:3,vars:0,consts:[[3,"label"],[1,"d-flex","align-items-center","gap-16","p-16","m-b-16","b-1","r-12"],[1,"flex-grow-1","w-0"],[1,"f-w-600"],[1,"f-s-14"]],template:function(r,o){r&1&&(m(0,"mat-tab-group"),Mt(1,ige,3,1,"mat-tab",0,qi),h()),r&2&&(g(),Et(o.tabs))},dependencies:[cp,C_,cw],styles:[`.matero-sidebar-notice{display:block;height:100%}.matero-sidebar-notice .mat-mdc-tab-group{height:100%}.matero-sidebar-notice .mat-mdc-tab-body-wrapper{flex:1}.matero-sidebar-notice .mat-mdc-tab-body-content{padding:1rem} +`],encapsulation:2});let t=n;return t})();var nge=["switch"],rge=["*"];function oge(t,n){t&1&&(m(0,"span",11),ii(),m(1,"svg",13),M(2,"path",14),h(),m(3,"svg",15),M(4,"path",16),h()())}var age=new O("mat-slide-toggle-default-options",{providedIn:"root",factory:()=>({disableToggleValue:!1,hideIcon:!1,disabledInteractive:!1})}),dw=class{source;checked;constructor(n,e){this.source=n,this.checked=e}},UI=(()=>{class t{_elementRef=u(Y);_focusMonitor=u(oi);_changeDetectorRef=u(ye);defaults=u(age);_onChange=e=>{};_onTouched=()=>{};_validatorOnChange=()=>{};_uniqueId;_checked=!1;_createChangeEvent(e){return new dw(this,e)}_labelId;get buttonId(){return`${this.id||this._uniqueId}-button`}_switchElement;focus(){this._switchElement.nativeElement.focus()}_noopAnimations=Qe();_focused;name=null;id;labelPosition="after";ariaLabel=null;ariaLabelledby=null;ariaDescribedby;required;color;disabled=!1;disableRipple=!1;tabIndex=0;get checked(){return this._checked}set checked(e){this._checked=e,this._changeDetectorRef.markForCheck()}hideIcon;disabledInteractive;change=new U;toggleChange=new U;get inputId(){return`${this.id||this._uniqueId}-input`}constructor(){u(ft).load(Oi);let e=u(new Li("tabindex"),{optional:!0}),i=this.defaults;this.tabIndex=e==null?0:parseInt(e)||0,this.color=i.color||"accent",this.id=this._uniqueId=u(et).getId("mat-mdc-slide-toggle-"),this.hideIcon=i.hideIcon??!1,this.disabledInteractive=i.disabledInteractive??!1,this._labelId=this._uniqueId+"-label"}ngAfterContentInit(){this._focusMonitor.monitor(this._elementRef,!0).subscribe(e=>{e==="keyboard"||e==="program"?(this._focused=!0,this._changeDetectorRef.markForCheck()):e||Promise.resolve().then(()=>{this._focused=!1,this._onTouched(),this._changeDetectorRef.markForCheck()})})}ngOnChanges(e){e.required&&this._validatorOnChange()}ngOnDestroy(){this._focusMonitor.stopMonitoring(this._elementRef)}writeValue(e){this.checked=!!e}registerOnChange(e){this._onChange=e}registerOnTouched(e){this._onTouched=e}validate(e){return this.required&&e.value!==!0?{required:!0}:null}registerOnValidatorChange(e){this._validatorOnChange=e}setDisabledState(e){this.disabled=e,this._changeDetectorRef.markForCheck()}toggle(){this.checked=!this.checked,this._onChange(this.checked)}_emitChangeEvent(){this._onChange(this.checked),this.change.emit(this._createChangeEvent(this.checked))}_handleClick(){this.disabled||(this.toggleChange.emit(),this.defaults.disableToggleValue||(this.checked=!this.checked,this._onChange(this.checked),this.change.emit(new dw(this,this.checked))))}_getAriaLabelledBy(){return this.ariaLabelledby?this.ariaLabelledby:this.ariaLabel?null:this._labelId}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-slide-toggle"]],viewQuery:function(i,r){if(i&1&&ie(nge,5),i&2){let o;j(o=H())&&(r._switchElement=o.first)}},hostAttrs:[1,"mat-mdc-slide-toggle"],hostVars:13,hostBindings:function(i,r){i&2&&(pi("id",r.id),X("tabindex",null)("aria-label",null)("name",null)("aria-labelledby",null),at(r.color?"mat-"+r.color:""),G("mat-mdc-slide-toggle-focused",r._focused)("mat-mdc-slide-toggle-checked",r.checked)("_mat-animation-noopable",r._noopAnimations))},inputs:{name:"name",id:"id",labelPosition:"labelPosition",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],ariaDescribedby:[0,"aria-describedby","ariaDescribedby"],required:[2,"required","required",B],color:"color",disabled:[2,"disabled","disabled",B],disableRipple:[2,"disableRipple","disableRipple",B],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:ht(e)],checked:[2,"checked","checked",B],hideIcon:[2,"hideIcon","hideIcon",B],disabledInteractive:[2,"disabledInteractive","disabledInteractive",B]},outputs:{change:"change",toggleChange:"toggleChange"},exportAs:["matSlideToggle"],features:[we([{provide:dr,useExisting:li(()=>t),multi:!0},{provide:sa,useExisting:t,multi:!0}]),Oe],ngContentSelectors:rge,decls:14,vars:27,consts:[["switch",""],["mat-internal-form-field","",3,"labelPosition"],["role","switch","type","button",1,"mdc-switch",3,"click","tabIndex","disabled"],[1,"mat-mdc-slide-toggle-touch-target"],[1,"mdc-switch__track"],[1,"mdc-switch__handle-track"],[1,"mdc-switch__handle"],[1,"mdc-switch__shadow"],[1,"mdc-elevation-overlay"],[1,"mdc-switch__ripple"],["mat-ripple","",1,"mat-mdc-slide-toggle-ripple","mat-focus-indicator",3,"matRippleTrigger","matRippleDisabled","matRippleCentered"],[1,"mdc-switch__icons"],[1,"mdc-label",3,"click","for"],["viewBox","0 0 24 24","aria-hidden","true",1,"mdc-switch__icon","mdc-switch__icon--on"],["d","M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z"],["viewBox","0 0 24 24","aria-hidden","true",1,"mdc-switch__icon","mdc-switch__icon--off"],["d","M20 13H4v-2h16v2z"]],template:function(i,r){if(i&1){let o=q();Ee(),m(0,"div",1)(1,"button",2,0),S("click",function(){return k(o),T(r._handleClick())}),M(3,"div",3)(4,"span",4),m(5,"span",5)(6,"span",6)(7,"span",7),M(8,"span",8),h(),m(9,"span",9),M(10,"span",10),h(),L(11,oge,5,0,"span",11),h()()(),m(12,"label",12),S("click",function(s){return k(o),T(s.stopPropagation())}),ne(13),h()()}if(i&2){let o=Te(2);v("labelPosition",r.labelPosition),g(),G("mdc-switch--selected",r.checked)("mdc-switch--unselected",!r.checked)("mdc-switch--checked",r.checked)("mdc-switch--disabled",r.disabled)("mat-mdc-slide-toggle-disabled-interactive",r.disabledInteractive),v("tabIndex",r.disabled&&!r.disabledInteractive?-1:r.tabIndex)("disabled",r.disabled&&!r.disabledInteractive),X("id",r.buttonId)("name",r.name)("aria-label",r.ariaLabel)("aria-labelledby",r._getAriaLabelledBy())("aria-describedby",r.ariaDescribedby)("aria-required",r.required||null)("aria-checked",r.checked)("aria-disabled",r.disabled&&r.disabledInteractive?"true":null),g(9),v("matRippleTrigger",o)("matRippleDisabled",r.disableRipple||r.disabled)("matRippleCentered",!0),g(),V(r.hideIcon?-1:11),g(),v("for",r.buttonId),X("id",r._labelId)}},dependencies:[qn,Zm],styles:[`.mdc-switch{align-items:center;background:none;border:none;cursor:pointer;display:inline-flex;flex-shrink:0;margin:0;outline:none;overflow:visible;padding:0;position:relative;width:var(--mat-slide-toggle-track-width, 52px)}.mdc-switch.mdc-switch--disabled{cursor:default;pointer-events:none}.mdc-switch.mat-mdc-slide-toggle-disabled-interactive{pointer-events:auto}.mdc-switch__track{overflow:hidden;position:relative;width:100%;height:var(--mat-slide-toggle-track-height, 32px);border-radius:var(--mat-slide-toggle-track-shape, var(--mat-sys-corner-full))}.mdc-switch--disabled.mdc-switch .mdc-switch__track{opacity:var(--mat-slide-toggle-disabled-track-opacity, 0.12)}.mdc-switch__track::before,.mdc-switch__track::after{border:1px solid rgba(0,0,0,0);border-radius:inherit;box-sizing:border-box;content:"";height:100%;left:0;position:absolute;width:100%;border-width:var(--mat-slide-toggle-track-outline-width, 2px);border-color:var(--mat-slide-toggle-track-outline-color, var(--mat-sys-outline))}.mdc-switch--selected .mdc-switch__track::before,.mdc-switch--selected .mdc-switch__track::after{border-width:var(--mat-slide-toggle-selected-track-outline-width, 2px);border-color:var(--mat-slide-toggle-selected-track-outline-color, transparent)}.mdc-switch--disabled .mdc-switch__track::before,.mdc-switch--disabled .mdc-switch__track::after{border-width:var(--mat-slide-toggle-disabled-unselected-track-outline-width, 2px);border-color:var(--mat-slide-toggle-disabled-unselected-track-outline-color, var(--mat-sys-on-surface))}@media(forced-colors: active){.mdc-switch__track{border-color:currentColor}}.mdc-switch__track::before{transition:transform 75ms 0ms cubic-bezier(0, 0, 0.2, 1);transform:translateX(0);background:var(--mat-slide-toggle-unselected-track-color, var(--mat-sys-surface-variant))}.mdc-switch--selected .mdc-switch__track::before{transition:transform 75ms 0ms cubic-bezier(0.4, 0, 0.6, 1);transform:translateX(100%)}[dir=rtl] .mdc-switch--selected .mdc-switch--selected .mdc-switch__track::before{transform:translateX(-100%)}.mdc-switch--selected .mdc-switch__track::before{opacity:var(--mat-slide-toggle-hidden-track-opacity, 0);transition:var(--mat-slide-toggle-hidden-track-transition, opacity 75ms)}.mdc-switch--unselected .mdc-switch__track::before{opacity:var(--mat-slide-toggle-visible-track-opacity, 1);transition:var(--mat-slide-toggle-visible-track-transition, opacity 75ms)}.mdc-switch:enabled:hover:not(:focus):not(:active) .mdc-switch__track::before{background:var(--mat-slide-toggle-unselected-hover-track-color, var(--mat-sys-surface-variant))}.mdc-switch:enabled:focus:not(:active) .mdc-switch__track::before{background:var(--mat-slide-toggle-unselected-focus-track-color, var(--mat-sys-surface-variant))}.mdc-switch:enabled:active .mdc-switch__track::before{background:var(--mat-slide-toggle-unselected-pressed-track-color, var(--mat-sys-surface-variant))}.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:hover:not(:focus):not(:active) .mdc-switch__track::before,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:focus:not(:active) .mdc-switch__track::before,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:active .mdc-switch__track::before,.mdc-switch.mdc-switch--disabled .mdc-switch__track::before{background:var(--mat-slide-toggle-disabled-unselected-track-color, var(--mat-sys-surface-variant))}.mdc-switch__track::after{transform:translateX(-100%);background:var(--mat-slide-toggle-selected-track-color, var(--mat-sys-primary))}[dir=rtl] .mdc-switch__track::after{transform:translateX(100%)}.mdc-switch--selected .mdc-switch__track::after{transform:translateX(0)}.mdc-switch--selected .mdc-switch__track::after{opacity:var(--mat-slide-toggle-visible-track-opacity, 1);transition:var(--mat-slide-toggle-visible-track-transition, opacity 75ms)}.mdc-switch--unselected .mdc-switch__track::after{opacity:var(--mat-slide-toggle-hidden-track-opacity, 0);transition:var(--mat-slide-toggle-hidden-track-transition, opacity 75ms)}.mdc-switch:enabled:hover:not(:focus):not(:active) .mdc-switch__track::after{background:var(--mat-slide-toggle-selected-hover-track-color, var(--mat-sys-primary))}.mdc-switch:enabled:focus:not(:active) .mdc-switch__track::after{background:var(--mat-slide-toggle-selected-focus-track-color, var(--mat-sys-primary))}.mdc-switch:enabled:active .mdc-switch__track::after{background:var(--mat-slide-toggle-selected-pressed-track-color, var(--mat-sys-primary))}.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:hover:not(:focus):not(:active) .mdc-switch__track::after,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:focus:not(:active) .mdc-switch__track::after,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:active .mdc-switch__track::after,.mdc-switch.mdc-switch--disabled .mdc-switch__track::after{background:var(--mat-slide-toggle-disabled-selected-track-color, var(--mat-sys-on-surface))}.mdc-switch__handle-track{height:100%;pointer-events:none;position:absolute;top:0;transition:transform 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1);left:0;right:auto;transform:translateX(0);width:calc(100% - var(--mat-slide-toggle-handle-width))}[dir=rtl] .mdc-switch__handle-track{left:auto;right:0}.mdc-switch--selected .mdc-switch__handle-track{transform:translateX(100%)}[dir=rtl] .mdc-switch--selected .mdc-switch__handle-track{transform:translateX(-100%)}.mdc-switch__handle{display:flex;pointer-events:auto;position:absolute;top:50%;transform:translateY(-50%);left:0;right:auto;transition:width 75ms cubic-bezier(0.4, 0, 0.2, 1),height 75ms cubic-bezier(0.4, 0, 0.2, 1),margin 75ms cubic-bezier(0.4, 0, 0.2, 1);width:var(--mat-slide-toggle-handle-width);height:var(--mat-slide-toggle-handle-height);border-radius:var(--mat-slide-toggle-handle-shape, var(--mat-sys-corner-full))}[dir=rtl] .mdc-switch__handle{left:auto;right:0}.mat-mdc-slide-toggle .mdc-switch--unselected .mdc-switch__handle{width:var(--mat-slide-toggle-unselected-handle-size, 16px);height:var(--mat-slide-toggle-unselected-handle-size, 16px);margin:var(--mat-slide-toggle-unselected-handle-horizontal-margin, 0 8px)}.mat-mdc-slide-toggle .mdc-switch--unselected .mdc-switch__handle:has(.mdc-switch__icons){margin:var(--mat-slide-toggle-unselected-with-icon-handle-horizontal-margin, 0 4px)}.mat-mdc-slide-toggle .mdc-switch--selected .mdc-switch__handle{width:var(--mat-slide-toggle-selected-handle-size, 24px);height:var(--mat-slide-toggle-selected-handle-size, 24px);margin:var(--mat-slide-toggle-selected-handle-horizontal-margin, 0 24px)}.mat-mdc-slide-toggle .mdc-switch--selected .mdc-switch__handle:has(.mdc-switch__icons){margin:var(--mat-slide-toggle-selected-with-icon-handle-horizontal-margin, 0 24px)}.mat-mdc-slide-toggle .mdc-switch__handle:has(.mdc-switch__icons){width:var(--mat-slide-toggle-with-icon-handle-size, 24px);height:var(--mat-slide-toggle-with-icon-handle-size, 24px)}.mat-mdc-slide-toggle .mdc-switch:active:not(.mdc-switch--disabled) .mdc-switch__handle{width:var(--mat-slide-toggle-pressed-handle-size, 28px);height:var(--mat-slide-toggle-pressed-handle-size, 28px)}.mat-mdc-slide-toggle .mdc-switch--selected:active:not(.mdc-switch--disabled) .mdc-switch__handle{margin:var(--mat-slide-toggle-selected-pressed-handle-horizontal-margin, 0 22px)}.mat-mdc-slide-toggle .mdc-switch--unselected:active:not(.mdc-switch--disabled) .mdc-switch__handle{margin:var(--mat-slide-toggle-unselected-pressed-handle-horizontal-margin, 0 2px)}.mdc-switch--disabled.mdc-switch--selected .mdc-switch__handle::after{opacity:var(--mat-slide-toggle-disabled-selected-handle-opacity, 1)}.mdc-switch--disabled.mdc-switch--unselected .mdc-switch__handle::after{opacity:var(--mat-slide-toggle-disabled-unselected-handle-opacity, 0.38)}.mdc-switch__handle::before,.mdc-switch__handle::after{border:1px solid rgba(0,0,0,0);border-radius:inherit;box-sizing:border-box;content:"";width:100%;height:100%;left:0;position:absolute;top:0;transition:background-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1),border-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1);z-index:-1}@media(forced-colors: active){.mdc-switch__handle::before,.mdc-switch__handle::after{border-color:currentColor}}.mdc-switch--selected:enabled .mdc-switch__handle::after{background:var(--mat-slide-toggle-selected-handle-color, var(--mat-sys-on-primary))}.mdc-switch--selected:enabled:hover:not(:focus):not(:active) .mdc-switch__handle::after{background:var(--mat-slide-toggle-selected-hover-handle-color, var(--mat-sys-primary-container))}.mdc-switch--selected:enabled:focus:not(:active) .mdc-switch__handle::after{background:var(--mat-slide-toggle-selected-focus-handle-color, var(--mat-sys-primary-container))}.mdc-switch--selected:enabled:active .mdc-switch__handle::after{background:var(--mat-slide-toggle-selected-pressed-handle-color, var(--mat-sys-primary-container))}.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled.mdc-switch--selected:hover:not(:focus):not(:active) .mdc-switch__handle::after,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled.mdc-switch--selected:focus:not(:active) .mdc-switch__handle::after,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled.mdc-switch--selected:active .mdc-switch__handle::after,.mdc-switch--selected.mdc-switch--disabled .mdc-switch__handle::after{background:var(--mat-slide-toggle-disabled-selected-handle-color, var(--mat-sys-surface))}.mdc-switch--unselected:enabled .mdc-switch__handle::after{background:var(--mat-slide-toggle-unselected-handle-color, var(--mat-sys-outline))}.mdc-switch--unselected:enabled:hover:not(:focus):not(:active) .mdc-switch__handle::after{background:var(--mat-slide-toggle-unselected-hover-handle-color, var(--mat-sys-on-surface-variant))}.mdc-switch--unselected:enabled:focus:not(:active) .mdc-switch__handle::after{background:var(--mat-slide-toggle-unselected-focus-handle-color, var(--mat-sys-on-surface-variant))}.mdc-switch--unselected:enabled:active .mdc-switch__handle::after{background:var(--mat-slide-toggle-unselected-pressed-handle-color, var(--mat-sys-on-surface-variant))}.mdc-switch--unselected.mdc-switch--disabled .mdc-switch__handle::after{background:var(--mat-slide-toggle-disabled-unselected-handle-color, var(--mat-sys-on-surface))}.mdc-switch__handle::before{background:var(--mat-slide-toggle-handle-surface-color)}.mdc-switch__shadow{border-radius:inherit;bottom:0;left:0;position:absolute;right:0;top:0}.mdc-switch:enabled .mdc-switch__shadow{box-shadow:var(--mat-slide-toggle-handle-elevation-shadow)}.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:hover:not(:focus):not(:active) .mdc-switch__shadow,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:focus:not(:active) .mdc-switch__shadow,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:active .mdc-switch__shadow,.mdc-switch.mdc-switch--disabled .mdc-switch__shadow{box-shadow:var(--mat-slide-toggle-disabled-handle-elevation-shadow)}.mdc-switch__ripple{left:50%;position:absolute;top:50%;transform:translate(-50%, -50%);z-index:-1;width:var(--mat-slide-toggle-state-layer-size, 40px);height:var(--mat-slide-toggle-state-layer-size, 40px)}.mdc-switch__ripple::after{content:"";opacity:0}.mdc-switch--disabled .mdc-switch__ripple::after{display:none}.mat-mdc-slide-toggle-disabled-interactive .mdc-switch__ripple::after{display:block}.mdc-switch:hover .mdc-switch__ripple::after{transition:75ms opacity cubic-bezier(0, 0, 0.2, 1)}.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:enabled:focus .mdc-switch__ripple::after,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:enabled:active .mdc-switch__ripple::after,.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled:enabled:hover:not(:focus) .mdc-switch__ripple::after,.mdc-switch--unselected:enabled:hover:not(:focus) .mdc-switch__ripple::after{background:var(--mat-slide-toggle-unselected-hover-state-layer-color, var(--mat-sys-on-surface));opacity:var(--mat-slide-toggle-unselected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mdc-switch--unselected:enabled:focus .mdc-switch__ripple::after{background:var(--mat-slide-toggle-unselected-focus-state-layer-color, var(--mat-sys-on-surface));opacity:var(--mat-slide-toggle-unselected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mdc-switch--unselected:enabled:active .mdc-switch__ripple::after{background:var(--mat-slide-toggle-unselected-pressed-state-layer-color, var(--mat-sys-on-surface));opacity:var(--mat-slide-toggle-unselected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity));transition:opacity 75ms linear}.mdc-switch--selected:enabled:hover:not(:focus) .mdc-switch__ripple::after{background:var(--mat-slide-toggle-selected-hover-state-layer-color, var(--mat-sys-primary));opacity:var(--mat-slide-toggle-selected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mdc-switch--selected:enabled:focus .mdc-switch__ripple::after{background:var(--mat-slide-toggle-selected-focus-state-layer-color, var(--mat-sys-primary));opacity:var(--mat-slide-toggle-selected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mdc-switch--selected:enabled:active .mdc-switch__ripple::after{background:var(--mat-slide-toggle-selected-pressed-state-layer-color, var(--mat-sys-primary));opacity:var(--mat-slide-toggle-selected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity));transition:opacity 75ms linear}.mdc-switch__icons{position:relative;height:100%;width:100%;z-index:1;transform:translateZ(0)}.mdc-switch--disabled.mdc-switch--unselected .mdc-switch__icons{opacity:var(--mat-slide-toggle-disabled-unselected-icon-opacity, 0.38)}.mdc-switch--disabled.mdc-switch--selected .mdc-switch__icons{opacity:var(--mat-slide-toggle-disabled-selected-icon-opacity, 0.38)}.mdc-switch__icon{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0;opacity:0;transition:opacity 30ms 0ms cubic-bezier(0.4, 0, 1, 1)}.mdc-switch--unselected .mdc-switch__icon{width:var(--mat-slide-toggle-unselected-icon-size, 16px);height:var(--mat-slide-toggle-unselected-icon-size, 16px);fill:var(--mat-slide-toggle-unselected-icon-color, var(--mat-sys-surface-variant))}.mdc-switch--unselected.mdc-switch--disabled .mdc-switch__icon{fill:var(--mat-slide-toggle-disabled-unselected-icon-color, var(--mat-sys-surface-variant))}.mdc-switch--selected .mdc-switch__icon{width:var(--mat-slide-toggle-selected-icon-size, 16px);height:var(--mat-slide-toggle-selected-icon-size, 16px);fill:var(--mat-slide-toggle-selected-icon-color, var(--mat-sys-on-primary-container))}.mdc-switch--selected.mdc-switch--disabled .mdc-switch__icon{fill:var(--mat-slide-toggle-disabled-selected-icon-color, var(--mat-sys-on-surface))}.mdc-switch--selected .mdc-switch__icon--on,.mdc-switch--unselected .mdc-switch__icon--off{opacity:1;transition:opacity 45ms 30ms cubic-bezier(0, 0, 0.2, 1)}.mat-mdc-slide-toggle{-webkit-user-select:none;user-select:none;display:inline-block;-webkit-tap-highlight-color:rgba(0,0,0,0);outline:0}.mat-mdc-slide-toggle .mat-mdc-slide-toggle-ripple,.mat-mdc-slide-toggle .mdc-switch__ripple::after{top:0;left:0;right:0;bottom:0;position:absolute;border-radius:50%;pointer-events:none}.mat-mdc-slide-toggle .mat-mdc-slide-toggle-ripple:not(:empty),.mat-mdc-slide-toggle .mdc-switch__ripple::after:not(:empty){transform:translateZ(0)}.mat-mdc-slide-toggle.mat-mdc-slide-toggle-focused .mat-focus-indicator::before{content:""}.mat-mdc-slide-toggle .mat-internal-form-field{color:var(--mat-slide-toggle-label-text-color, var(--mat-sys-on-surface));font-family:var(--mat-slide-toggle-label-text-font, var(--mat-sys-body-medium-font));line-height:var(--mat-slide-toggle-label-text-line-height, var(--mat-sys-body-medium-line-height));font-size:var(--mat-slide-toggle-label-text-size, var(--mat-sys-body-medium-size));letter-spacing:var(--mat-slide-toggle-label-text-tracking, var(--mat-sys-body-medium-tracking));font-weight:var(--mat-slide-toggle-label-text-weight, var(--mat-sys-body-medium-weight))}.mat-mdc-slide-toggle .mat-ripple-element{opacity:.12}.mat-mdc-slide-toggle .mat-focus-indicator::before{border-radius:50%}.mat-mdc-slide-toggle._mat-animation-noopable .mdc-switch__handle-track,.mat-mdc-slide-toggle._mat-animation-noopable .mdc-switch__icon,.mat-mdc-slide-toggle._mat-animation-noopable .mdc-switch__handle::before,.mat-mdc-slide-toggle._mat-animation-noopable .mdc-switch__handle::after,.mat-mdc-slide-toggle._mat-animation-noopable .mdc-switch__track::before,.mat-mdc-slide-toggle._mat-animation-noopable .mdc-switch__track::after{transition:none}.mat-mdc-slide-toggle .mdc-switch:enabled+.mdc-label{cursor:pointer}.mat-mdc-slide-toggle .mdc-switch--disabled+label{color:var(--mat-slide-toggle-disabled-label-text-color, var(--mat-sys-on-surface))}.mat-mdc-slide-toggle-touch-target{position:absolute;top:50%;left:50%;height:var(--mat-slide-toggle-touch-target-size, 48px);width:100%;transform:translate(-50%, -50%);display:var(--mat-slide-toggle-touch-target-display, block)}[dir=rtl] .mat-mdc-slide-toggle-touch-target{left:auto;right:50%;transform:translate(50%, -50%)} +`],encapsulation:2,changeDetection:0})}return t})(),Q8=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[UI,De,De]})}return t})();var uw=(()=>{let n=class n{constructor(){this.router=u(Ae),this.menu=u(bo),this.navItems=[],this.router.events.pipe(ce(i=>i instanceof Si)).subscribe(()=>this.checkOpenedItems()),this.menu.change().pipe(Dt(10)).subscribe(()=>{setTimeout(()=>this.checkOpenedItems())})}addItem(i){this.navItems.push(i)}removeItem(i){let r=this.navItems.indexOf(i);r!==-1&&this.navItems.splice(r,1)}closeOtherItems(i){this.navItems.forEach(r=>{r!==i&&r.setExpanded(!1)})}checkOpenedItems(){this.navItems.forEach(i=>{let r=i.route();r&&this.router.url.split("/").includes(r)&&(i.setExpanded(!0),this.closeOtherItems(i))})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","navAccordion",""]],exportAs:["navAccordion"]});let t=n;return t})();var mw=(()=>{let n=class n{constructor(){this.nav=u(uw),this.route=re(""),this.expanded=he(!1)}ngOnInit(){this.nav.addItem(this)}ngOnDestroy(){this.nav.removeItem(this)}toggle(){this.expanded.update(i=>!i),this.expanded()&&this.nav.closeOtherItems(this)}setExpanded(i){this.expanded()!==i&&this.expanded.set(i)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","navAccordionItem",""]],hostVars:2,hostBindings:function(r,o){r&2&&G("expanded",o.expanded())},inputs:{route:[1,"route"]},exportAs:["navAccordionItem"]});let t=n;return t})();var K8=(()=>{let n=class n{constructor(){this.navItem=u(mw)}onClick(){this.navItem.toggle()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","navAccordionToggle",""]],hostBindings:function(r,o){r&1&&S("click",function(){return o.onClick()})},exportAs:["navAccordionToggle"]});let t=n;return t})();var lge=()=>[],cge=(t,n)=>({menuList:t,parentRoute:n,level:0}),Z8=t=>[t],hw=(t,n)=>({item:t,level:n}),dge=(t,n,e)=>({menuList:t,parentRoute:n,level:e});function uge(t,n){if(t&1&&(m(0,"a",6),qe(1,2),h()),t&2){let e=x(2).$implicit,i=x(),r=i.parentRoute,o=i.level,a=x(),s=Te(5);v("routerLink",a.menu.buildRoute(r.concat($t(3,Z8,e.route)))),g(),v("ngTemplateOutlet",s)("ngTemplateOutletContext",ja(5,hw,e,o))}}function mge(t,n){if(t&1&&(m(0,"a",7),qe(1,2),h()),t&2){let e=x(2).$implicit,i=x().level;x();let r=Te(5);v("href",e.route,to),g(),v("ngTemplateOutlet",r)("ngTemplateOutletContext",ja(3,hw,e,i))}}function hge(t,n){if(t&1&&(m(0,"a",8),qe(1,2),h()),t&2){let e=x(2).$implicit,i=x().level;x();let r=Te(5);v("href",e.route,to),g(),v("ngTemplateOutlet",r)("ngTemplateOutletContext",ja(3,hw,e,i))}}function pge(t,n){if(t&1&&(m(0,"button",9),qe(1,2),h(),qe(2,2)),t&2){let e=x(2).$implicit,i=x(),r=i.parentRoute,o=i.level;x();let a=Te(3),s=Te(5);g(),v("ngTemplateOutlet",s)("ngTemplateOutletContext",ja(4,hw,e,o)),g(),v("ngTemplateOutlet",a)("ngTemplateOutletContext",Hd(9,dge,e.children,r.concat($t(7,Z8,e.route)),o+1))}}function fge(t,n){if(t&1&&(m(0,"li",5),L(1,uge,2,8,"a",6),L(2,mge,2,6,"a",7),L(3,hge,2,6,"a",8),L(4,pge,3,13),h()),t&2){let e=x().$implicit;v("route",e.route),g(),V(e.type==="link"?1:-1),g(),V(e.type==="extLink"?2:-1),g(),V(e.type==="extTabLink"?3:-1),g(),V(e.type==="sub"?4:-1)}}function gge(t,n){if(t&1&&A(0,fge,5,5,"ng-template",4),t&2){let e=n.$implicit;v("ngxPermissionsOnly",e.permissions==null?null:e.permissions.only)("ngxPermissionsExcept",e.permissions==null?null:e.permissions.except)}}function _ge(t,n){if(t&1&&(m(0,"ul",3),Mt(1,gge,1,2,null,4,qi),h()),t&2){let e=n.menuList,i=n.level;at(Zo("matero-sidemenu level-",i)),G("submenu",i>0),g(),Et(e)}}function bge(t,n){if(t&1&&(m(0,"mat-icon",14),f(1),h()),t&2){let e=x(),i=e.item,r=e.level;G("submenu-icon",r>0),g(),N(i.icon)}}function vge(t,n){if(t&1&&(m(0,"span",14),f(1),me(2,"translate"),me(3,"slice"),h()),t&2){let e=x(),i=e.item,r=e.level;G("submenu-icon",r>0),g(),fe(" ",GM(3,5,Re(2,3,i.name),0,1)," ")}}function yge(t,n){if(t&1&&(m(0,"span"),f(1),h()),t&2){let e=x().item;at(Zo("menu-label bg-",e.label.color)),g(),N(e.label.value)}}function xge(t,n){if(t&1&&(m(0,"span"),f(1),h()),t&2){let e=x().item;at(Zo("menu-badge bg-",e.badge.color)),g(),N(e.badge.value)}}function Cge(t,n){if(t&1&&(m(0,"mat-icon",13),f(1),h()),t&2){let e=x().item;g(),N(e.type==="sub"?"arrow_drop_down":"launch")}}function wge(t,n){if(t&1&&(L(0,bge,2,3,"mat-icon",10)(1,vge,4,9,"span",10),m(2,"span",11),f(3),me(4,"translate"),h(),L(5,yge,2,4,"span",12),L(6,xge,2,4,"span",12),L(7,Cge,2,1,"mat-icon",13)),t&2){let e=n.item;V(e.icon?0:1),g(3),N(Re(4,5,e.name)),g(2),V(e.label?5:-1),g(),V(e.badge?6:-1),g(),V(e.type!=="link"?7:-1)}}var X8=(()=>{let n=class n{constructor(){this.menu=u(bo)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-sidemenu"]],decls:6,vars:8,consts:[["menuTpl",""],["linkTypeTpl",""],[3,"ngTemplateOutlet","ngTemplateOutletContext"],["navAccordion",""],[3,"ngxPermissionsOnly","ngxPermissionsExcept"],["navAccordionItem","","routerLinkActive","active",1,"menu-item",3,"route"],["matRipple","",1,"menu-heading",3,"routerLink"],["matRipple","",1,"menu-heading",3,"href"],["target","_blank","matRipple","",1,"menu-heading",3,"href"],["navAccordionToggle","","matRipple","",1,"menu-heading","menu-toggle"],[1,"menu-icon",3,"submenu-icon"],[1,"menu-name"],[3,"class"],[1,"menu-caret"],[1,"menu-icon"]],template:function(r,o){if(r&1&&(qe(0,2),me(1,"async"),A(2,_ge,3,5,"ng-template",null,0,Mi)(4,wge,8,7,"ng-template",null,1,Mi)),r&2){let a=Te(3);v("ngTemplateOutlet",a)("ngTemplateOutletContext",ja(5,cge,Re(1,2,o.menu.getAll()),dt(4,lge)))}},dependencies:[$n,Wn,Kd,Bc,Fh,Ge,Ze,Io,qn,Rr,uw,mw,K8,cn,_E,Or],styles:[`.matero-sidemenu{padding:0;margin:0;list-style:none}.matero-sidemenu .menu-item{overflow:hidden;border-radius:1.5rem;transition:background 225ms cubic-bezier(.4,0,.2,1)}.matero-sidemenu .menu-item>.submenu{visibility:hidden;height:0;transition:all 225ms cubic-bezier(.4,0,.2,1)}.matero-sidemenu .menu-item>.menu-heading:hover,.matero-sidemenu .menu-item>.menu-heading:focus{background-color:var(--sidemenu-heading-hover-background-color)}.matero-sidemenu .menu-item.active>.menu-heading{color:var(--sidemenu-active-heading-text-color);background-color:var(--sidemenu-active-heading-background-color)}.matero-sidemenu .menu-item.active>.menu-heading:hover,.matero-sidemenu .menu-item.active>.menu-heading:focus{background-color:var(--sidemenu-active-heading-hover-background-color)}.matero-sidemenu .menu-item.expanded{background-color:var(--sidemenu-expanded-background-color)}.matero-sidemenu .menu-item.expanded>.submenu{visibility:visible;height:auto;height:calc-size(auto,size)}.matero-sidemenu .menu-item.expanded>.menu-toggle>.menu-caret{transform:rotate(-180deg)}.matero-sidemenu .menu-item:has(.active)>.menu-heading{color:var(--sidemenu-active-heading-text-color)}.matero-sidemenu .menu-heading{position:relative;display:flex;align-items:center;width:calc(var(--sidenav-width) - 1rem);padding:.75rem;font-size:inherit;color:inherit;text-decoration:none;cursor:pointer;outline:none;background-color:transparent;border:none;border-radius:1.5rem}.matero-sidemenu .menu-icon{width:1.5rem;height:1.5rem;margin-right:.75rem;font-size:1.25rem;line-height:1.5rem;text-align:center}[dir=rtl] .matero-sidemenu .menu-icon{margin-right:0;margin-left:.75rem}.matero-sidemenu .menu-icon.submenu-icon{position:absolute;left:.75rem;width:1.5rem;height:1.5rem;font-size:1rem;line-height:1.5rem;opacity:0}[dir=rtl] .matero-sidemenu .menu-icon.submenu-icon{right:.75rem;left:0}.matero-sidemenu .menu-caret{display:block;width:1.5rem;height:1.5rem;font-size:1.5rem;line-height:1.5rem;text-align:center;transition:transform .4s cubic-bezier(.25,.8,.25,1)}.matero-sidemenu .menu-name{flex:1;overflow:hidden;text-overflow:ellipsis;text-align:initial}.matero-sidemenu .menu-icon,.matero-sidemenu .menu-name,.matero-sidemenu .menu-label,.matero-sidemenu .menu-badge{transition:opacity .4s cubic-bezier(.25,.8,.25,1)}.matero-sidemenu .menu-label,.matero-sidemenu .menu-badge{padding:.25rem .5rem;font-size:.75rem;font-weight:700;line-height:1;color:#fff;background-color:#757575;border-radius:.375rem}.matero-sidemenu .menu-badge{border-radius:50rem}.matero-sidemenu.level-1>li>.menu-heading{padding-left:3rem}[dir=rtl] .matero-sidemenu.level-1>li>.menu-heading{padding-right:3rem;padding-left:1rem}.matero-sidemenu.level-2>li>.menu-heading{padding-left:4rem}[dir=rtl] .matero-sidemenu.level-2>li>.menu-heading{padding-right:4rem;padding-left:1rem}.matero-sidemenu.level-2 [class^=level-]>li>.menu-heading{padding-left:5rem}[dir=rtl] .matero-sidemenu.level-2 [class^=level-]>li>.menu-heading{padding-right:5rem;padding-left:1rem} +`],encapsulation:2});let t=n;return t})();var J8=(()=>{let n=class n{constructor(){this.oidcAuth=u(jt),this.userName="Guest",this.userEmail="Anonymous"}ngOnInit(){this.updateUserInfo(),this.authSubscription=this.oidcAuth.isAuthenticated$.subscribe(i=>{console.log("UserPanel: Auth state changed:",i),this.updateUserInfo()})}ngOnDestroy(){this.authSubscription?.unsubscribe()}updateUserInfo(){let i=this.oidcAuth.isAuthenticated();if(console.log("UserPanel: Updating user info, isAuthenticated:",i),!i){this.userName="Guest",this.userEmail="Anonymous";return}let r=this.oidcAuth.getUserInfo();console.log("UserPanel: User info from service:",r),console.log("UserPanel: Available claims:",r?Object.keys(r):"null"),this.userName=r?.name||r?.preferred_username||r?.given_name||r?.["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"]||r?.sub||"User",this.userEmail=r?.email||r?.["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]||"",console.log("UserPanel: Set userName to:",this.userName,"email to:",this.userEmail)}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-user-panel"]],decls:8,vars:4,consts:[[1,"matero-user-panel"],[1,"matero-user-panel-avatar"],[1,"matero-user-panel-info"]],template:function(r,o){r&1&&(m(0,"div",0)(1,"mat-icon",1),f(2,"account_circle"),h(),m(3,"div",2)(4,"h4"),f(5),h(),m(6,"h5"),f(7),h()()()),r&2&&(G("authenticated",o.oidcAuth.isAuthenticated()),g(5),N(o.userName),g(2),N(o.userEmail))},dependencies:[Je,Fe,Ge,Ze,An,Rr],styles:[`.matero-user-panel{display:flex;align-items:center;padding:.75rem;margin-bottom:.75rem;cursor:pointer;outline:none;background-color:var(--user-panel-background-color);border-radius:.75rem}.matero-user-panel:hover,.matero-user-panel:focus{background-color:var(--user-panel-hover-background-color)}.matero-user-panel-avatar{width:3rem;height:3rem;font-size:3rem;border-radius:50rem;color:#0000008a;transform-origin:0 1.5rem;transition:transform .4s cubic-bezier(.25,.8,.25,1)}[dir=rtl] .matero-user-panel-avatar{transform-origin:3rem 1.5rem}.matero-user-panel-info{flex:1;width:0;margin-left:.75rem;opacity:1;transition:opacity .4s cubic-bezier(.25,.8,.25,1)}[dir=rtl] .matero-user-panel-info{margin-right:.75rem;margin-left:0}.matero-user-panel-info h4,.matero-user-panel-info h5{margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.matero-user-panel-info h4{margin-bottom:4px;font-size:1rem;font-weight:500}.matero-user-panel-info h5{font-size:.75rem;font-weight:400} +`],encapsulation:2});let t=n;return t})();function Dge(t,n){if(t&1){let e=q();m(0,"mat-slide-toggle",6),S("change",function(){k(e);let r=x(2);return T(r.toggleCollapsed.emit())}),h()}if(t&2){let e=x(2);v("checked",e.toggleChecked())}}function Mge(t,n){if(t&1){let e=q();m(0,"button",7),S("click",function(){k(e);let r=x(2);return T(r.closeSidenav.emit())}),m(1,"mat-icon"),f(2,"close"),h()()}}function Ege(t,n){if(t&1&&(m(0,"div",0)(1,"mat-toolbar"),M(2,"app-branding",2)(3,"span",3),L(4,Dge,1,1,"mat-slide-toggle",4)(5,Mge,3,0,"button",5),h()()),t&2){let e=x();g(2),v("showName",!e.toggleChecked()),g(2),V(e.showToggle()?4:5)}}function Sge(t,n){t&1&&M(0,"app-user-panel")}var e6=(()=>{let n=class n{constructor(){this.showToggle=re(!0),this.showUser=re(!0),this.showHeader=re(!0),this.toggleChecked=re(!1),this.toggleCollapsed=Ei(),this.closeSidenav=Ei()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-sidebar"]],inputs:{showToggle:[1,"showToggle"],showUser:[1,"showUser"],showHeader:[1,"showHeader"],toggleChecked:[1,"toggleChecked"]},outputs:{toggleCollapsed:"toggleCollapsed",closeSidenav:"closeSidenav"},decls:4,vars:2,consts:[[1,"matero-sidebar-header"],[1,"matero-sidebar-main"],[3,"showName"],[1,"flex-fill"],["hideIcon","",3,"checked"],["mat-icon-button",""],["hideIcon","",3,"change","checked"],["mat-icon-button","",3,"click"]],template:function(r,o){r&1&&(L(0,Ege,6,2,"div",0),m(1,"div",1),L(2,Sge,1,0,"app-user-panel"),M(3,"app-sidemenu"),h()),r&2&&(V(o.showHeader()?0:-1),g(2),V(o.showUser()?2:-1))},dependencies:[Q8,UI,Ge,Ze,Fe,Ft,ow,rw,aw,X8,J8],styles:[`.matero-sidebar-header{overflow:hidden}.matero-sidebar-header mat-toolbar{width:var(--sidenav-width);padding:0 .5rem}.matero-sidebar-header mat-slide-toggle .mdc-label{display:none}.matero-sidebar-main{height:calc(100% - var(--mat-toolbar-standard-height));padding:0 .5rem .5rem;overflow:auto;scrollbar-width:none}@media (max-width: 599px){.matero-sidebar-main{height:calc(100% - var(--mat-toolbar-mobile-height))}} +`],encapsulation:2});let t=n;return t})();var t6=t=>[t],kge=()=>[];function Tge(t,n){if(t&1){let e=q();m(0,"a",8,0),S("click",function(){k(e);let r=Te(1),o=x(3);return T(o.onRouterLinkClick(r))}),f(2),me(3,"translate"),h()}if(t&2){let e=x(2).$implicit,i=x();v("routerLink",i.menu.buildRoute(i.parentRoute().concat($t(4,t6,e.route)))),g(2),fe(" ",Re(3,2,e.name)," ")}}function Ige(t,n){if(t&1&&(m(0,"a",5)(1,"span",9),f(2),me(3,"translate"),h(),m(4,"mat-icon"),f(5,"launch"),h()()),t&2){let e=x(2).$implicit;v("href",e.route,to),g(2),N(Re(3,2,e.name))}}function Age(t,n){if(t&1&&(m(0,"a",6)(1,"span",9),f(2),me(3,"translate"),h(),m(4,"mat-icon"),f(5,"launch"),h()()),t&2){let e=x(2).$implicit;v("href",e.route,to),g(2),N(Re(3,2,e.name))}}function Oge(t,n){if(t&1){let e=q();m(0,"button",10),f(1),me(2,"translate"),m(3,"app-topmenu-panel",11,1),S("routeChange",function(r){k(e);let o=x(2).$implicit,a=x();return T(a.onRouteChange(r,o))}),h()()}if(t&2){let e=Te(4),i=x(2).$implicit,r=x();G("active",i.active==null?null:i.active()),v("matMenuTriggerFor",e.menuPanel()),g(),fe(" ",Re(2,7,i.name)," "),g(2),v("items",i.children||dt(9,kge))("parentRoute",r.parentRoute().concat($t(10,t6,i.route)))("level",r.level()+1)}}function Rge(t,n){if(t&1&&(L(0,Tge,4,6,"a",4),L(1,Ige,6,4,"a",5),L(2,Age,6,4,"a",6),L(3,Oge,5,12,"button",7)),t&2){let e=x().$implicit;V(e.type==="link"?0:-1),g(),V(e.type==="extLink"?1:-1),g(),V(e.type==="extTabLink"?2:-1),g(),V(e.type==="sub"?3:-1)}}function Pge(t,n){if(t&1&&A(0,Rge,4,4,"ng-template",3),t&2){let e=n.$implicit;v("ngxPermissionsOnly",e.permissions==null?null:e.permissions.only)("ngxPermissionsExcept",e.permissions==null?null:e.permissions.except)}}var i6=(()=>{let n=class n{constructor(){this.router=u(Ae),this.menu=u(bo),this.menuPanel=ir.required(Zr),this.items=re([]),this.parentRoute=re([]),this.level=re(1),this.routeChange=Ei(),this.routerSubscription=this.router.events.pipe(ce(i=>i instanceof Si)).subscribe(i=>{this.items().map(r=>r.active?.set(!1))})}ngOnInit(){this.items().forEach(i=>{i.active=he(this.checkRoute(i))})}ngOnDestroy(){this.routerSubscription.unsubscribe()}checkRoute(i){return i.route?this.router.url.split("/").includes(i.route):this.checkChildRoute(i.children)}checkChildRoute(i=[]){return i.some(r=>this.router.url.split("/").includes(r.route)?!0:(!r.route&&r.children&&this.checkChildRoute(r.children),!1))}onRouterLinkClick(i){this.routeChange.emit(i)}onRouteChange(i,r){this.routeChange.emit(i),this.routerSubscription.unsubscribe(),this.routerSubscription=this.router.events.pipe(ce(o=>o instanceof Si),He(()=>{this.items().filter(a=>a!=r).map(a=>a.active?.set(!1))}),Dt(10),He(()=>{r.active?.set(i.isActive)})).subscribe()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-topmenu-panel"]],viewQuery:function(r,o){r&1&&zn(o.menuPanel,Zr,5),r&2&&Ko()},inputs:{items:[1,"items"],parentRoute:[1,"parentRoute"],level:[1,"level"]},outputs:{routeChange:"routeChange"},decls:3,vars:0,consts:[["rla","routerLinkActive"],["submenu",""],[1,"matero-topmenu-panel"],[3,"ngxPermissionsOnly","ngxPermissionsExcept"],["mat-menu-item","","routerLinkActive","active",3,"routerLink"],["mat-menu-item","",3,"href"],["mat-menu-item","","target","_blank",3,"href"],["mat-menu-item","",3,"matMenuTriggerFor","active"],["mat-menu-item","","routerLinkActive","active",3,"click","routerLink"],[1,"menu-name"],["mat-menu-item","",3,"matMenuTriggerFor"],[3,"routeChange","items","parentRoute","level"]],template:function(r,o){r&1&&(m(0,"mat-menu",2),Mt(1,Pge,1,2,null,3,qi),h()),r&2&&(g(),Et(o.items()))},dependencies:[n,Wn,Kd,Ge,Ze,tl,Zr,Ul,el,Bc,Fh,Rr,Or],encapsulation:2});let t=n;return t})();var n6=t=>[t],pw=t=>({item:t}),Fge=()=>[];function Nge(t,n){if(t&1&&(m(0,"a",5),qe(1,9),h()),t&2){let e=x(2).$implicit,i=x(),r=Te(7);v("routerLink",i.menu.buildRoute($t(3,n6,e.route))),g(),v("ngTemplateOutlet",r)("ngTemplateOutletContext",$t(5,pw,e))}}function Lge(t,n){if(t&1&&(m(0,"a",6),qe(1,9),h()),t&2){let e=x(2).$implicit;x();let i=Te(7);v("href",e.route,to),g(),v("ngTemplateOutlet",i)("ngTemplateOutletContext",$t(3,pw,e))}}function Vge(t,n){if(t&1&&(m(0,"a",7),qe(1,9),h()),t&2){let e=x(2).$implicit;x();let i=Te(7);v("href",e.route,to),g(),v("ngTemplateOutlet",i)("ngTemplateOutletContext",$t(3,pw,e))}}function Bge(t,n){if(t&1){let e=q();m(0,"button",10),qe(1,9),m(2,"app-topmenu-panel",11,2),S("routeChange",function(r){k(e);let o=x(2).$implicit,a=x();return T(a.onRouteChange(r,o))}),h()()}if(t&2){let e=Te(3),i=x(2).$implicit;x();let r=Te(7);G("active",i.active==null?null:i.active()),v("matMenuTriggerFor",e.menuPanel()),g(),v("ngTemplateOutlet",r)("ngTemplateOutletContext",$t(8,pw,i)),g(),v("items",i.children||dt(10,Fge))("parentRoute",$t(11,n6,i.route))("level",1)}}function jge(t,n){if(t&1&&(L(0,Nge,2,7,"a",5),L(1,Lge,2,5,"a",6),L(2,Vge,2,5,"a",7),L(3,Bge,4,13,"button",8)),t&2){let e=x().$implicit;V(e.type==="link"?0:-1),g(),V(e.type==="extLink"?1:-1),g(),V(e.type==="extTabLink"?2:-1),g(),V(e.type==="sub"?3:-1)}}function Hge(t,n){if(t&1&&A(0,jge,4,4,"ng-template",4),t&2){let e=n.$implicit;v("ngxPermissionsOnly",e.permissions==null?null:e.permissions.only)("ngxPermissionsExcept",e.permissions==null?null:e.permissions.except)}}function zge(t,n){if(t&1&&(m(0,"span"),f(1),h()),t&2){let e=x().item;at(Zo("menu-label bg-",e.label.color)),g(),N(e.label.value)}}function Uge(t,n){if(t&1&&(m(0,"span"),f(1),h()),t&2){let e=x().item;at(Zo("menu-badge bg-",e.badge.color)),g(),N(e.badge.value)}}function $ge(t,n){if(t&1&&(m(0,"mat-icon",15),f(1),h()),t&2){let e=x().item;g(),N(e.type==="sub"?"arrow_drop_down":"launch")}}function Wge(t,n){if(t&1&&(m(0,"mat-icon",12),f(1),h(),m(2,"span",13),f(3),me(4,"translate"),h(),L(5,zge,2,4,"span",14),L(6,Uge,2,4,"span",14),L(7,$ge,2,1,"mat-icon",15)),t&2){let e=n.item;g(),N(e.icon),g(2),N(Re(4,5,e.name)),g(2),V(e.label?5:-1),g(),V(e.badge?6:-1),g(),V(e.type!=="link"?7:-1)}}var r6=(()=>{let n=class n{constructor(){this.router=u(Ae),this.menu=u(bo),this.menuList=[],this.menuSubscription=this.menu.getAll().subscribe(i=>{this.menuList=i.map(r=>{let o=this.router.url.split("/").includes(r.route);return r.active=he(o),r})}),this.routerSubscription=this.router.events.pipe(ce(i=>i instanceof Si)).subscribe(i=>{this.menuList.map(r=>r.active?.set(!1))})}ngOnDestroy(){this.menuSubscription.unsubscribe(),this.routerSubscription.unsubscribe()}onRouteChange(i,r){this.routerSubscription.unsubscribe(),this.routerSubscription=this.router.events.pipe(ce(o=>o instanceof Si),He(()=>{this.menuList.filter(o=>o!=r).map(o=>o.active?.set(!1))}),Dt(10),He(()=>{r.active?.set(i.isActive)})).subscribe()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-topmenu"]],hostAttrs:[1,"matero-topmenu"],decls:8,vars:3,consts:[["tabPanel",""],["linkTypeTpl",""],["submenu",""],["mat-tab-nav-bar","",3,"tabPanel"],[3,"ngxPermissionsOnly","ngxPermissionsExcept"],["matButton","","routerLinkActive","active",3,"routerLink"],["matButton","",3,"href"],["matButton","","target","_blank",3,"href"],["matButton","",3,"matMenuTriggerFor","active"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],["matButton","",3,"matMenuTriggerFor"],[3,"routeChange","items","parentRoute","level"],[1,"menu-icon"],[1,"menu-name"],[3,"class"],[1,"menu-caret"]],template:function(r,o){if(r&1&&(m(0,"nav",3),Mt(1,Hge,1,2,null,4,qi),me(3,"async"),h(),M(4,"mat-tab-nav-panel",null,0),A(6,Wge,8,7,"ng-template",null,1,Mi)),r&2){let a=Te(5);v("tabPanel",a),g(),Et(Re(3,1,o.menu.getAll()))}},dependencies:[$n,Wn,Kd,Fe,_t,Ge,Ze,tl,el,cp,HI,q8,Bc,Fh,Rr,i6,cn,Or],styles:[`.matero-topmenu{--mat-tab-divider-height: 0;--mat-button-text-label-text-color: var(--topmenu-text-color);position:sticky;z-index:200;display:block;padding:.5rem;background-color:var(--topmenu-background-color);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.matero-topmenu .mat-mdc-button{padding:0 1rem;white-space:nowrap}.matero-topmenu .mat-mdc-button.active{background-color:var(--topmenu-item-active-background-color)}.matero-topmenu .menu-icon,.matero-topmenu .menu-caret,.matero-topmenu .menu-name{vertical-align:middle}.matero-topmenu .mat-icon.menu-icon{width:1.125rem;height:1.125rem;margin-right:.5rem;font-size:1.125rem;line-height:1.125rem}[dir=rtl] .matero-topmenu .mat-icon.menu-icon{margin-right:auto;margin-left:.5rem}.matero-topmenu .mat-icon.menu-caret{margin-right:-.5rem}[dir=rtl] .matero-topmenu .mat-icon.menu-caret{margin-right:auto;margin-left:-.5rem}.matero-topmenu .menu-label,.matero-topmenu .menu-badge{padding:.25rem .5rem;margin-left:.5rem;font-size:.75rem;font-weight:700;line-height:1;color:#fff;background-color:#757575;border-radius:.375rem}[dir=rtl] .matero-topmenu .menu-label,[dir=rtl] .matero-topmenu .menu-badge{margin-right:.5rem;margin-left:auto}.matero-topmenu .menu-badge{border-radius:50rem}.matero-topmenu .mat-tab-nav-bar,.matero-topmenu .mat-tab-header{border-bottom:none}.matero-topmenu-panel .mat-mdc-menu-item.active{color:var(--topmenu-dropdown-item-active-text-color)} +`],encapsulation:2});let t=n;return t})();var Gge=["sidenav"],qge=["content"];function Yge(t,n){if(t&1){let e=q();m(0,"app-header",13),S("toggleSidenav",function(){k(e),x();let r=Te(5);return T(r.toggle())})("toggleSidenavNotice",function(){k(e),x();let r=Te(8);return T(r.toggle())}),h()}t&2&&v("showBranding",!0)}function Qge(t,n){if(t&1){let e=q();m(0,"app-header",14),S("toggleSidenav",function(){k(e),x();let r=Te(5);return T(r.toggle())})("toggleSidenavNotice",function(){k(e),x();let r=Te(8);return T(r.toggle())}),h()}if(t&2){let e=x();v("showToggle",!e.options.sidenavCollapsed&&e.options.navPos!=="top")("showBranding",e.options.navPos==="top")}}function Kge(t,n){t&1&&M(0,"app-topmenu")}var o6="screen and (max-width: 599px)",a6="screen and (min-width: 600px) and (max-width: 959px)",s6="screen and (min-width: 960px)",l6=(()=>{let n=class n{get themeColor(){return this.settings.getThemeColor()}get isOver(){return this.isMobileScreen}get contentWidthFix(){return this.isContentWidthFixed&&this.options.navPos==="side"&&this.options.sidenavOpened&&!this.isOver}get collapsedWidthFix(){return this.isCollapsedWidthFixed&&(this.options.navPos==="top"||this.options.sidenavOpened&&this.isOver)}constructor(){this.sidenav=ir.required("sidenav"),this.content=ir.required("content"),this.breakpointObserver=u(Ml),this.router=u(Ae),this.settings=u(ha),this.options=this.settings.options,this.isMobileScreen=!1,this.isContentWidthFixed=!0,this.isCollapsedWidthFixed=!1,this.layoutChangesSubscription=ke.EMPTY,this.layoutChangesSubscription=this.breakpointObserver.observe([o6,a6,s6]).subscribe(i=>{this.options.sidenavOpened=!0,this.isMobileScreen=i.breakpoints[o6],this.options.sidenavCollapsed=i.breakpoints[a6],this.isContentWidthFixed=i.breakpoints[s6]}),this.router.events.pipe(ce(i=>i instanceof Si)).subscribe(i=>{this.isOver&&this.sidenav().close(),this.content().scrollTo({top:0})})}ngOnDestroy(){this.layoutChangesSubscription.unsubscribe()}toggleCollapsed(){this.isContentWidthFixed=!1,this.options.sidenavCollapsed=!this.options.sidenavCollapsed,this.resetCollapsedState()}resetCollapsedState(i=400){setTimeout(()=>{this.settings.setOptions(this.options)},i)}onSidenavClosedStart(){this.isContentWidthFixed=!1}onSidenavOpenedChange(i){this.isCollapsedWidthFixed=!this.isOver,this.options.sidenavOpened=i,this.settings.setOptions(this.options)}updateOptions(i){this.options=i,this.settings.setOptions(i),this.settings.setDirection(),this.settings.setTheme()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-admin-layout"]],viewQuery:function(r,o){r&1&&(zn(o.sidenav,Gge,5),zn(o.content,qge,5)),r&2&&Ko(2)},hostVars:4,hostBindings:function(r,o){r&2&&G("matero-content-width-fix",o.contentWidthFix)("matero-sidenav-collapsed-fix",o.collapsedWidthFix)},decls:16,vars:20,consts:[["sidenav",""],["sidenavNotice",""],["content",""],[1,"matero-container-wrap",3,"dir"],["ngProgressRouter",""],[3,"showBranding"],["autosize","","autoFocus","",1,"matero-container"],[1,"matero-sidenav",3,"openedChange","closedStart","mode","opened"],[3,"toggleCollapsed","closeSidenav","showToggle","showUser","showHeader","toggleChecked"],["position","end","mode","over"],[1,"matero-content"],[3,"showToggle","showBranding"],[1,"matero-page-content"],[3,"toggleSidenav","toggleSidenavNotice","showBranding"],[3,"toggleSidenav","toggleSidenavNotice","showToggle","showBranding"]],template:function(r,o){if(r&1){let a=q();m(0,"div",3),M(1,"ng-progress",4),L(2,Yge,1,1,"app-header",5),m(3,"mat-sidenav-container",6)(4,"mat-sidenav",7,0),S("openedChange",function(l){return k(a),T(o.onSidenavOpenedChange(l))})("closedStart",function(){return k(a),T(o.onSidenavClosedStart())}),m(6,"app-sidebar",8),S("toggleCollapsed",function(){return k(a),T(o.toggleCollapsed())})("closeSidenav",function(){k(a);let l=Te(5);return T(l.close())}),h()(),m(7,"mat-sidenav",9,1),M(9,"app-sidebar-notice"),h(),m(10,"mat-sidenav-content",10,2),L(12,Qge,1,2,"app-header",11),L(13,Kge,1,0,"app-topmenu"),m(14,"main",12),M(15,"router-outlet"),h()()()()}r&2&&(G("matero-sidenav-collapsed",o.options.sidenavCollapsed&&o.options.navPos!=="top")("matero-navbar-side",o.options.navPos==="side")("matero-navbar-top",o.options.navPos==="top")("matero-header-above",o.options.headerPos==="above")("matero-header-fixed",o.options.headerPos==="fixed"),v("dir",o.options.dir),g(2),V(o.options.showHeader&&o.options.headerPos==="above"?2:-1),g(2),v("mode",o.isOver?"over":"side")("opened",o.options.navPos==="side"&&o.options.sidenavOpened&&!o.isOver),g(2),v("showToggle",!o.isOver)("showUser",!!o.options.showUserPanel)("showHeader",o.options.headerPos!=="above")("toggleChecked",!!o.options.sidenavCollapsed),g(6),V(o.options.showHeader&&o.options.headerPos!=="above"?12:-1),g(),V(o.options.navPos==="top"?13:-1))},dependencies:[wl,Ns,YL,u8,RI,d8,ew,h8,f8,O8,r6,e6,Y8],styles:[`.matero-container-wrap,.matero-container{--mat-sidenav-content-background-color: transparent;height:100%}.matero-sidenav{--mat-sidenav-container-width: var(--sidenav-width);position:absolute;overflow-x:hidden;border-width:0!important;transition-property:transform,width!important}.matero-header-above .matero-container{height:calc(100% - var(--mat-toolbar-standard-height))!important}@media (max-width: 599px){.matero-header-above .matero-container{height:calc(100% - var(--mat-toolbar-mobile-height))!important}}.matero-header-above .matero-sidebar-main{height:100%!important;padding-top:.5rem}.matero-sidenav-collapsed .matero-sidenav,.matero-sidenav-collapsed-fix .matero-sidenav{width:var(--sidenav-collapsed-width)}.matero-sidenav-collapsed .matero-sidenav .menu-name,.matero-sidenav-collapsed .matero-sidenav .menu-label,.matero-sidenav-collapsed .matero-sidenav .menu-badge,.matero-sidenav-collapsed .matero-sidenav .menu-caret,.matero-sidenav-collapsed .matero-sidenav .matero-user-panel-info,.matero-sidenav-collapsed-fix .matero-sidenav .menu-name,.matero-sidenav-collapsed-fix .matero-sidenav .menu-label,.matero-sidenav-collapsed-fix .matero-sidenav .menu-badge,.matero-sidenav-collapsed-fix .matero-sidenav .menu-caret,.matero-sidenav-collapsed-fix .matero-sidenav .matero-user-panel-info{opacity:0}.matero-sidenav-collapsed .matero-sidenav .menu-icon.submenu-icon,.matero-sidenav-collapsed-fix .matero-sidenav .menu-icon.submenu-icon{opacity:1}.matero-sidenav-collapsed .matero-sidenav .matero-user-panel-avatar,.matero-sidenav-collapsed-fix .matero-sidenav .matero-user-panel-avatar{transform:scale(.5)}.matero-sidenav-collapsed .matero-sidenav:hover,.matero-sidenav-collapsed-fix .matero-sidenav:hover{width:var(--sidenav-width)}.matero-sidenav-collapsed .matero-sidenav:hover .menu-name,.matero-sidenav-collapsed .matero-sidenav:hover .menu-label,.matero-sidenav-collapsed .matero-sidenav:hover .menu-badge,.matero-sidenav-collapsed .matero-sidenav:hover .menu-caret,.matero-sidenav-collapsed .matero-sidenav:hover .matero-user-panel-info,.matero-sidenav-collapsed-fix .matero-sidenav:hover .menu-name,.matero-sidenav-collapsed-fix .matero-sidenav:hover .menu-label,.matero-sidenav-collapsed-fix .matero-sidenav:hover .menu-badge,.matero-sidenav-collapsed-fix .matero-sidenav:hover .menu-caret,.matero-sidenav-collapsed-fix .matero-sidenav:hover .matero-user-panel-info{opacity:1}.matero-sidenav-collapsed .matero-sidenav:hover .menu-icon.submenu-icon,.matero-sidenav-collapsed-fix .matero-sidenav:hover .menu-icon.submenu-icon{opacity:0}.matero-sidenav-collapsed .matero-sidenav:hover .matero-user-panel-avatar,.matero-sidenav-collapsed-fix .matero-sidenav:hover .matero-user-panel-avatar{transform:scale(1)}.matero-sidenav-collapsed .matero-content{margin-left:var(--sidenav-collapsed-width)!important}[dir=rtl] .matero-sidenav-collapsed .matero-content,.matero-sidenav-collapsed[dir=rtl] .matero-content{margin-right:var(--sidenav-collapsed-width)!important;margin-left:auto!important}.matero-navbar-top .matero-topmenu{top:0}.matero-navbar-top .matero-branding{margin-left:1rem}[dir=rtl] .matero-navbar-top .matero-branding{margin-right:1rem;margin-left:auto}.matero-header-fixed .matero-header{position:sticky;top:0}.matero-header-fixed .matero-topmenu{top:var(--mat-toolbar-standard-height)}@media (max-width: 599px){.matero-header-fixed .matero-topmenu{top:var(--mat-toolbar-mobile-height)}}.matero-content-width-fix .matero-content{margin-left:var(--sidenav-width)!important}[dir=rtl] .matero-content-width-fix .matero-content{margin-right:var(--sidenav-width)!important;margin-left:auto!important}.matero-page-content{position:relative;padding:var(--gutter)}.matero-header+.matero-page-content,.matero-header-above .matero-page-content,.matero-navbar-top .matero-page-content{padding-top:0} +`],encapsulation:2});let t=n;return t})();var c6=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-auth-layout"]],decls:2,vars:0,consts:[[1,"matero-auth-container"]],template:function(r,o){r&1&&(m(0,"div",0),M(1,"router-outlet"),h())},dependencies:[wl],styles:[`.matero-auth-container{position:relative;display:flex;justify-content:center;min-height:100%;background-image:radial-gradient(at 97% 21%,hsla(125,98%,72%,.3) 0,transparent 50%),radial-gradient(at 52% 99%,hsla(354,98%,61%,.3) 0,transparent 50%),radial-gradient(at 10% 29%,hsla(256,96%,67%,.3) 0,transparent 50%),radial-gradient(at 97% 96%,hsla(38,60%,74%,.3) 0,transparent 50%),radial-gradient(at 33% 50%,hsla(222,67%,73%,.3) 0,transparent 50%),radial-gradient(at 79% 53%,hsla(343,68%,79%,.3) 0,transparent 50%)} +`],encapsulation:2});let t=n;return t})();var Zge=["determinateSpinner"];function Xge(t,n){if(t&1&&(ii(),m(0,"svg",11),M(1,"circle",12),h()),t&2){let e=x();X("viewBox",e._viewBox()),g(),At("stroke-dasharray",e._strokeCircumference(),"px")("stroke-dashoffset",e._strokeCircumference()/2,"px")("stroke-width",e._circleStrokeWidth(),"%"),X("r",e._circleRadius())}}var Jge=new O("mat-progress-spinner-default-options",{providedIn:"root",factory:e_e});function e_e(){return{diameter:d6}}var d6=100,t_e=10,Kt=(()=>{class t{_elementRef=u(Y);_noopAnimations;get color(){return this._color||this._defaultColor}set color(e){this._color=e}_color;_defaultColor="primary";_determinateCircle;constructor(){let e=u(Jge),i=xS(),r=this._elementRef.nativeElement;this._noopAnimations=i==="di-disabled"&&!!e&&!e._forceAnimations,this.mode=r.nodeName.toLowerCase()==="mat-spinner"?"indeterminate":"determinate",!this._noopAnimations&&i==="reduced-motion"&&r.classList.add("mat-progress-spinner-reduced-motion"),e&&(e.color&&(this.color=this._defaultColor=e.color),e.diameter&&(this.diameter=e.diameter),e.strokeWidth&&(this.strokeWidth=e.strokeWidth))}mode;get value(){return this.mode==="determinate"?this._value:0}set value(e){this._value=Math.max(0,Math.min(100,e||0))}_value=0;get diameter(){return this._diameter}set diameter(e){this._diameter=e||0}_diameter=d6;get strokeWidth(){return this._strokeWidth??this.diameter/10}set strokeWidth(e){this._strokeWidth=e||0}_strokeWidth;_circleRadius(){return(this.diameter-t_e)/2}_viewBox(){let e=this._circleRadius()*2+this.strokeWidth;return`0 0 ${e} ${e}`}_strokeCircumference(){return 2*Math.PI*this._circleRadius()}_strokeDashOffset(){return this.mode==="determinate"?this._strokeCircumference()*(100-this._value)/100:null}_circleStrokeWidth(){return this.strokeWidth/this.diameter*100}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-progress-spinner"],["mat-spinner"]],viewQuery:function(i,r){if(i&1&&ie(Zge,5),i&2){let o;j(o=H())&&(r._determinateCircle=o.first)}},hostAttrs:["role","progressbar","tabindex","-1",1,"mat-mdc-progress-spinner","mdc-circular-progress"],hostVars:18,hostBindings:function(i,r){i&2&&(X("aria-valuemin",0)("aria-valuemax",100)("aria-valuenow",r.mode==="determinate"?r.value:null)("mode",r.mode),at("mat-"+r.color),At("width",r.diameter,"px")("height",r.diameter,"px")("--mat-progress-spinner-size",r.diameter+"px")("--mat-progress-spinner-active-indicator-width",r.diameter+"px"),G("_mat-animation-noopable",r._noopAnimations)("mdc-circular-progress--indeterminate",r.mode==="indeterminate"))},inputs:{color:"color",mode:"mode",value:[2,"value","value",ht],diameter:[2,"diameter","diameter",ht],strokeWidth:[2,"strokeWidth","strokeWidth",ht]},exportAs:["matProgressSpinner"],decls:14,vars:11,consts:[["circle",""],["determinateSpinner",""],["aria-hidden","true",1,"mdc-circular-progress__determinate-container"],["xmlns","http://www.w3.org/2000/svg","focusable","false",1,"mdc-circular-progress__determinate-circle-graphic"],["cx","50%","cy","50%",1,"mdc-circular-progress__determinate-circle"],["aria-hidden","true",1,"mdc-circular-progress__indeterminate-container"],[1,"mdc-circular-progress__spinner-layer"],[1,"mdc-circular-progress__circle-clipper","mdc-circular-progress__circle-left"],[3,"ngTemplateOutlet"],[1,"mdc-circular-progress__gap-patch"],[1,"mdc-circular-progress__circle-clipper","mdc-circular-progress__circle-right"],["xmlns","http://www.w3.org/2000/svg","focusable","false",1,"mdc-circular-progress__indeterminate-circle-graphic"],["cx","50%","cy","50%"]],template:function(i,r){if(i&1&&(A(0,Xge,2,8,"ng-template",null,0,Mi),m(2,"div",2,1),ii(),m(4,"svg",3),M(5,"circle",4),h()(),Qo(),m(6,"div",5)(7,"div",6)(8,"div",7),qe(9,8),h(),m(10,"div",9),qe(11,8),h(),m(12,"div",10),qe(13,8),h()()()),i&2){let o=Te(1);g(4),X("viewBox",r._viewBox()),g(),At("stroke-dasharray",r._strokeCircumference(),"px")("stroke-dashoffset",r._strokeDashOffset(),"px")("stroke-width",r._circleStrokeWidth(),"%"),X("r",r._circleRadius()),g(4),v("ngTemplateOutlet",o),g(2),v("ngTemplateOutlet",o),g(2),v("ngTemplateOutlet",o)}},dependencies:[$n],styles:[`.mat-mdc-progress-spinner{--mat-progress-spinner-animation-multiplier: 1;display:block;overflow:hidden;line-height:0;position:relative;direction:ltr;transition:opacity 250ms cubic-bezier(0.4, 0, 0.6, 1)}.mat-mdc-progress-spinner circle{stroke-width:var(--mat-progress-spinner-active-indicator-width, 4px)}.mat-mdc-progress-spinner._mat-animation-noopable,.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__determinate-circle{transition:none !important}.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-circle-graphic,.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__spinner-layer,.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-container{animation:none !important}.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-container circle{stroke-dasharray:0 !important}@media(forced-colors: active){.mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic,.mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle{stroke:currentColor;stroke:CanvasText}}.mat-progress-spinner-reduced-motion{--mat-progress-spinner-animation-multiplier: 1.25}.mdc-circular-progress__determinate-container,.mdc-circular-progress__indeterminate-circle-graphic,.mdc-circular-progress__indeterminate-container,.mdc-circular-progress__spinner-layer{position:absolute;width:100%;height:100%}.mdc-circular-progress__determinate-container{transform:rotate(-90deg)}.mdc-circular-progress--indeterminate .mdc-circular-progress__determinate-container{opacity:0}.mdc-circular-progress__indeterminate-container{font-size:0;letter-spacing:0;white-space:nowrap;opacity:0}.mdc-circular-progress--indeterminate .mdc-circular-progress__indeterminate-container{opacity:1;animation:mdc-circular-progress-container-rotate calc(1568.2352941176ms*var(--mat-progress-spinner-animation-multiplier)) linear infinite}.mdc-circular-progress__determinate-circle-graphic,.mdc-circular-progress__indeterminate-circle-graphic{fill:rgba(0,0,0,0)}.mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle,.mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic{stroke:var(--mat-progress-spinner-active-indicator-color, var(--mat-sys-primary))}@media(forced-colors: active){.mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle,.mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic{stroke:CanvasText}}.mdc-circular-progress__determinate-circle{transition:stroke-dashoffset 500ms cubic-bezier(0, 0, 0.2, 1)}.mdc-circular-progress__gap-patch{position:absolute;top:0;left:47.5%;box-sizing:border-box;width:5%;height:100%;overflow:hidden}.mdc-circular-progress__gap-patch .mdc-circular-progress__indeterminate-circle-graphic{left:-900%;width:2000%;transform:rotate(180deg)}.mdc-circular-progress__circle-clipper .mdc-circular-progress__indeterminate-circle-graphic{width:200%}.mdc-circular-progress__circle-right .mdc-circular-progress__indeterminate-circle-graphic{left:-100%}.mdc-circular-progress--indeterminate .mdc-circular-progress__circle-left .mdc-circular-progress__indeterminate-circle-graphic{animation:mdc-circular-progress-left-spin calc(1333ms*var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress--indeterminate .mdc-circular-progress__circle-right .mdc-circular-progress__indeterminate-circle-graphic{animation:mdc-circular-progress-right-spin calc(1333ms*var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both}.mdc-circular-progress__circle-clipper{display:inline-flex;position:relative;width:50%;height:100%;overflow:hidden}.mdc-circular-progress--indeterminate .mdc-circular-progress__spinner-layer{animation:mdc-circular-progress-spinner-layer-rotate calc(5332ms*var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both}@keyframes mdc-circular-progress-container-rotate{to{transform:rotate(360deg)}}@keyframes mdc-circular-progress-spinner-layer-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}100%{transform:rotate(1080deg)}}@keyframes mdc-circular-progress-left-spin{from{transform:rotate(265deg)}50%{transform:rotate(130deg)}to{transform:rotate(265deg)}}@keyframes mdc-circular-progress-right-spin{from{transform:rotate(-265deg)}50%{transform:rotate(-130deg)}to{transform:rotate(-265deg)}} +`],encapsulation:2,changeDetection:0})}return t})();var Zt=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De]})}return t})();function i_e(t,n){if(t&1){let e=q();m(0,"div",1)(1,"button",2),S("click",function(){k(e);let r=x();return T(r.action())}),f(2),h()()}if(t&2){let e=x();g(2),fe(" ",e.data.action," ")}}var n_e=["label"];function r_e(t,n){}var o_e=Math.pow(2,31)-1,w_=class{_overlayRef;instance;containerInstance;_afterDismissed=new z;_afterOpened=new z;_onAction=new z;_durationTimeoutId;_dismissedByAction=!1;constructor(n,e){this._overlayRef=e,this.containerInstance=n,n._onExit.subscribe(()=>this._finishDismiss())}dismiss(){this._afterDismissed.closed||this.containerInstance.exit(),clearTimeout(this._durationTimeoutId)}dismissWithAction(){this._onAction.closed||(this._dismissedByAction=!0,this._onAction.next(),this._onAction.complete(),this.dismiss()),clearTimeout(this._durationTimeoutId)}closeWithAction(){this.dismissWithAction()}_dismissAfter(n){this._durationTimeoutId=setTimeout(()=>this.dismiss(),Math.min(n,o_e))}_open(){this._afterOpened.closed||(this._afterOpened.next(),this._afterOpened.complete())}_finishDismiss(){this._overlayRef.dispose(),this._onAction.closed||this._onAction.complete(),this._afterDismissed.next({dismissedByAction:this._dismissedByAction}),this._afterDismissed.complete(),this._dismissedByAction=!1}afterDismissed(){return this._afterDismissed}afterOpened(){return this.containerInstance._onEnter}onAction(){return this._onAction}},u6=new O("MatSnackBarData"),up=class{politeness="polite";announcementMessage="";viewContainerRef;duration=0;panelClass;direction;data=null;horizontalPosition="center";verticalPosition="bottom"},a_e=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matSnackBarLabel",""]],hostAttrs:[1,"mat-mdc-snack-bar-label","mdc-snackbar__label"]})}return t})(),s_e=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matSnackBarActions",""]],hostAttrs:[1,"mat-mdc-snack-bar-actions","mdc-snackbar__actions"]})}return t})(),l_e=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matSnackBarAction",""]],hostAttrs:[1,"mat-mdc-snack-bar-action","mdc-snackbar__action"]})}return t})(),m6=(()=>{class t{snackBarRef=u(w_);data=u(u6);constructor(){}action(){this.snackBarRef.dismissWithAction()}get hasAction(){return!!this.data.action}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["simple-snack-bar"]],hostAttrs:[1,"mat-mdc-simple-snack-bar"],exportAs:["matSnackBar"],decls:3,vars:2,consts:[["matSnackBarLabel",""],["matSnackBarActions",""],["matButton","","matSnackBarAction","",3,"click"]],template:function(i,r){i&1&&(m(0,"div",0),f(1),h(),L(2,i_e,3,1,"div",1)),i&2&&(g(),fe(" ",r.data.message,` +`),g(),V(r.hasAction?2:-1))},dependencies:[_t,a_e,s_e,l_e],styles:[`.mat-mdc-simple-snack-bar{display:flex}.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-label{max-height:50vh;overflow:auto} +`],encapsulation:2,changeDetection:0})}return t})(),$I="_mat-snack-bar-enter",WI="_mat-snack-bar-exit",c_e=(()=>{class t extends kc{_ngZone=u(ae);_elementRef=u(Y);_changeDetectorRef=u(ye);_platform=u(Ye);_animationsDisabled=Qe();snackBarConfig=u(up);_document=u(_e);_trackedModals=new Set;_enterFallback;_exitFallback;_injector=u(de);_announceDelay=150;_announceTimeoutId;_destroyed=!1;_portalOutlet;_onAnnounce=new z;_onExit=new z;_onEnter=new z;_animationState="void";_live;_label;_role;_liveElementId=u(et).getId("mat-snack-bar-container-live-");constructor(){super();let e=this.snackBarConfig;e.politeness==="assertive"&&!e.announcementMessage?this._live="assertive":e.politeness==="off"?this._live="off":this._live="polite",this._platform.FIREFOX&&(this._live==="polite"&&(this._role="status"),this._live==="assertive"&&(this._role="alert"))}attachComponentPortal(e){this._assertNotAttached();let i=this._portalOutlet.attachComponentPortal(e);return this._afterPortalAttached(),i}attachTemplatePortal(e){this._assertNotAttached();let i=this._portalOutlet.attachTemplatePortal(e);return this._afterPortalAttached(),i}attachDomPortal=e=>{this._assertNotAttached();let i=this._portalOutlet.attachDomPortal(e);return this._afterPortalAttached(),i};onAnimationEnd(e){e===WI?this._completeExit():e===$I&&(clearTimeout(this._enterFallback),this._ngZone.run(()=>{this._onEnter.next(),this._onEnter.complete()}))}enter(){this._destroyed||(this._animationState="visible",this._changeDetectorRef.markForCheck(),this._changeDetectorRef.detectChanges(),this._screenReaderAnnounce(),this._animationsDisabled?vt(()=>{this._ngZone.run(()=>queueMicrotask(()=>this.onAnimationEnd($I)))},{injector:this._injector}):(clearTimeout(this._enterFallback),this._enterFallback=setTimeout(()=>{this._elementRef.nativeElement.classList.add("mat-snack-bar-fallback-visible"),this.onAnimationEnd($I)},200)))}exit(){return this._destroyed?Q(void 0):(this._ngZone.run(()=>{this._animationState="hidden",this._changeDetectorRef.markForCheck(),this._elementRef.nativeElement.setAttribute("mat-exit",""),clearTimeout(this._announceTimeoutId),this._animationsDisabled?vt(()=>{this._ngZone.run(()=>queueMicrotask(()=>this.onAnimationEnd(WI)))},{injector:this._injector}):(clearTimeout(this._exitFallback),this._exitFallback=setTimeout(()=>this.onAnimationEnd(WI),200))}),this._onExit)}ngOnDestroy(){this._destroyed=!0,this._clearFromModals(),this._completeExit()}_completeExit(){clearTimeout(this._exitFallback),queueMicrotask(()=>{this._onExit.next(),this._onExit.complete()})}_afterPortalAttached(){let e=this._elementRef.nativeElement,i=this.snackBarConfig.panelClass;i&&(Array.isArray(i)?i.forEach(a=>e.classList.add(a)):e.classList.add(i)),this._exposeToModals();let r=this._label.nativeElement,o="mdc-snackbar__label";r.classList.toggle(o,!r.querySelector(`.${o}`))}_exposeToModals(){let e=this._liveElementId,i=this._document.querySelectorAll('body > .cdk-overlay-container [aria-modal="true"]');for(let r=0;r{let i=e.getAttribute("aria-owns");if(i){let r=i.replace(this._liveElementId,"").trim();r.length>0?e.setAttribute("aria-owns",r):e.removeAttribute("aria-owns")}}),this._trackedModals.clear()}_assertNotAttached(){this._portalOutlet.hasAttached()}_screenReaderAnnounce(){this._announceTimeoutId||this._ngZone.runOutsideAngular(()=>{this._announceTimeoutId=setTimeout(()=>{if(this._destroyed)return;let e=this._elementRef.nativeElement,i=e.querySelector("[aria-hidden]"),r=e.querySelector("[aria-live]");if(i&&r){let o=null;this._platform.isBrowser&&document.activeElement instanceof HTMLElement&&i.contains(document.activeElement)&&(o=document.activeElement),i.removeAttribute("aria-hidden"),r.appendChild(i),o?.focus(),this._onAnnounce.next(),this._onAnnounce.complete()}},this._announceDelay)})}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-snack-bar-container"]],viewQuery:function(i,r){if(i&1&&(ie(Ir,7),ie(n_e,7)),i&2){let o;j(o=H())&&(r._portalOutlet=o.first),j(o=H())&&(r._label=o.first)}},hostAttrs:[1,"mdc-snackbar","mat-mdc-snack-bar-container"],hostVars:6,hostBindings:function(i,r){i&1&&S("animationend",function(a){return r.onAnimationEnd(a.animationName)})("animationcancel",function(a){return r.onAnimationEnd(a.animationName)}),i&2&&G("mat-snack-bar-container-enter",r._animationState==="visible")("mat-snack-bar-container-exit",r._animationState==="hidden")("mat-snack-bar-container-animations-enabled",!r._animationsDisabled)},features:[le],decls:6,vars:3,consts:[["label",""],[1,"mdc-snackbar__surface","mat-mdc-snackbar-surface"],[1,"mat-mdc-snack-bar-label"],["aria-hidden","true"],["cdkPortalOutlet",""]],template:function(i,r){i&1&&(m(0,"div",1)(1,"div",2,0)(3,"div",3),A(4,r_e,0,0,"ng-template",4),h(),M(5,"div"),h()()),i&2&&(g(5),X("aria-live",r._live)("role",r._role)("id",r._liveElementId))},dependencies:[Ir],styles:[`@keyframes _mat-snack-bar-enter{from{transform:scale(0.8);opacity:0}to{transform:scale(1);opacity:1}}@keyframes _mat-snack-bar-exit{from{opacity:1}to{opacity:0}}.mat-mdc-snack-bar-container{display:flex;align-items:center;justify-content:center;box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0);margin:8px}.mat-mdc-snack-bar-handset .mat-mdc-snack-bar-container{width:100vw}.mat-snack-bar-container-animations-enabled{opacity:0}.mat-snack-bar-container-animations-enabled.mat-snack-bar-fallback-visible{opacity:1}.mat-snack-bar-container-animations-enabled.mat-snack-bar-container-enter{animation:_mat-snack-bar-enter 150ms cubic-bezier(0, 0, 0.2, 1) forwards}.mat-snack-bar-container-animations-enabled.mat-snack-bar-container-exit{animation:_mat-snack-bar-exit 75ms cubic-bezier(0.4, 0, 1, 1) forwards}.mat-mdc-snackbar-surface{box-shadow:0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);display:flex;align-items:center;justify-content:flex-start;box-sizing:border-box;padding-left:0;padding-right:8px}[dir=rtl] .mat-mdc-snackbar-surface{padding-right:0;padding-left:8px}.mat-mdc-snack-bar-container .mat-mdc-snackbar-surface{min-width:344px;max-width:672px}.mat-mdc-snack-bar-handset .mat-mdc-snackbar-surface{width:100%;min-width:0}@media(forced-colors: active){.mat-mdc-snackbar-surface{outline:solid 1px}}.mat-mdc-snack-bar-container .mat-mdc-snackbar-surface{color:var(--mat-snack-bar-supporting-text-color, var(--mat-sys-inverse-on-surface));border-radius:var(--mat-snack-bar-container-shape, var(--mat-sys-corner-extra-small));background-color:var(--mat-snack-bar-container-color, var(--mat-sys-inverse-surface))}.mdc-snackbar__label{width:100%;flex-grow:1;box-sizing:border-box;margin:0;padding:14px 8px 14px 16px}[dir=rtl] .mdc-snackbar__label{padding-left:8px;padding-right:16px}.mat-mdc-snack-bar-container .mdc-snackbar__label{font-family:var(--mat-snack-bar-supporting-text-font, var(--mat-sys-body-medium-font));font-size:var(--mat-snack-bar-supporting-text-size, var(--mat-sys-body-medium-size));font-weight:var(--mat-snack-bar-supporting-text-weight, var(--mat-sys-body-medium-weight));line-height:var(--mat-snack-bar-supporting-text-line-height, var(--mat-sys-body-medium-line-height))}.mat-mdc-snack-bar-actions{display:flex;flex-shrink:0;align-items:center;box-sizing:border-box}.mat-mdc-snack-bar-handset,.mat-mdc-snack-bar-container,.mat-mdc-snack-bar-label{flex:1 1 auto}.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled){--mat-button-text-state-layer-color: currentColor;--mat-button-text-ripple-color: currentColor}.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled).mat-unthemed{color:var(--mat-snack-bar-button-color, var(--mat-sys-inverse-primary))}.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled) .mat-ripple-element{opacity:.1} +`],encapsulation:2})}return t})();function d_e(){return new up}var u_e=new O("mat-snack-bar-default-options",{providedIn:"root",factory:d_e}),_i=(()=>{class t{_live=u(Xf);_injector=u(de);_breakpointObserver=u(Ml);_parentSnackBar=u(t,{optional:!0,skipSelf:!0});_defaultConfig=u(u_e);_animationsDisabled=Qe();_snackBarRefAtThisLevel=null;simpleSnackBarComponent=m6;snackBarContainerComponent=c_e;handsetCssClass="mat-mdc-snack-bar-handset";get _openedSnackBarRef(){let e=this._parentSnackBar;return e?e._openedSnackBarRef:this._snackBarRefAtThisLevel}set _openedSnackBarRef(e){this._parentSnackBar?this._parentSnackBar._openedSnackBarRef=e:this._snackBarRefAtThisLevel=e}constructor(){}openFromComponent(e,i){return this._attach(e,i)}openFromTemplate(e,i){return this._attach(e,i)}open(e,i="",r){let o=I(I({},this._defaultConfig),r);return o.data={message:e,action:i},o.announcementMessage===e&&(o.announcementMessage=void 0),this.openFromComponent(this.simpleSnackBarComponent,o)}dismiss(){this._openedSnackBarRef&&this._openedSnackBarRef.dismiss()}ngOnDestroy(){this._snackBarRefAtThisLevel&&this._snackBarRefAtThisLevel.dismiss()}_attachSnackBarContainer(e,i){let r=i&&i.viewContainerRef&&i.viewContainerRef.injector,o=de.create({parent:r||this._injector,providers:[{provide:up,useValue:i}]}),a=new ao(this.snackBarContainerComponent,i.viewContainerRef,o),s=e.attach(a);return s.instance.snackBarConfig=i,s.instance}_attach(e,i){let r=I(I(I({},new up),this._defaultConfig),i),o=this._createOverlay(r),a=this._attachSnackBarContainer(o,r),s=new w_(a,o);if(e instanceof te){let l=new kn(e,null,{$implicit:r.data,snackBarRef:s});s.instance=a.attachTemplatePortal(l)}else{let l=this._createInjector(r,s),c=new ao(e,void 0,l),d=a.attachComponentPortal(c);s.instance=d.instance}return this._breakpointObserver.observe(jv.HandsetPortrait).pipe(xe(o.detachments())).subscribe(l=>{o.overlayElement.classList.toggle(this.handsetCssClass,l.matches)}),r.announcementMessage&&a._onAnnounce.subscribe(()=>{this._live.announce(r.announcementMessage,r.politeness)}),this._animateSnackBar(s,r),this._openedSnackBarRef=s,this._openedSnackBarRef}_animateSnackBar(e,i){e.afterDismissed().subscribe(()=>{this._openedSnackBarRef==e&&(this._openedSnackBarRef=null),i.announcementMessage&&this._live.clear()}),i.duration&&i.duration>0&&e.afterOpened().subscribe(()=>e._dismissAfter(i.duration)),this._openedSnackBarRef?(this._openedSnackBarRef.afterDismissed().subscribe(()=>{e.containerInstance.enter()}),this._openedSnackBarRef.dismiss()):e.containerInstance.enter()}_createOverlay(e){let i=new Gr;i.direction=e.direction;let r=Us(this._injector),o=e.direction==="rtl",a=e.horizontalPosition==="left"||e.horizontalPosition==="start"&&!o||e.horizontalPosition==="end"&&o,s=!a&&e.horizontalPosition!=="center";return a?r.left("0"):s?r.right("0"):r.centerHorizontally(),e.verticalPosition==="top"?r.top("0"):r.bottom("0"),i.positionStrategy=r,i.disableAnimations=this._animationsDisabled,qr(this._injector,i)}_createInjector(e,i){let r=e&&e.viewContainerRef&&e.viewContainerRef.injector;return de.create({parent:r||this._injector,providers:[{provide:w_,useValue:i},{provide:u6,useValue:e.data}]})}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var wi=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[_i],imports:[cr,Oo,Fe,De,m6,De]})}return t})();var Xn=(()=>{let n=class n{constructor(){this.authService=u(jt),this.templateRef=u(te),this.viewContainer=u(st)}set appHasRole(i){this.updateView(i)}ngOnInit(){this.subscription=this.authService.isAuthenticated$.subscribe(()=>{let i=this.roles;i&&this.updateView(i)})}ngOnDestroy(){this.subscription?.unsubscribe()}updateView(i){this.roles=i,this.viewContainer.clear(),this.checkRole(i)&&this.viewContainer.createEmbeddedView(this.templateRef)}checkRole(i){return this.authService.isAuthenticated()?typeof i=="string"?this.authService.hasRole(i):Array.isArray(i)?this.authService.hasAnyRole(i):!1:!1}};n.\u0275fac=function(r){return new(r||n)},n.\u0275dir=P({type:n,selectors:[["","appHasRole",""]],inputs:{appHasRole:"appHasRole"}});let t=n;return t})();var nd=(()=>{let n=class n{constructor(){this.http=u(kr),this.apiUrl=Ji.apiUrl}chat(i,r){return this.http.post(`${this.apiUrl}/ai/chat`,{message:i,systemPrompt:r})}hrInsight(i){return this.http.post(`${this.apiUrl}/ai/hr-insight`,{question:i}).pipe(se(r=>r.value))}nlEmployeeSearch(i){return this.http.post(`${this.apiUrl}/ai/nl-employee-search`,{query:i})}semanticPositionSearch(i,r=10){return this.http.post(`${this.apiUrl}/positions/semantic-search`,{queryText:i,topK:r})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var ga=class{constructor(){this.http=u(kr),this.apiUrl=Ji.apiUrl}getAll(n){let e=this.buildHttpParams(n);return this.http.get(`${this.apiUrl}/${this.endpoint}`,{params:e}).pipe(se(i=>i.value))}getAllPaged(n){let e=this.buildHttpParams(n);return this.http.get(`${this.apiUrl}/${this.endpoint}`,{params:e})}getById(n){return this.http.get(`${this.apiUrl}/${this.endpoint}/${n}`).pipe(se(e=>e.value))}create(n){return this.http.post(`${this.apiUrl}/${this.endpoint}`,n).pipe(se(e=>e&&"value"in e&&typeof e.value=="string"?{id:e.value}:e))}update(n,e){return this.http.put(`${this.apiUrl}/${this.endpoint}/${n}`,e)}delete(n){return this.http.delete(`${this.apiUrl}/${this.endpoint}/${n}`)}buildHttpParams(n){let e=new rr;return n&&Object.keys(n).forEach(i=>{let r=n[i];r!=null&&(e=e.set(i,r.toString()))}),e}};var h6=(()=>{let n=class n extends ga{constructor(){super(...arguments),this.endpoint="Dashboard"}getDashboardMetrics(){return this.http.get(`${this.apiUrl}/${this.endpoint}/Metrics`).pipe(se(i=>{if(i.isSuccess&&i.value)return i.value;throw new Error(i.message||"Failed to load dashboard metrics")}))}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var _a=(()=>{let n=class n extends ga{constructor(){super(...arguments),this.endpoint="Departments"}createDepartment(i){return this.create(i)}updateDepartment(i){return this.update(i.id,i)}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var rd=(()=>{let n=class n extends ga{constructor(){super(...arguments),this.endpoint="Employees"}createEmployee(i){return this.create(i)}updateEmployee(i){return this.update(i.id,i)}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var od=(()=>{let n=class n extends ga{constructor(){super(...arguments),this.endpoint="Positions"}createPosition(i){return this.create(i)}updatePosition(i){return this.update(i.id,i)}getAllPaged(i){let r=this.buildHttpParams(i);return this.http.get(`${this.apiUrl}/${this.endpoint}`,{params:r})}addMockPositions(i){return this.http.post(`${this.apiUrl}/${this.endpoint}/AddMock`,i)}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var ad=(()=>{let n=class n extends ga{constructor(){super(...arguments),this.endpoint="SalaryRanges"}createSalaryRange(i){return this.create(i)}updateSalaryRange(i){return this.update(i.id,i)}};n.\u0275fac=(()=>{let i;return function(o){return(i||(i=ge(n)))(o||n)}})(),n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var p6=(()=>{let n=class n{decodeToken(i){if(!i)return null;try{let r=i.split(".");if(r.length!==3)return console.warn("Invalid JWT token format"),null;let o=this.decodeBase64Url(r[0]),a=this.decodeBase64Url(r[1]),s=r[2],l=JSON.parse(a),c=l.iat?new Date(l.iat*1e3):void 0,d=l.exp?new Date(l.exp*1e3):void 0,p=new Date,_=d?d24){let s=Math.floor(o/24);return`Expires in ${s} day${s>1?"s":""}`}else return o>0?`Expires in ${o}h ${a}m`:a>0?`Expires in ${a}m`:"Expires soon"}};n.\u0275fac=function(r){return new(r||n)},n.\u0275prov=R({token:n,factory:n.\u0275fac,providedIn:"root"});let t=n;return t})();var m_e=()=>["HRAdmin","Manager"],f6=()=>["HRAdmin"];function h_e(t,n){t&1&&(m(0,"div",3),M(1,"mat-spinner"),h())}function p_e(t,n){if(t&1&&(m(0,"div",21),M(1,"canvas",22),h()),t&2){let e=x(2);g(),v("data",e.departmentChartData)("options",e.departmentChartOptions)}}function f_e(t,n){t&1&&(m(0,"div",23)(1,"mat-icon"),f(2,"pie_chart"),h(),m(3,"p"),f(4,"No department data available"),h()())}function g_e(t,n){if(t&1&&(m(0,"div",21),M(1,"canvas",24),h()),t&2){let e=x(2);g(),v("data",e.positionChartData)("options",e.positionChartOptions)}}function __e(t,n){t&1&&(m(0,"div",23)(1,"mat-icon"),f(2,"bar_chart"),h(),m(3,"p"),f(4,"No position data available"),h()())}function b_e(t,n){if(t&1&&(m(0,"div",21),M(1,"canvas",25),h()),t&2){let e=x(2);g(),v("data",e.genderChartData)("options",e.genderChartOptions)}}function v_e(t,n){t&1&&(m(0,"div",23)(1,"mat-icon"),f(2,"donut_large"),h(),m(3,"p"),f(4,"No gender data available"),h()())}function y_e(t,n){if(t&1&&(m(0,"div",21),M(1,"canvas",24),h()),t&2){let e=x(2);g(),v("data",e.salaryChartData)("options",e.salaryChartOptions)}}function x_e(t,n){t&1&&(m(0,"div",23)(1,"mat-icon"),f(2,"bar_chart"),h(),m(3,"p"),f(4,"No salary range data available"),h()())}function C_e(t,n){if(t&1){let e=q();m(0,"mat-list-item",27),S("click",function(){let r=k(e).$implicit,o=x(3);return T(o.navigateToEmployee(r.id))}),m(1,"mat-icon",28),f(2,"person"),h(),m(3,"div",29),f(4),h(),m(5,"div",30),f(6),h(),m(7,"div",31),f(8),me(9,"date"),h()()}if(t&2){let e=n.$implicit;g(4),N(e.fullName),g(2),_l("",e.positionTitle," \u2022 ",e.departmentName),g(2),N(Ui(9,4,e.createdAt,"MMM d, y"))}}function w_e(t,n){if(t&1&&(m(0,"mat-list"),A(1,C_e,10,7,"mat-list-item",26),h()),t&2){let e=x(2);g(),v("ngForOf",e.metrics.recentEmployees)}}function D_e(t,n){t&1&&(m(0,"div",23)(1,"mat-icon"),f(2,"people_outline"),h(),m(3,"p"),f(4,"No recent employees"),h()())}function M_e(t,n){if(t&1){let e=q();m(0,"button",36),S("click",function(){k(e);let r=x(3);return T(r.navigateToAddDepartment())}),m(1,"mat-icon"),f(2,"add_business"),h(),f(3," Add Department "),h()}}function E_e(t,n){if(t&1){let e=q();m(0,"button",36),S("click",function(){k(e);let r=x(3);return T(r.navigateToAddPosition())}),m(1,"mat-icon"),f(2,"work_outline"),h(),f(3," Add Position "),h()}}function S_e(t,n){if(t&1){let e=q();m(0,"div",32)(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4,"Quick Actions"),h()(),m(5,"mat-card-content")(6,"div",33)(7,"button",34),S("click",function(){k(e);let r=x(2);return T(r.navigateToAddEmployee())}),m(8,"mat-icon"),f(9,"person_add"),h(),f(10," Add Employee "),h(),A(11,M_e,4,0,"button",35)(12,E_e,4,0,"button",35),h()()()()}t&2&&(g(11),v("appHasRole",dt(2,f6)),g(),v("appHasRole",dt(3,f6)))}function k_e(t,n){if(t&1){let e=q();m(0,"div",4)(1,"div",5)(2,"mat-card",6)(3,"mat-card-content")(4,"div",7)(5,"mat-icon"),f(6,"people"),h()(),m(7,"div",8),f(8),h(),m(9,"div",9),f(10,"Total Employees"),h()()(),m(11,"mat-card",6)(12,"mat-card-content")(13,"div",7)(14,"mat-icon"),f(15,"business"),h()(),m(16,"div",8),f(17),h(),m(18,"div",9),f(19,"Departments"),h()()(),m(20,"mat-card",6)(21,"mat-card-content")(22,"div",7)(23,"mat-icon"),f(24,"work"),h()(),m(25,"div",8),f(26),h(),m(27,"div",9),f(28,"Positions"),h()()(),m(29,"mat-card",6)(30,"mat-card-content")(31,"div",7)(32,"mat-icon"),f(33,"attach_money"),h()(),m(34,"div",8),f(35),h(),m(36,"div",9),f(37,"Salary Ranges"),h()()()(),m(38,"div",10)(39,"mat-card",11)(40,"mat-card-content")(41,"div",12)(42,"mat-icon"),f(43,"person_add"),h()(),m(44,"div",8),f(45),h(),m(46,"div",9),f(47,"New Hires This Month"),h()()(),m(48,"mat-card",11)(49,"mat-card-content")(50,"div",12)(51,"mat-icon"),f(52,"payments"),h()(),m(53,"div",8),f(54),me(55,"currency"),h(),m(56,"div",9),f(57,"Average Salary"),h()()()(),m(58,"div",13)(59,"mat-card",14)(60,"mat-card-header")(61,"mat-card-title"),f(62,"Employees by Department"),h()(),m(63,"mat-card-content"),A(64,p_e,2,2,"div",15)(65,f_e,5,0,"div",16),h()(),m(66,"mat-card",14)(67,"mat-card-header")(68,"mat-card-title"),f(69,"Top 10 Positions"),h()(),m(70,"mat-card-content"),A(71,g_e,2,2,"div",15)(72,__e,5,0,"div",16),h()()(),m(73,"div",13)(74,"mat-card",14)(75,"mat-card-header")(76,"mat-card-title"),f(77,"Gender Distribution"),h()(),m(78,"mat-card-content"),A(79,b_e,2,2,"div",15)(80,v_e,5,0,"div",16),h()(),m(81,"mat-card",14)(82,"mat-card-header")(83,"mat-card-title"),f(84,"Salary Range Distribution"),h()(),m(85,"mat-card-content"),A(86,y_e,2,2,"div",15)(87,x_e,5,0,"div",16),h()()(),m(88,"div",17)(89,"mat-card")(90,"mat-card-header")(91,"mat-card-title"),f(92,"Recent Employees"),h(),m(93,"button",18),S("click",function(){k(e);let r=x();return T(r.navigateToEmployees())}),f(94," View All "),m(95,"mat-icon"),f(96,"arrow_forward"),h()()(),m(97,"mat-card-content"),A(98,w_e,2,1,"mat-list",19)(99,D_e,5,0,"div",16),h()()(),A(100,S_e,13,4,"div",20),h()}if(t&2){let e=x();g(8),N(e.metrics.totalEmployees),g(9),N(e.metrics.totalDepartments),g(9),N(e.metrics.totalPositions),g(9),N(e.metrics.totalSalaryRanges),g(10),N(e.metrics.newHiresThisMonth),g(9),N(Tm(55,17,e.metrics.averageSalary,"USD","symbol","1.0-0")),g(10),v("ngIf",e.departmentChartData),g(),v("ngIf",!e.departmentChartData),g(6),v("ngIf",e.positionChartData),g(),v("ngIf",!e.positionChartData),g(7),v("ngIf",e.genderChartData),g(),v("ngIf",!e.genderChartData),g(6),v("ngIf",e.salaryChartData),g(),v("ngIf",!e.salaryChartData),g(11),v("ngIf",e.metrics.recentEmployees&&e.metrics.recentEmployees.length>0),g(),v("ngIf",!e.metrics.recentEmployees||e.metrics.recentEmployees.length===0),g(),v("appHasRole",dt(22,m_e))}}function T_e(t,n){if(t&1){let e=q();m(0,"div",37)(1,"mat-card")(2,"mat-card-content")(3,"div",23)(4,"mat-icon"),f(5,"error_outline"),h(),m(6,"p"),f(7,"Unable to load dashboard data"),h(),m(8,"button",34),S("click",function(){k(e);let r=x();return T(r.loadDashboardMetrics())}),m(9,"mat-icon"),f(10,"refresh"),h(),f(11," Retry "),h()()()()()}}var g6=(()=>{let n=class n{constructor(){this.dashboardService=u(h6),this.router=u(Ae),this.snackBar=u(_i),this.loading=!0,this.metrics=null,this.departmentChartData=null,this.departmentChartOptions={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"bottom"},tooltip:{callbacks:{label:i=>{let r=i.label||"",o=i.parsed||0;return`${r}: ${o} employees`}}}}},this.positionChartData=null,this.positionChartOptions={responsive:!0,maintainAspectRatio:!1,indexAxis:"y",plugins:{legend:{display:!1},tooltip:{callbacks:{label:i=>`${i.parsed.x} employees`}}},scales:{x:{beginAtZero:!0,ticks:{precision:0}}}},this.genderChartData=null,this.genderChartOptions={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"bottom"},tooltip:{callbacks:{label:i=>{let r=i.label||"",o=i.parsed||0;return`${r}: ${o} employees`}}}}},this.salaryChartData=null,this.salaryChartOptions={responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1},tooltip:{callbacks:{label:i=>`${i.parsed.y} employees`}}},scales:{y:{beginAtZero:!0,ticks:{precision:0}}}}}ngOnInit(){this.loadDashboardMetrics()}loadDashboardMetrics(){this.loading=!0,this.dashboardService.getDashboardMetrics().subscribe({next:i=>{this.metrics=i,this.prepareCharts(i),this.loading=!1},error:i=>{console.error("Error loading dashboard metrics:",i),this.showMessage("Error loading dashboard data"),this.loading=!1}})}prepareCharts(i){this.prepareDepartmentChart(i.employeesByDepartment),this.preparePositionChart(i.employeesByPosition),this.prepareGenderChart(i.genderDistribution),this.prepareSalaryChart(i.employeesBySalaryRange)}prepareDepartmentChart(i){if(!i||i.length===0){this.departmentChartData=null;return}this.departmentChartData={labels:i.map(r=>r.departmentName),datasets:[{data:i.map(r=>r.employeeCount),backgroundColor:["#FF6384","#36A2EB","#FFCE56","#4BC0C0","#9966FF","#FF9F40","#FF6384","#C9CBCF"]}]}}preparePositionChart(i){if(!i||i.length===0){this.positionChartData=null;return}this.positionChartData={labels:i.map(r=>r.positionTitle),datasets:[{label:"Employees",data:i.map(r=>r.employeeCount),backgroundColor:"#36A2EB"}]}}prepareGenderChart(i){if(!i){this.genderChartData=null;return}this.genderChartData={labels:["Male","Female"],datasets:[{data:[i.male,i.female],backgroundColor:["#36A2EB","#FF6384"]}]}}prepareSalaryChart(i){if(!i||i.length===0){this.salaryChartData=null;return}this.salaryChartData={labels:i.map(r=>r.rangeName),datasets:[{label:"Employees",data:i.map(r=>r.employeeCount),backgroundColor:"#4BC0C0"}]}}navigateToAddEmployee(){this.router.navigate(["/employees/create"])}navigateToAddDepartment(){this.router.navigate(["/departments/create"])}navigateToAddPosition(){this.router.navigate(["/positions/create"])}navigateToEmployees(){this.router.navigate(["/employees"])}navigateToEmployee(i){this.router.navigate(["/employees",i])}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-dashboard"]],decls:4,vars:3,consts:[["class","loading-spinner",4,"ngIf"],["class","dashboard-container",4,"ngIf"],["class","error-state",4,"ngIf"],[1,"loading-spinner"],[1,"dashboard-container"],[1,"metrics-row"],[1,"metric-card"],[1,"metric-icon"],[1,"metric-value"],[1,"metric-label"],[1,"secondary-metrics-row"],[1,"metric-card","secondary"],[1,"metric-icon","secondary"],[1,"charts-row"],[1,"chart-card"],["class","chart-container",4,"ngIf"],["class","no-data",4,"ngIf"],[1,"activity-row"],["mat-button","","color","primary",3,"click"],[4,"ngIf"],["class","actions-row",4,"appHasRole"],[1,"chart-container"],["baseChart","","type","pie",3,"data","options"],[1,"no-data"],["baseChart","","type","bar",3,"data","options"],["baseChart","","type","doughnut",3,"data","options"],["class","clickable-item",3,"click",4,"ngFor","ngForOf"],[1,"clickable-item",3,"click"],["matListItemIcon",""],["matListItemTitle",""],["matListItemLine",""],["matListItemMeta",""],[1,"actions-row"],[1,"action-buttons"],["mat-raised-button","","color","primary",3,"click"],["mat-raised-button","","color","accent",3,"click",4,"appHasRole"],["mat-raised-button","","color","accent",3,"click"],[1,"error-state"]],template:function(r,o){r&1&&(M(0,"page-header"),A(1,h_e,2,0,"div",0)(2,k_e,101,23,"div",1)(3,T_e,12,0,"div",2)),r&2&&(g(),v("ngIf",o.loading),g(),v("ngIf",!o.loading&&o.metrics),g(),v("ngIf",!o.loading&&!o.metrics))},dependencies:[Je,Un,Wt,It,kt,Tt,Rt,Ot,Ge,Ze,Fe,_t,Zt,Kt,fa,Js,pa,y_,Fu,Hl,FI,wi,ez,Lt,Xn,yl,Wa],styles:[".loading-spinner[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:400px}.dashboard-container[_ngcontent-%COMP%]{padding:16px;max-width:1400px;margin:0 auto}.metrics-row[_ngcontent-%COMP%], .secondary-metrics-row[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}@media (max-width: 768px){.metrics-row[_ngcontent-%COMP%], .secondary-metrics-row[_ngcontent-%COMP%]{grid-template-columns:repeat(2,1fr)}}@media (max-width: 480px){.metrics-row[_ngcontent-%COMP%], .secondary-metrics-row[_ngcontent-%COMP%]{grid-template-columns:1fr}}.secondary-metrics-row[_ngcontent-%COMP%]{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}.metric-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:24px!important;text-align:center}.metric-card[_ngcontent-%COMP%] .metric-icon[_ngcontent-%COMP%]{display:inline-flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:50%;background-color:#3f51b51a;margin-bottom:16px}.metric-card[_ngcontent-%COMP%] .metric-icon[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:32px;width:32px;height:32px;color:#3f51b5}.metric-card[_ngcontent-%COMP%] .metric-icon.secondary[_ngcontent-%COMP%]{background-color:#ff98001a}.metric-card[_ngcontent-%COMP%] .metric-icon.secondary[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{color:#ff9800}.metric-card[_ngcontent-%COMP%] .metric-value[_ngcontent-%COMP%]{font-size:32px;font-weight:600;color:#000000de;margin-bottom:8px;line-height:1.2}.metric-card[_ngcontent-%COMP%] .metric-label[_ngcontent-%COMP%]{font-size:14px;color:#0009;text-transform:uppercase;letter-spacing:.5px}.metric-card[_ngcontent-%COMP%]:hover{box-shadow:0 4px 8px #00000026;transition:box-shadow .3s ease}.charts-row[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));gap:24px;margin-bottom:24px}@media (max-width: 900px){.charts-row[_ngcontent-%COMP%]{grid-template-columns:1fr}}.chart-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%]{padding:16px 16px 0;margin-bottom:16px}.chart-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:18px;font-weight:500;margin:0}.chart-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:0 16px 16px}.chart-card[_ngcontent-%COMP%] .chart-container[_ngcontent-%COMP%]{position:relative;height:300px;width:100%}.chart-card[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;color:#00000061}.chart-card[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:12px}.chart-card[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:14px}.activity-row[_ngcontent-%COMP%]{margin-bottom:24px}.activity-row[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}.activity-row[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:18px;font-weight:500;margin:0}.activity-row[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-left:4px;font-size:18px;width:18px;height:18px}.activity-row[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:0}.activity-row[_ngcontent-%COMP%] mat-list[_ngcontent-%COMP%]{padding:0}.activity-row[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%]{height:auto;padding:12px 16px;cursor:pointer;transition:background-color .2s ease}.activity-row[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%]:hover{background-color:#0000000a}.activity-row[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%]:not(:last-child){border-bottom:1px solid rgba(0,0,0,.08)}.activity-row[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{color:#0000008a}.activity-row[_ngcontent-%COMP%] .clickable-item[_ngcontent-%COMP%]{cursor:pointer}.activity-row[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;color:#00000061}.activity-row[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:12px}.activity-row[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:14px}.actions-row[_ngcontent-%COMP%]{margin-bottom:24px}.actions-row[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%]{padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}.actions-row[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:18px;font-weight:500;margin:0}.actions-row[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:24px 16px}.actions-row[_ngcontent-%COMP%] .action-buttons[_ngcontent-%COMP%]{display:flex;gap:12px;flex-wrap:wrap}.actions-row[_ngcontent-%COMP%] .action-buttons[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:8px}@media (max-width: 600px){.actions-row[_ngcontent-%COMP%] .action-buttons[_ngcontent-%COMP%]{flex-direction:column}.actions-row[_ngcontent-%COMP%] .action-buttons[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{width:100%}}.error-state[_ngcontent-%COMP%]{padding:16px}.error-state[_ngcontent-%COMP%] mat-card[_ngcontent-%COMP%]{max-width:600px;margin:0 auto}.error-state[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;color:#0000008a}.error-state[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:64px;width:64px;height:64px;margin-bottom:16px;color:#f44336}.error-state[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0 0 24px;font-size:16px}.error-state[_ngcontent-%COMP%] .no-data[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:8px;font-size:20px;width:20px;height:20px}.ai-insights-card[_ngcontent-%COMP%]{margin:16px 16px 0}.ai-insights-card[_ngcontent-%COMP%] mat-card-avatar[_ngcontent-%COMP%]{background:none;color:#1976d2}.ai-insights-card[_ngcontent-%COMP%] .ai-insight-loading[_ngcontent-%COMP%]{display:flex;align-items:center;gap:12px;color:#0000008a;font-size:14px;padding:8px 0}.ai-insights-card[_ngcontent-%COMP%] .ai-insight-text[_ngcontent-%COMP%]{font-size:15px;line-height:1.6;color:#000000de;margin:0;padding:4px 0}.ai-insights-card[_ngcontent-%COMP%] .ai-insight-error[_ngcontent-%COMP%]{display:flex;align-items:center;gap:8px;font-size:14px;color:#b00020}.ai-insights-card[_ngcontent-%COMP%] .ai-insight-error[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:18px;height:18px;width:18px}@media (max-width: 1200px){.dashboard-container[_ngcontent-%COMP%]{padding:12px}.charts-row[_ngcontent-%COMP%]{gap:16px}}@media (max-width: 768px){.metric-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:16px!important}.metric-card[_ngcontent-%COMP%] .metric-icon[_ngcontent-%COMP%]{width:56px;height:56px}.metric-card[_ngcontent-%COMP%] .metric-icon[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:28px;width:28px;height:28px}.metric-card[_ngcontent-%COMP%] .metric-value[_ngcontent-%COMP%]{font-size:28px}.metric-card[_ngcontent-%COMP%] .metric-label[_ngcontent-%COMP%]{font-size:12px}.chart-card[_ngcontent-%COMP%] .chart-container[_ngcontent-%COMP%]{height:250px}}@media (max-width: 480px){.dashboard-container[_ngcontent-%COMP%]{padding:8px}.metrics-row[_ngcontent-%COMP%], .secondary-metrics-row[_ngcontent-%COMP%], .charts-row[_ngcontent-%COMP%]{gap:12px;margin-bottom:16px}.metric-card[_ngcontent-%COMP%] .metric-value[_ngcontent-%COMP%]{font-size:24px}.chart-card[_ngcontent-%COMP%] .chart-container[_ngcontent-%COMP%]{height:200px}}"]});let t=n;return t})();var _6=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-error-403"]],decls:1,vars:0,consts:[["code","403","title","Permission denied!","message","You do not have permission to access the requested data."]],template:function(r,o){r&1&&M(0,"error-code",0)},dependencies:[ap],encapsulation:2});let t=n;return t})();var b6=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-error-404"]],decls:1,vars:0,consts:[["code","404","title","Page not found!","message","This is not the web page you are looking for."]],template:function(r,o){r&1&&M(0,"error-code",0)},dependencies:[ap],encapsulation:2});let t=n;return t})();var v6=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-error-500"]],decls:1,vars:0,consts:[["code","500","title","Server went wrong!","message","Just kidding, looks like we have an internal issue, please try refreshing."]],template:function(r,o){r&1&&M(0,"error-code",0)},dependencies:[ap],encapsulation:2});let t=n;return t})();var I_e=t=>({value:t});function A_e(t,n){t&1&&(m(0,"mat-error")(1,"span"),f(2),me(3,"translate"),h()()),t&2&&(g(2),N(Re(3,1,"validation.required")))}function O_e(t,n){t&1&&(m(0,"mat-error")(1,"span"),f(2),me(3,"translate"),h()()),t&2&&(g(2),N(Re(3,1,"validation.required")))}function R_e(t,n){t&1&&(m(0,"mat-error")(1,"span"),f(2),me(3,"translate"),h()()),t&2&&(g(2),N(Re(3,1,"validation.required")))}function P_e(t,n){t&1&&(m(0,"mat-error",8),me(1,"translate"),m(2,"span"),f(3),h()()),t&2&&(v("translateParams",$t(4,I_e,Re(1,2,"login.password"))),g(3),N("validation.inconsistent"))}var y6=(()=>{let n=class n{constructor(){this.fb=u(co),this.registerForm=this.fb.nonNullable.group({username:["",[Ve.required]],password:["",[Ve.required]],confirmPassword:["",[Ve.required]]},{validators:[this.matchValidator("password","confirmPassword")]})}matchValidator(i,r){return o=>{let a=o.get(i),s=o.get(r);return s.errors&&!s.errors.mismatch?null:a.value!==s.value?(s.setErrors({mismatch:!0}),{mismatch:!0}):(s.setErrors(null),null)}}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-register"]],decls:43,vars:32,consts:[[1,"d-flex","w-full","h-full"],[1,"m-auto",2,"max-width","380px"],[1,"m-b-24"],[1,"form-field-full",3,"formGroup"],["appearance","outline"],["matInput","","formControlName","username","required",""],["matInput","","type","password","formControlName","password","required",""],["matInput","","type","password","formControlName","confirmPassword","required",""],["translate","",3,"translateParams"],["matButton","filled",1,"w-full","m-y-16"],["routerLink","/auth/login"]],template:function(r,o){if(r&1&&(m(0,"div",0)(1,"mat-card",1)(2,"mat-card-header",2)(3,"mat-card-title"),f(4),me(5,"translate"),M(6,"br"),f(7),me(8,"translate"),h()(),m(9,"mat-card-content")(10,"form",3)(11,"mat-form-field",4)(12,"mat-label"),f(13),me(14,"translate"),h(),M(15,"input",5),L(16,A_e,4,3,"mat-error"),h(),m(17,"mat-form-field",4)(18,"mat-label"),f(19),me(20,"translate"),h(),M(21,"input",6),L(22,O_e,4,3,"mat-error"),h(),m(23,"mat-form-field",4)(24,"mat-label"),f(25),me(26,"translate"),h(),M(27,"input",7),L(28,R_e,4,3,"mat-error"),L(29,P_e,4,6,"mat-error",8),h(),m(30,"mat-checkbox"),f(31),me(32,"translate"),h(),m(33,"button",9),f(34),me(35,"translate"),h(),m(36,"div")(37,"span"),f(38),me(39,"translate"),h(),m(40,"a",10),f(41),me(42,"translate"),h()()()()()()),r&2){let a,s,l,c;g(4),fe(" ",Re(5,14,"register_welcome"),", "),g(3),fe(" ",Re(8,16,"register_title")," "),g(3),v("formGroup",o.registerForm),g(3),N(Re(14,18,"username")),g(3),V((a=o.registerForm.get("username"))!=null&&a.invalid?16:-1),g(3),N(Re(20,20,"password")),g(3),V((s=o.registerForm.get("password"))!=null&&s.invalid?22:-1),g(3),N(Re(26,22,"confirm_password")),g(3),V((l=o.registerForm.get("confirmPassword"))!=null&&l.hasError("required")?28:-1),g(),V((c=o.registerForm.get("confirmPassword"))!=null&&c.hasError("mismatch")?29:-1),g(2),N(Re(32,24,"read_and_agree")),g(3),N(Re(35,26,"register")),g(4),fe("",Re(39,28,"have_an_account"),"?"),g(3),N(Re(42,30,"login"))}},dependencies:[Wn,Qr,lo,di,Pt,so,Fo,Zn,nn,Yr,Fe,_t,It,kt,Tt,Rt,Ot,kx,is,ai,Xt,gi,Ao,Bi,Ci,Rr,Y3,Or],encapsulation:2});let t=n;return t})();var x6=(()=>{let n=class n{constructor(){this.authService=u(jt),this.router=u(Ae)}ngOnInit(){return yn(this,null,function*(){try{(yield this.authService.initAuth())?this.router.navigate(["/dashboard"]):(console.warn("Authentication callback failed, redirecting to dashboard as guest"),this.router.navigate(["/dashboard"]))}catch(i){console.error("Error processing callback:",i),this.router.navigate(["/dashboard"])}})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-callback"]],decls:5,vars:0,consts:[[1,"callback-container"]],template:function(r,o){r&1&&(gt(0,"div",0)(1,"h2"),f(2,"Processing login..."),yt(),gt(3,"p"),f(4,"Please wait while we complete your authentication."),yt()())},styles:[".callback-container[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;text-align:center}"]});let t=n;return t})();var F_e=[[["caption"]],[["colgroup"],["col"]],"*"],N_e=["caption","colgroup, col","*"];function L_e(t,n){t&1&&ne(0,2)}function V_e(t,n){t&1&&(m(0,"thead",0),qe(1,1),h(),m(2,"tbody",0),qe(3,2)(4,3),h(),m(5,"tfoot",0),qe(6,4),h())}function B_e(t,n){t&1&&qe(0,1)(1,2)(2,3)(3,4)}var os=new O("CDK_TABLE");var bw=(()=>{class t{template=u(te);constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkCellDef",""]]})}return t})(),vw=(()=>{class t{template=u(te);constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkHeaderCellDef",""]]})}return t})(),D6=(()=>{class t{template=u(te);constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkFooterCellDef",""]]})}return t})(),mp=(()=>{class t{_table=u(os,{optional:!0});_hasStickyChanged=!1;get name(){return this._name}set name(e){this._setNameInput(e)}_name;get sticky(){return this._sticky}set sticky(e){e!==this._sticky&&(this._sticky=e,this._hasStickyChanged=!0)}_sticky=!1;get stickyEnd(){return this._stickyEnd}set stickyEnd(e){e!==this._stickyEnd&&(this._stickyEnd=e,this._hasStickyChanged=!0)}_stickyEnd=!1;cell;headerCell;footerCell;cssClassFriendlyName;_columnCssClassName;constructor(){}hasStickyChanged(){let e=this._hasStickyChanged;return this.resetStickyChanged(),e}resetStickyChanged(){this._hasStickyChanged=!1}_updateColumnCssClassName(){this._columnCssClassName=[`cdk-column-${this.cssClassFriendlyName}`]}_setNameInput(e){e&&(this._name=e,this.cssClassFriendlyName=e.replace(/[^a-z0-9_-]/gi,"-"),this._updateColumnCssClassName())}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkColumnDef",""]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,bw,5),Ce(o,vw,5),Ce(o,D6,5)),i&2){let a;j(a=H())&&(r.cell=a.first),j(a=H())&&(r.headerCell=a.first),j(a=H())&&(r.footerCell=a.first)}},inputs:{name:[0,"cdkColumnDef","name"],sticky:[2,"sticky","sticky",B],stickyEnd:[2,"stickyEnd","stickyEnd",B]},features:[we([{provide:"MAT_SORT_HEADER_COLUMN_DEF",useExisting:t}])]})}return t})(),gw=class{constructor(n,e){e.nativeElement.classList.add(...n._columnCssClassName)}},M6=(()=>{class t extends gw{constructor(){super(u(mp),u(Y))}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["cdk-header-cell"],["th","cdk-header-cell",""]],hostAttrs:["role","columnheader",1,"cdk-header-cell"],features:[le]})}return t})();var E6=(()=>{class t extends gw{constructor(){let e=u(mp),i=u(Y);super(e,i);let r=e._table?._getCellRole();r&&i.nativeElement.setAttribute("role",r)}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["cdk-cell"],["td","cdk-cell",""]],hostAttrs:[1,"cdk-cell"],features:[le]})}return t})();var qI=(()=>{class t{template=u(te);_differs=u(Ss);columns;_columnsDiffer;constructor(){}ngOnChanges(e){if(!this._columnsDiffer){let i=e.columns&&e.columns.currentValue||[];this._columnsDiffer=this._differs.find(i).create(),this._columnsDiffer.diff(i)}}getColumnsDiff(){return this._columnsDiffer.diff(this.columns)}extractCellTemplate(e){return this instanceof D_?e.headerCell.template:this instanceof YI?e.footerCell.template:e.cell.template}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,features:[Oe]})}return t})(),D_=(()=>{class t extends qI{_table=u(os,{optional:!0});_hasStickyChanged=!1;get sticky(){return this._sticky}set sticky(e){e!==this._sticky&&(this._sticky=e,this._hasStickyChanged=!0)}_sticky=!1;constructor(){super(u(te),u(Ss))}ngOnChanges(e){super.ngOnChanges(e)}hasStickyChanged(){let e=this._hasStickyChanged;return this.resetStickyChanged(),e}resetStickyChanged(){this._hasStickyChanged=!1}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkHeaderRowDef",""]],inputs:{columns:[0,"cdkHeaderRowDef","columns"],sticky:[2,"cdkHeaderRowDefSticky","sticky",B]},features:[le,Oe]})}return t})(),YI=(()=>{class t extends qI{_table=u(os,{optional:!0});_hasStickyChanged=!1;get sticky(){return this._sticky}set sticky(e){e!==this._sticky&&(this._sticky=e,this._hasStickyChanged=!0)}_sticky=!1;constructor(){super(u(te),u(Ss))}ngOnChanges(e){super.ngOnChanges(e)}hasStickyChanged(){let e=this._hasStickyChanged;return this.resetStickyChanged(),e}resetStickyChanged(){this._hasStickyChanged=!1}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkFooterRowDef",""]],inputs:{columns:[0,"cdkFooterRowDef","columns"],sticky:[2,"cdkFooterRowDefSticky","sticky",B]},features:[le,Oe]})}return t})(),yw=(()=>{class t extends qI{_table=u(os,{optional:!0});when;constructor(){super(u(te),u(Ss))}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkRowDef",""]],inputs:{columns:[0,"cdkRowDefColumns","columns"],when:[0,"cdkRowDefWhen","when"]},features:[le]})}return t})(),Nu=(()=>{class t{_viewContainer=u(st);cells;context;static mostRecentCellOutlet=null;constructor(){t.mostRecentCellOutlet=this}ngOnDestroy(){t.mostRecentCellOutlet===this&&(t.mostRecentCellOutlet=null)}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","cdkCellOutlet",""]]})}return t})(),QI=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["cdk-header-row"],["tr","cdk-header-row",""]],hostAttrs:["role","row",1,"cdk-header-row"],decls:1,vars:0,consts:[["cdkCellOutlet",""]],template:function(i,r){i&1&&qe(0,0)},dependencies:[Nu],encapsulation:2})}return t})();var KI=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["cdk-row"],["tr","cdk-row",""]],hostAttrs:["role","row",1,"cdk-row"],decls:1,vars:0,consts:[["cdkCellOutlet",""]],template:function(i,r){i&1&&qe(0,0)},dependencies:[Nu],encapsulation:2})}return t})(),xw=(()=>{class t{templateRef=u(te);_contentClassNames=["cdk-no-data-row","cdk-row"];_cellClassNames=["cdk-cell","cdk-no-data-cell"];_cellSelector="td, cdk-cell, [cdk-cell], .cdk-cell";constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["ng-template","cdkNoDataRow",""]]})}return t})(),C6=["top","bottom","left","right"],GI=class{_isNativeHtmlTable;_stickCellCss;_isBrowser;_needsPositionStickyOnElement;direction;_positionListener;_tableInjector;_elemSizeCache=new WeakMap;_resizeObserver=globalThis?.ResizeObserver?new globalThis.ResizeObserver(n=>this._updateCachedSizes(n)):null;_updatedStickyColumnsParamsToReplay=[];_stickyColumnsReplayTimeout=null;_cachedCellWidths=[];_borderCellCss;_destroyed=!1;constructor(n,e,i=!0,r=!0,o,a,s){this._isNativeHtmlTable=n,this._stickCellCss=e,this._isBrowser=i,this._needsPositionStickyOnElement=r,this.direction=o,this._positionListener=a,this._tableInjector=s,this._borderCellCss={top:`${e}-border-elem-top`,bottom:`${e}-border-elem-bottom`,left:`${e}-border-elem-left`,right:`${e}-border-elem-right`}}clearStickyPositioning(n,e){(e.includes("left")||e.includes("right"))&&this._removeFromStickyColumnReplayQueue(n);let i=[];for(let r of n)r.nodeType===r.ELEMENT_NODE&&i.push(r,...Array.from(r.children));vt({write:()=>{for(let r of i)this._removeStickyStyle(r,e)}},{injector:this._tableInjector})}updateStickyColumns(n,e,i,r=!0,o=!0){if(!n.length||!this._isBrowser||!(e.some(C=>C)||i.some(C=>C))){this._positionListener?.stickyColumnsUpdated({sizes:[]}),this._positionListener?.stickyEndColumnsUpdated({sizes:[]});return}let a=n[0],s=a.children.length,l=this.direction==="rtl",c=l?"right":"left",d=l?"left":"right",p=e.lastIndexOf(!0),_=i.indexOf(!0),b,y,w;o&&this._updateStickyColumnReplayQueue({rows:[...n],stickyStartStates:[...e],stickyEndStates:[...i]}),vt({earlyRead:()=>{b=this._getCellWidths(a,r),y=this._getStickyStartColumnPositions(b,e),w=this._getStickyEndColumnPositions(b,i)},write:()=>{for(let C of n)for(let D=0;D!!C)&&(this._positionListener.stickyColumnsUpdated({sizes:p===-1?[]:b.slice(0,p+1).map((C,D)=>e[D]?C:null)}),this._positionListener.stickyEndColumnsUpdated({sizes:_===-1?[]:b.slice(_).map((C,D)=>i[D+_]?C:null).reverse()}))}},{injector:this._tableInjector})}stickRows(n,e,i){if(!this._isBrowser)return;let r=i==="bottom"?n.slice().reverse():n,o=i==="bottom"?e.slice().reverse():e,a=[],s=[],l=[];vt({earlyRead:()=>{for(let c=0,d=0;c{let c=o.lastIndexOf(!0);for(let d=0;d{let i=n.querySelector("tfoot");i&&(e.some(r=>!r)?this._removeStickyStyle(i,["bottom"]):this._addStickyStyle(i,"bottom",0,!1))}},{injector:this._tableInjector})}destroy(){this._stickyColumnsReplayTimeout&&clearTimeout(this._stickyColumnsReplayTimeout),this._resizeObserver?.disconnect(),this._destroyed=!0}_removeStickyStyle(n,e){if(!n.classList.contains(this._stickCellCss))return;for(let r of e)n.style[r]="",n.classList.remove(this._borderCellCss[r]);C6.some(r=>e.indexOf(r)===-1&&n.style[r])?n.style.zIndex=this._getCalculatedZIndex(n):(n.style.zIndex="",this._needsPositionStickyOnElement&&(n.style.position=""),n.classList.remove(this._stickCellCss))}_addStickyStyle(n,e,i,r){n.classList.add(this._stickCellCss),r&&n.classList.add(this._borderCellCss[e]),n.style[e]=`${i}px`,n.style.zIndex=this._getCalculatedZIndex(n),this._needsPositionStickyOnElement&&(n.style.cssText+="position: -webkit-sticky; position: sticky; ")}_getCalculatedZIndex(n){let e={top:100,bottom:10,left:1,right:1},i=0;for(let r of C6)n.style[r]&&(i+=e[r]);return i?`${i}`:""}_getCellWidths(n,e=!0){if(!e&&this._cachedCellWidths.length)return this._cachedCellWidths;let i=[],r=n.children;for(let o=0;o0;o--)e[o]&&(i[o]=r,r+=n[o]);return i}_retrieveElementSize(n){let e=this._elemSizeCache.get(n);if(e)return e;let i=n.getBoundingClientRect(),r={width:i.width,height:i.height};return this._resizeObserver&&(this._elemSizeCache.set(n,r),this._resizeObserver.observe(n,{box:"border-box"})),r}_updateStickyColumnReplayQueue(n){this._removeFromStickyColumnReplayQueue(n.rows),this._stickyColumnsReplayTimeout||this._updatedStickyColumnsParamsToReplay.push(n)}_removeFromStickyColumnReplayQueue(n){let e=new Set(n);for(let i of this._updatedStickyColumnsParamsToReplay)i.rows=i.rows.filter(r=>!e.has(r));this._updatedStickyColumnsParamsToReplay=this._updatedStickyColumnsParamsToReplay.filter(i=>!!i.rows.length)}_updateCachedSizes(n){let e=!1;for(let i of n){let r=i.borderBoxSize?.length?{width:i.borderBoxSize[0].inlineSize,height:i.borderBoxSize[0].blockSize}:{width:i.contentRect.width,height:i.contentRect.height};r.width!==this._elemSizeCache.get(i.target)?.width&&j_e(i.target)&&(e=!0),this._elemSizeCache.set(i.target,r)}e&&this._updatedStickyColumnsParamsToReplay.length&&(this._stickyColumnsReplayTimeout&&clearTimeout(this._stickyColumnsReplayTimeout),this._stickyColumnsReplayTimeout=setTimeout(()=>{if(!this._destroyed){for(let i of this._updatedStickyColumnsParamsToReplay)this.updateStickyColumns(i.rows,i.stickyStartStates,i.stickyEndStates,!0,!1);this._updatedStickyColumnsParamsToReplay=[],this._stickyColumnsReplayTimeout=null}},0))}};function j_e(t){return["cdk-cell","cdk-header-cell","cdk-footer-cell"].some(n=>t.classList.contains(n))}var _w=new O("CDK_SPL");var ZI=(()=>{class t{viewContainer=u(st);elementRef=u(Y);constructor(){let e=u(os);e._rowOutlet=this,e._outletAssigned()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","rowOutlet",""]]})}return t})(),XI=(()=>{class t{viewContainer=u(st);elementRef=u(Y);constructor(){let e=u(os);e._headerRowOutlet=this,e._outletAssigned()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","headerRowOutlet",""]]})}return t})(),JI=(()=>{class t{viewContainer=u(st);elementRef=u(Y);constructor(){let e=u(os);e._footerRowOutlet=this,e._outletAssigned()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","footerRowOutlet",""]]})}return t})(),eA=(()=>{class t{viewContainer=u(st);elementRef=u(Y);constructor(){let e=u(os);e._noDataRowOutlet=this,e._outletAssigned()}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","noDataRowOutlet",""]]})}return t})(),tA=(()=>{class t{_differs=u(Ss);_changeDetectorRef=u(ye);_elementRef=u(Y);_dir=u(Yt,{optional:!0});_platform=u(Ye);_viewRepeater=u(yh);_viewportRuler=u(sr);_stickyPositioningListener=u(_w,{optional:!0,skipSelf:!0});_document=u(_e);_data;_onDestroy=new z;_renderRows;_renderChangeSubscription;_columnDefsByName=new Map;_rowDefs;_headerRowDefs;_footerRowDefs;_dataDiffer;_defaultRowDef;_customColumnDefs=new Set;_customRowDefs=new Set;_customHeaderRowDefs=new Set;_customFooterRowDefs=new Set;_customNoDataRow;_headerRowDefChanged=!0;_footerRowDefChanged=!0;_stickyColumnStylesNeedReset=!0;_forceRecalculateCellWidths=!0;_cachedRenderRowsMap=new Map;_isNativeHtmlTable;_stickyStyler;stickyCssClass="cdk-table-sticky";needsPositionStickyOnElement=!0;_isServer;_isShowingNoDataRow=!1;_hasAllOutlets=!1;_hasInitialized=!1;_getCellRole(){if(this._cellRoleInternal===void 0){let e=this._elementRef.nativeElement.getAttribute("role");return e==="grid"||e==="treegrid"?"gridcell":"cell"}return this._cellRoleInternal}_cellRoleInternal=void 0;get trackBy(){return this._trackByFn}set trackBy(e){this._trackByFn=e}_trackByFn;get dataSource(){return this._dataSource}set dataSource(e){this._dataSource!==e&&this._switchDataSource(e)}_dataSource;get multiTemplateDataRows(){return this._multiTemplateDataRows}set multiTemplateDataRows(e){this._multiTemplateDataRows=e,this._rowOutlet&&this._rowOutlet.viewContainer.length&&(this._forceRenderDataRows(),this.updateStickyColumnStyles())}_multiTemplateDataRows=!1;get fixedLayout(){return this._fixedLayout}set fixedLayout(e){this._fixedLayout=e,this._forceRecalculateCellWidths=!0,this._stickyColumnStylesNeedReset=!0}_fixedLayout=!1;contentChanged=new U;viewChange=new rt({start:0,end:Number.MAX_VALUE});_rowOutlet;_headerRowOutlet;_footerRowOutlet;_noDataRowOutlet;_contentColumnDefs;_contentRowDefs;_contentHeaderRowDefs;_contentFooterRowDefs;_noDataRow;_injector=u(de);constructor(){u(new Li("role"),{optional:!0})||this._elementRef.nativeElement.setAttribute("role","table"),this._isServer=!this._platform.isBrowser,this._isNativeHtmlTable=this._elementRef.nativeElement.nodeName==="TABLE",this._dataDiffer=this._differs.find([]).create((i,r)=>this.trackBy?this.trackBy(r.dataIndex,r.data):r)}ngOnInit(){this._setupStickyStyler(),this._viewportRuler.change().pipe(xe(this._onDestroy)).subscribe(()=>{this._forceRecalculateCellWidths=!0})}ngAfterContentInit(){this._hasInitialized=!0}ngAfterContentChecked(){this._canRender()&&this._render()}ngOnDestroy(){this._stickyStyler?.destroy(),[this._rowOutlet?.viewContainer,this._headerRowOutlet?.viewContainer,this._footerRowOutlet?.viewContainer,this._cachedRenderRowsMap,this._customColumnDefs,this._customRowDefs,this._customHeaderRowDefs,this._customFooterRowDefs,this._columnDefsByName].forEach(e=>{e?.clear()}),this._headerRowDefs=[],this._footerRowDefs=[],this._defaultRowDef=null,this._onDestroy.next(),this._onDestroy.complete(),lg(this.dataSource)&&this.dataSource.disconnect(this)}renderRows(){this._renderRows=this._getAllRenderRows();let e=this._dataDiffer.diff(this._renderRows);if(!e){this._updateNoDataRow(),this.contentChanged.next();return}let i=this._rowOutlet.viewContainer;this._viewRepeater.applyChanges(e,i,(r,o,a)=>this._getEmbeddedViewArgs(r.item,a),r=>r.item.data,r=>{r.operation===Sc.INSERTED&&r.context&&this._renderCellTemplateForItem(r.record.item.rowDef,r.context)}),this._updateRowIndexContext(),e.forEachIdentityChange(r=>{let o=i.get(r.currentIndex);o.context.$implicit=r.item.data}),this._updateNoDataRow(),this.contentChanged.next(),this.updateStickyColumnStyles()}addColumnDef(e){this._customColumnDefs.add(e)}removeColumnDef(e){this._customColumnDefs.delete(e)}addRowDef(e){this._customRowDefs.add(e)}removeRowDef(e){this._customRowDefs.delete(e)}addHeaderRowDef(e){this._customHeaderRowDefs.add(e),this._headerRowDefChanged=!0}removeHeaderRowDef(e){this._customHeaderRowDefs.delete(e),this._headerRowDefChanged=!0}addFooterRowDef(e){this._customFooterRowDefs.add(e),this._footerRowDefChanged=!0}removeFooterRowDef(e){this._customFooterRowDefs.delete(e),this._footerRowDefChanged=!0}setNoDataRow(e){this._customNoDataRow=e}updateStickyHeaderRowStyles(){let e=this._getRenderedRows(this._headerRowOutlet);if(this._isNativeHtmlTable){let r=w6(this._headerRowOutlet,"thead");r&&(r.style.display=e.length?"":"none")}let i=this._headerRowDefs.map(r=>r.sticky);this._stickyStyler.clearStickyPositioning(e,["top"]),this._stickyStyler.stickRows(e,i,"top"),this._headerRowDefs.forEach(r=>r.resetStickyChanged())}updateStickyFooterRowStyles(){let e=this._getRenderedRows(this._footerRowOutlet);if(this._isNativeHtmlTable){let r=w6(this._footerRowOutlet,"tfoot");r&&(r.style.display=e.length?"":"none")}let i=this._footerRowDefs.map(r=>r.sticky);this._stickyStyler.clearStickyPositioning(e,["bottom"]),this._stickyStyler.stickRows(e,i,"bottom"),this._stickyStyler.updateStickyFooterContainer(this._elementRef.nativeElement,i),this._footerRowDefs.forEach(r=>r.resetStickyChanged())}updateStickyColumnStyles(){let e=this._getRenderedRows(this._headerRowOutlet),i=this._getRenderedRows(this._rowOutlet),r=this._getRenderedRows(this._footerRowOutlet);(this._isNativeHtmlTable&&!this._fixedLayout||this._stickyColumnStylesNeedReset)&&(this._stickyStyler.clearStickyPositioning([...e,...i,...r],["left","right"]),this._stickyColumnStylesNeedReset=!1),e.forEach((o,a)=>{this._addStickyColumnStyles([o],this._headerRowDefs[a])}),this._rowDefs.forEach(o=>{let a=[];for(let s=0;s{this._addStickyColumnStyles([o],this._footerRowDefs[a])}),Array.from(this._columnDefsByName.values()).forEach(o=>o.resetStickyChanged())}_outletAssigned(){!this._hasAllOutlets&&this._rowOutlet&&this._headerRowOutlet&&this._footerRowOutlet&&this._noDataRowOutlet&&(this._hasAllOutlets=!0,this._canRender()&&this._render())}_canRender(){return this._hasAllOutlets&&this._hasInitialized}_render(){this._cacheRowDefs(),this._cacheColumnDefs(),!this._headerRowDefs.length&&!this._footerRowDefs.length&&this._rowDefs.length;let i=this._renderUpdatedColumns()||this._headerRowDefChanged||this._footerRowDefChanged;this._stickyColumnStylesNeedReset=this._stickyColumnStylesNeedReset||i,this._forceRecalculateCellWidths=i,this._headerRowDefChanged&&(this._forceRenderHeaderRows(),this._headerRowDefChanged=!1),this._footerRowDefChanged&&(this._forceRenderFooterRows(),this._footerRowDefChanged=!1),this.dataSource&&this._rowDefs.length>0&&!this._renderChangeSubscription?this._observeRenderChanges():this._stickyColumnStylesNeedReset&&this.updateStickyColumnStyles(),this._checkStickyStates()}_getAllRenderRows(){let e=[],i=this._cachedRenderRowsMap;if(this._cachedRenderRowsMap=new Map,!this._data)return e;for(let r=0;r{let s=r&&r.has(a)?r.get(a):[];if(s.length){let l=s.shift();return l.dataIndex=i,l}else return{data:e,rowDef:a,dataIndex:i}})}_cacheColumnDefs(){this._columnDefsByName.clear(),fw(this._getOwnDefs(this._contentColumnDefs),this._customColumnDefs).forEach(i=>{this._columnDefsByName.has(i.name),this._columnDefsByName.set(i.name,i)})}_cacheRowDefs(){this._headerRowDefs=fw(this._getOwnDefs(this._contentHeaderRowDefs),this._customHeaderRowDefs),this._footerRowDefs=fw(this._getOwnDefs(this._contentFooterRowDefs),this._customFooterRowDefs),this._rowDefs=fw(this._getOwnDefs(this._contentRowDefs),this._customRowDefs);let e=this._rowDefs.filter(i=>!i.when);!this.multiTemplateDataRows&&e.length>1,this._defaultRowDef=e[0]}_renderUpdatedColumns(){let e=(a,s)=>{let l=!!s.getColumnsDiff();return a||l},i=this._rowDefs.reduce(e,!1);i&&this._forceRenderDataRows();let r=this._headerRowDefs.reduce(e,!1);r&&this._forceRenderHeaderRows();let o=this._footerRowDefs.reduce(e,!1);return o&&this._forceRenderFooterRows(),i||r||o}_switchDataSource(e){this._data=[],lg(this.dataSource)&&this.dataSource.disconnect(this),this._renderChangeSubscription&&(this._renderChangeSubscription.unsubscribe(),this._renderChangeSubscription=null),e||(this._dataDiffer&&this._dataDiffer.diff([]),this._rowOutlet&&this._rowOutlet.viewContainer.clear()),this._dataSource=e}_observeRenderChanges(){if(!this.dataSource)return;let e;lg(this.dataSource)?e=this.dataSource.connect(this):Gi(this.dataSource)?e=this.dataSource:Array.isArray(this.dataSource)&&(e=Q(this.dataSource)),this._renderChangeSubscription=e.pipe(xe(this._onDestroy)).subscribe(i=>{this._data=i||[],this.renderRows()})}_forceRenderHeaderRows(){this._headerRowOutlet.viewContainer.length>0&&this._headerRowOutlet.viewContainer.clear(),this._headerRowDefs.forEach((e,i)=>this._renderRow(this._headerRowOutlet,e,i)),this.updateStickyHeaderRowStyles()}_forceRenderFooterRows(){this._footerRowOutlet.viewContainer.length>0&&this._footerRowOutlet.viewContainer.clear(),this._footerRowDefs.forEach((e,i)=>this._renderRow(this._footerRowOutlet,e,i)),this.updateStickyFooterRowStyles()}_addStickyColumnStyles(e,i){let r=Array.from(i?.columns||[]).map(s=>{let l=this._columnDefsByName.get(s);return l}),o=r.map(s=>s.sticky),a=r.map(s=>s.stickyEnd);this._stickyStyler.updateStickyColumns(e,o,a,!this._fixedLayout||this._forceRecalculateCellWidths)}_getRenderedRows(e){let i=[];for(let r=0;r!o.when||o.when(i,e));else{let o=this._rowDefs.find(a=>a.when&&a.when(i,e))||this._defaultRowDef;o&&r.push(o)}return r.length,r}_getEmbeddedViewArgs(e,i){let r=e.rowDef,o={$implicit:e.data};return{templateRef:r.template,context:o,index:i}}_renderRow(e,i,r,o={}){let a=e.viewContainer.createEmbeddedView(i.template,o,r);return this._renderCellTemplateForItem(i,o),a}_renderCellTemplateForItem(e,i){for(let r of this._getCellTemplates(e))Nu.mostRecentCellOutlet&&Nu.mostRecentCellOutlet._viewContainer.createEmbeddedView(r,i);this._changeDetectorRef.markForCheck()}_updateRowIndexContext(){let e=this._rowOutlet.viewContainer;for(let i=0,r=e.length;i{let r=this._columnDefsByName.get(i);return e.extractCellTemplate(r)})}_forceRenderDataRows(){this._dataDiffer.diff([]),this._rowOutlet.viewContainer.clear(),this.renderRows()}_checkStickyStates(){let e=(i,r)=>i||r.hasStickyChanged();this._headerRowDefs.reduce(e,!1)&&this.updateStickyHeaderRowStyles(),this._footerRowDefs.reduce(e,!1)&&this.updateStickyFooterRowStyles(),Array.from(this._columnDefsByName.values()).reduce(e,!1)&&(this._stickyColumnStylesNeedReset=!0,this.updateStickyColumnStyles())}_setupStickyStyler(){let e=this._dir?this._dir.value:"ltr";this._stickyStyler=new GI(this._isNativeHtmlTable,this.stickyCssClass,this._platform.isBrowser,this.needsPositionStickyOnElement,e,this._stickyPositioningListener,this._injector),(this._dir?this._dir.change:Q()).pipe(xe(this._onDestroy)).subscribe(i=>{this._stickyStyler.direction=i,this.updateStickyColumnStyles()})}_getOwnDefs(e){return e.filter(i=>!i._table||i._table===this)}_updateNoDataRow(){let e=this._customNoDataRow||this._noDataRow;if(!e)return;let i=this._rowOutlet.viewContainer.length===0;if(i===this._isShowingNoDataRow)return;let r=this._noDataRowOutlet.viewContainer;if(i){let o=r.createEmbeddedView(e.templateRef),a=o.rootNodes[0];if(o.rootNodes.length===1&&a?.nodeType===this._document.ELEMENT_NODE){a.setAttribute("role","row"),a.classList.add(...e._contentClassNames);let s=a.querySelectorAll(e._cellSelector);for(let l=0;l{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[cg]})}return t})();var H_e=[[["caption"]],[["colgroup"],["col"]],"*"],z_e=["caption","colgroup, col","*"];function U_e(t,n){t&1&&ne(0,2)}function $_e(t,n){t&1&&(m(0,"thead",0),qe(1,1),h(),m(2,"tbody",2),qe(3,3)(4,4),h(),m(5,"tfoot",0),qe(6,5),h())}function W_e(t,n){t&1&&qe(0,1)(1,3)(2,4)(3,5)}var ba=(()=>{class t extends tA{stickyCssClass="mat-mdc-table-sticky";needsPositionStickyOnElement=!1;static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-table"],["table","mat-table",""]],hostAttrs:[1,"mat-mdc-table","mdc-data-table__table"],hostVars:2,hostBindings:function(i,r){i&2&&G("mdc-table-fixed-layout",r.fixedLayout)},exportAs:["matTable"],features:[we([{provide:tA,useExisting:t},{provide:os,useExisting:t},{provide:yh,useClass:pu},{provide:_w,useValue:null}]),le],ngContentSelectors:z_e,decls:5,vars:2,consts:[["role","rowgroup"],["headerRowOutlet",""],["role","rowgroup",1,"mdc-data-table__content"],["rowOutlet",""],["noDataRowOutlet",""],["footerRowOutlet",""]],template:function(i,r){i&1&&(Ee(H_e),ne(0),ne(1,1),L(2,U_e,1,0),L(3,$_e,7,0)(4,W_e,4,0)),i&2&&(g(2),V(r._isServer?2:-1),g(),V(r._isNativeHtmlTable?3:4))},dependencies:[XI,ZI,eA,JI],styles:[`.mat-mdc-table-sticky{position:sticky !important}mat-table{display:block}mat-header-row{min-height:var(--mat-table-header-container-height, 56px)}mat-row{min-height:var(--mat-table-row-item-container-height, 52px)}mat-footer-row{min-height:var(--mat-table-footer-container-height, 52px)}mat-row,mat-header-row,mat-footer-row{display:flex;border-width:0;border-bottom-width:1px;border-style:solid;align-items:center;box-sizing:border-box}mat-cell:first-of-type,mat-header-cell:first-of-type,mat-footer-cell:first-of-type{padding-left:24px}[dir=rtl] mat-cell:first-of-type:not(:only-of-type),[dir=rtl] mat-header-cell:first-of-type:not(:only-of-type),[dir=rtl] mat-footer-cell:first-of-type:not(:only-of-type){padding-left:0;padding-right:24px}mat-cell:last-of-type,mat-header-cell:last-of-type,mat-footer-cell:last-of-type{padding-right:24px}[dir=rtl] mat-cell:last-of-type:not(:only-of-type),[dir=rtl] mat-header-cell:last-of-type:not(:only-of-type),[dir=rtl] mat-footer-cell:last-of-type:not(:only-of-type){padding-right:0;padding-left:24px}mat-cell,mat-header-cell,mat-footer-cell{flex:1;display:flex;align-items:center;overflow:hidden;word-wrap:break-word;min-height:inherit}.mat-mdc-table{min-width:100%;border:0;border-spacing:0;table-layout:auto;white-space:normal;background-color:var(--mat-table-background-color, var(--mat-sys-surface))}.mdc-data-table__cell{box-sizing:border-box;overflow:hidden;text-align:left;text-overflow:ellipsis}[dir=rtl] .mdc-data-table__cell{text-align:right}.mdc-data-table__cell,.mdc-data-table__header-cell{padding:0 16px}.mat-mdc-header-row{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;height:var(--mat-table-header-container-height, 56px);color:var(--mat-table-header-headline-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87)));font-family:var(--mat-table-header-headline-font, var(--mat-sys-title-small-font, Roboto, sans-serif));line-height:var(--mat-table-header-headline-line-height, var(--mat-sys-title-small-line-height));font-size:var(--mat-table-header-headline-size, var(--mat-sys-title-small-size, 14px));font-weight:var(--mat-table-header-headline-weight, var(--mat-sys-title-small-weight, 500))}.mat-mdc-row{height:var(--mat-table-row-item-container-height, 52px);color:var(--mat-table-row-item-label-text-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87)))}.mat-mdc-row,.mdc-data-table__content{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:var(--mat-table-row-item-label-text-font, var(--mat-sys-body-medium-font, Roboto, sans-serif));line-height:var(--mat-table-row-item-label-text-line-height, var(--mat-sys-body-medium-line-height));font-size:var(--mat-table-row-item-label-text-size, var(--mat-sys-body-medium-size, 14px));font-weight:var(--mat-table-row-item-label-text-weight, var(--mat-sys-body-medium-weight))}.mat-mdc-footer-row{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;height:var(--mat-table-footer-container-height, 52px);color:var(--mat-table-row-item-label-text-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87)));font-family:var(--mat-table-footer-supporting-text-font, var(--mat-sys-body-medium-font, Roboto, sans-serif));line-height:var(--mat-table-footer-supporting-text-line-height, var(--mat-sys-body-medium-line-height));font-size:var(--mat-table-footer-supporting-text-size, var(--mat-sys-body-medium-size, 14px));font-weight:var(--mat-table-footer-supporting-text-weight, var(--mat-sys-body-medium-weight));letter-spacing:var(--mat-table-footer-supporting-text-tracking, var(--mat-sys-body-medium-tracking))}.mat-mdc-header-cell{border-bottom-color:var(--mat-table-row-item-outline-color, var(--mat-sys-outline, rgba(0, 0, 0, 0.12)));border-bottom-width:var(--mat-table-row-item-outline-width, 1px);border-bottom-style:solid;letter-spacing:var(--mat-table-header-headline-tracking, var(--mat-sys-title-small-tracking));font-weight:inherit;line-height:inherit;box-sizing:border-box;text-overflow:ellipsis;overflow:hidden;outline:none;text-align:left}[dir=rtl] .mat-mdc-header-cell{text-align:right}.mdc-data-table__row:last-child>.mat-mdc-header-cell{border-bottom:none}.mat-mdc-cell{border-bottom-color:var(--mat-table-row-item-outline-color, var(--mat-sys-outline, rgba(0, 0, 0, 0.12)));border-bottom-width:var(--mat-table-row-item-outline-width, 1px);border-bottom-style:solid;letter-spacing:var(--mat-table-row-item-label-text-tracking, var(--mat-sys-body-medium-tracking));line-height:inherit}.mdc-data-table__row:last-child>.mat-mdc-cell{border-bottom:none}.mat-mdc-footer-cell{letter-spacing:var(--mat-table-row-item-label-text-tracking, var(--mat-sys-body-medium-tracking))}mat-row.mat-mdc-row,mat-header-row.mat-mdc-header-row,mat-footer-row.mat-mdc-footer-row{border-bottom:none}.mat-mdc-table tbody,.mat-mdc-table tfoot,.mat-mdc-table thead,.mat-mdc-cell,.mat-mdc-footer-cell,.mat-mdc-header-row,.mat-mdc-row,.mat-mdc-footer-row,.mat-mdc-table .mat-mdc-header-cell{background:inherit}.mat-mdc-table mat-header-row.mat-mdc-header-row,.mat-mdc-table mat-row.mat-mdc-row,.mat-mdc-table mat-footer-row.mat-mdc-footer-cell{height:unset}mat-header-cell.mat-mdc-header-cell,mat-cell.mat-mdc-cell,mat-footer-cell.mat-mdc-footer-cell{align-self:stretch} +`],encapsulation:2})}return t})(),va=(()=>{class t extends bw{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","matCellDef",""]],features:[we([{provide:bw,useExisting:t}]),le]})}return t})(),ya=(()=>{class t extends vw{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","matHeaderCellDef",""]],features:[we([{provide:vw,useExisting:t}]),le]})}return t})();var xa=(()=>{class t extends mp{get name(){return this._name}set name(e){this._setNameInput(e)}_updateColumnCssClassName(){super._updateColumnCssClassName(),this._columnCssClassName.push(`mat-column-${this.cssClassFriendlyName}`)}static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","matColumnDef",""]],inputs:{name:[0,"matColumnDef","name"]},features:[we([{provide:mp,useExisting:t},{provide:"MAT_SORT_HEADER_COLUMN_DEF",useExisting:t}]),le]})}return t})(),Ca=(()=>{class t extends M6{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["mat-header-cell"],["th","mat-header-cell",""]],hostAttrs:["role","columnheader",1,"mat-mdc-header-cell","mdc-data-table__header-cell"],features:[le]})}return t})();var wa=(()=>{class t extends E6{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["mat-cell"],["td","mat-cell",""]],hostAttrs:[1,"mat-mdc-cell","mdc-data-table__cell"],features:[le]})}return t})();var Da=(()=>{class t extends D_{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","matHeaderRowDef",""]],inputs:{columns:[0,"matHeaderRowDef","columns"],sticky:[2,"matHeaderRowDefSticky","sticky",B]},features:[we([{provide:D_,useExisting:t}]),le]})}return t})();var Ma=(()=>{class t extends yw{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275dir=P({type:t,selectors:[["","matRowDef",""]],inputs:{columns:[0,"matRowDefColumns","columns"],when:[0,"matRowDefWhen","when"]},features:[we([{provide:yw,useExisting:t}]),le]})}return t})(),Ea=(()=>{class t extends QI{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-header-row"],["tr","mat-header-row",""]],hostAttrs:["role","row",1,"mat-mdc-header-row","mdc-data-table__header-row"],exportAs:["matHeaderRow"],features:[we([{provide:QI,useExisting:t}]),le],decls:1,vars:0,consts:[["cdkCellOutlet",""]],template:function(i,r){i&1&&qe(0,0)},dependencies:[Nu],encapsulation:2})}return t})();var Sa=(()=>{class t extends KI{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-row"],["tr","mat-row",""]],hostAttrs:["role","row",1,"mat-mdc-row","mdc-data-table__row"],exportAs:["matRow"],features:[we([{provide:KI,useExisting:t}]),le],decls:1,vars:0,consts:[["cdkCellOutlet",""]],template:function(i,r){i&1&&qe(0,0)},dependencies:[Nu],encapsulation:2})}return t})(),ww=(()=>{class t extends xw{_cellSelector="td, mat-cell, [mat-cell], .mat-cell";constructor(){super(),this._contentClassNames.push("mat-mdc-no-data-row","mat-mdc-row","mdc-data-table__row"),this._cellClassNames.push("mat-mdc-cell","mdc-data-table__cell","mat-no-data-cell")}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["ng-template","matNoDataRow",""]],features:[we([{provide:xw,useExisting:t}]),le]})}return t})();var ka=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,S6,De]})}return t})(),G_e=9007199254740991,Cw=class extends sg{_data;_renderData=new rt([]);_filter=new rt("");_internalPageChanges=new z;_renderChangesSubscription=null;filteredData;get data(){return this._data.value}set data(n){n=Array.isArray(n)?n:[],this._data.next(n),this._renderChangesSubscription||this._filterData(n)}get filter(){return this._filter.value}set filter(n){this._filter.next(n),this._renderChangesSubscription||this._filterData(this.data)}get sort(){return this._sort}set sort(n){this._sort=n,this._updateChangeSubscription()}_sort;get paginator(){return this._paginator}set paginator(n){this._paginator=n,this._updateChangeSubscription()}_paginator;sortingDataAccessor=(n,e)=>{let i=n[e];if(Hv(i)){let r=Number(i);return r{let i=e.active,r=e.direction;return!i||r==""?n:n.sort((o,a)=>{let s=this.sortingDataAccessor(o,i),l=this.sortingDataAccessor(a,i),c=typeof s,d=typeof l;c!==d&&(c==="number"&&(s+=""),d==="number"&&(l+=""));let p=0;return s!=null&&l!=null?s>l?p=1:s{let i=e.trim().toLowerCase();return Object.values(n).some(r=>`${r}`.toLowerCase().includes(i))};constructor(n=[]){super(),this._data=new rt(n),this._updateChangeSubscription()}_updateChangeSubscription(){let n=this._sort?it(this._sort.sortChange,this._sort.initialized):Q(null),e=this._paginator?it(this._paginator.page,this._internalPageChanges,this._paginator.initialized):Q(null),i=this._data,r=yo([i,this._filter]).pipe(se(([s])=>this._filterData(s))),o=yo([r,n]).pipe(se(([s])=>this._orderData(s))),a=yo([o,e]).pipe(se(([s])=>this._pageData(s)));this._renderChangesSubscription?.unsubscribe(),this._renderChangesSubscription=a.subscribe(s=>this._renderData.next(s))}_filterData(n){return this.filteredData=this.filter==null||this.filter===""?n:n.filter(e=>this.filterPredicate(e,this.filter)),this.paginator&&this._updatePaginator(this.filteredData.length),this.filteredData}_orderData(n){return this.sort?this.sortData(n.slice(),this.sort):n}_pageData(n){if(!this.paginator)return n;let e=this.paginator.pageIndex*this.paginator.pageSize;return n.slice(e,e+this.paginator.pageSize)}_updatePaginator(n){Promise.resolve().then(()=>{let e=this.paginator;if(e&&(e.length=n,e.pageIndex>0)){let i=Math.ceil(e.length/e.pageSize)-1||0,r=Math.min(e.pageIndex,i);r!==e.pageIndex&&(e.pageIndex=r,this._internalPageChanges.next())}})}connect(){return this._renderChangesSubscription||this._updateChangeSubscription(),this._renderData}disconnect(){this._renderChangesSubscription?.unsubscribe(),this._renderChangesSubscription=null}};var q_e=["panel"],Y_e=["*"];function Q_e(t,n){if(t&1&&(gt(0,"div",1,0),ne(2),yt()),t&2){let e=n.id,i=x();at(i._classList),G("mat-mdc-autocomplete-visible",i.showPanel)("mat-mdc-autocomplete-hidden",!i.showPanel)("mat-autocomplete-panel-animations-enabled",!i._animationsDisabled)("mat-primary",i._color==="primary")("mat-accent",i._color==="accent")("mat-warn",i._color==="warn"),pi("id",i.id),X("aria-label",i.ariaLabel||null)("aria-labelledby",i._getPanelAriaLabelledby(e))}}var iA=class{source;option;constructor(n,e){this.source=n,this.option=e}},k6=new O("mat-autocomplete-default-options",{providedIn:"root",factory:K_e});function K_e(){return{autoActiveFirstOption:!1,autoSelectActiveOption:!1,hideSingleSelectionIndicator:!1,requireSelection:!1,hasBackdrop:!1}}var Dw=(()=>{class t{_changeDetectorRef=u(ye);_elementRef=u(Y);_defaults=u(k6);_animationsDisabled=Qe();_activeOptionChanges=ke.EMPTY;_keyManager;showPanel=!1;get isOpen(){return this._isOpen&&this.showPanel}_isOpen=!1;_latestOpeningTrigger;_setColor(e){this._color=e,this._changeDetectorRef.markForCheck()}_color;template;panel;options;optionGroups;ariaLabel;ariaLabelledby;displayWith=null;autoActiveFirstOption;autoSelectActiveOption;requireSelection;panelWidth;disableRipple;optionSelected=new U;opened=new U;closed=new U;optionActivated=new U;set classList(e){this._classList=e,this._elementRef.nativeElement.className=""}_classList;get hideSingleSelectionIndicator(){return this._hideSingleSelectionIndicator}set hideSingleSelectionIndicator(e){this._hideSingleSelectionIndicator=e,this._syncParentProperties()}_hideSingleSelectionIndicator;_syncParentProperties(){if(this.options)for(let e of this.options)e._changeDetectorRef.markForCheck()}id=u(et).getId("mat-autocomplete-");inertGroups;constructor(){let e=u(Ye);this.inertGroups=e?.SAFARI||!1,this.autoActiveFirstOption=!!this._defaults.autoActiveFirstOption,this.autoSelectActiveOption=!!this._defaults.autoSelectActiveOption,this.requireSelection=!!this._defaults.requireSelection,this._hideSingleSelectionIndicator=this._defaults.hideSingleSelectionIndicator??!1}ngAfterContentInit(){this._keyManager=new tu(this.options).withWrap().skipPredicate(this._skipPredicate),this._activeOptionChanges=this._keyManager.change.subscribe(e=>{this.isOpen&&this.optionActivated.emit({source:this,option:this.options.toArray()[e]||null})}),this._setVisibility()}ngOnDestroy(){this._keyManager?.destroy(),this._activeOptionChanges.unsubscribe()}_setScrollTop(e){this.panel&&(this.panel.nativeElement.scrollTop=e)}_getScrollTop(){return this.panel?this.panel.nativeElement.scrollTop:0}_setVisibility(){this.showPanel=!!this.options?.length,this._changeDetectorRef.markForCheck()}_emitSelectEvent(e){let i=new iA(this,e);this.optionSelected.emit(i)}_getPanelAriaLabelledby(e){if(this.ariaLabel)return null;let i=e?e+" ":"";return this.ariaLabelledby?i+this.ariaLabelledby:e}_skipPredicate(){return!1}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-autocomplete"]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,Sn,5),Ce(o,ou,5)),i&2){let a;j(a=H())&&(r.options=a),j(a=H())&&(r.optionGroups=a)}},viewQuery:function(i,r){if(i&1&&(ie(te,7),ie(q_e,5)),i&2){let o;j(o=H())&&(r.template=o.first),j(o=H())&&(r.panel=o.first)}},hostAttrs:[1,"mat-mdc-autocomplete"],inputs:{ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],displayWith:"displayWith",autoActiveFirstOption:[2,"autoActiveFirstOption","autoActiveFirstOption",B],autoSelectActiveOption:[2,"autoSelectActiveOption","autoSelectActiveOption",B],requireSelection:[2,"requireSelection","requireSelection",B],panelWidth:"panelWidth",disableRipple:[2,"disableRipple","disableRipple",B],classList:[0,"class","classList"],hideSingleSelectionIndicator:[2,"hideSingleSelectionIndicator","hideSingleSelectionIndicator",B]},outputs:{optionSelected:"optionSelected",opened:"opened",closed:"closed",optionActivated:"optionActivated"},exportAs:["matAutocomplete"],features:[we([{provide:ru,useExisting:t}])],ngContentSelectors:Y_e,decls:1,vars:0,consts:[["panel",""],["role","listbox",1,"mat-mdc-autocomplete-panel","mdc-menu-surface","mdc-menu-surface--open",3,"id"]],template:function(i,r){i&1&&(Ee(),Ba(0,Q_e,3,17,"ng-template"))},styles:[`div.mat-mdc-autocomplete-panel{width:100%;max-height:256px;visibility:hidden;transform-origin:center top;overflow:auto;padding:8px 0;box-sizing:border-box;position:relative;border-radius:var(--mat-autocomplete-container-shape, var(--mat-sys-corner-extra-small));box-shadow:var(--mat-autocomplete-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));background-color:var(--mat-autocomplete-background-color, var(--mat-sys-surface-container))}@media(forced-colors: active){div.mat-mdc-autocomplete-panel{outline:solid 1px}}.cdk-overlay-pane:not(.mat-mdc-autocomplete-panel-above) div.mat-mdc-autocomplete-panel{border-top-left-radius:0;border-top-right-radius:0}.mat-mdc-autocomplete-panel-above div.mat-mdc-autocomplete-panel{border-bottom-left-radius:0;border-bottom-right-radius:0;transform-origin:center bottom}div.mat-mdc-autocomplete-panel.mat-mdc-autocomplete-visible{visibility:visible}div.mat-mdc-autocomplete-panel.mat-mdc-autocomplete-hidden{visibility:hidden;pointer-events:none}@keyframes _mat-autocomplete-enter{from{opacity:0;transform:scaleY(0.8)}to{opacity:1;transform:none}}.mat-autocomplete-panel-animations-enabled{animation:_mat-autocomplete-enter 120ms cubic-bezier(0, 0, 0.2, 1)}mat-autocomplete{display:none} +`],encapsulation:2,changeDetection:0})}return t})();var Z_e={provide:dr,useExisting:li(()=>M_),multi:!0};var T6=new O("mat-autocomplete-scroll-strategy",{providedIn:"root",factory:()=>{let t=u(de);return()=>Tn(t)}});function X_e(t){let n=u(de);return()=>Tn(n)}var J_e={provide:T6,deps:[],useFactory:X_e},M_=(()=>{class t{_environmentInjector=u(ti);_element=u(Y);_injector=u(de);_viewContainerRef=u(st);_zone=u(ae);_changeDetectorRef=u(ye);_dir=u(Yt,{optional:!0});_formField=u(oa,{optional:!0,host:!0});_viewportRuler=u(sr);_scrollStrategy=u(T6);_renderer=u(ze);_animationsDisabled=Qe();_defaults=u(k6,{optional:!0});_overlayRef;_portal;_componentDestroyed=!1;_initialized=new z;_keydownSubscription;_outsideClickSubscription;_cleanupWindowBlur;_previousValue;_valueOnAttach;_valueOnLastKeydown;_positionStrategy;_manuallyFloatingLabel=!1;_closingActionsSubscription;_viewportSubscription=ke.EMPTY;_breakpointObserver=u(Ml);_handsetLandscapeSubscription=ke.EMPTY;_canOpenOnNextFocus=!0;_valueBeforeAutoSelection;_pendingAutoselectedOption;_closeKeyEventStream=new z;_overlayPanelClass=Dl(this._defaults?.overlayPanelClass||[]);_windowBlurHandler=()=>{this._canOpenOnNextFocus=this.panelOpen||!this._hasFocus()};_onChange=()=>{};_onTouched=()=>{};autocomplete;position="auto";connectedTo;autocompleteAttribute="off";autocompleteDisabled;constructor(){}_aboveClass="mat-mdc-autocomplete-panel-above";ngAfterViewInit(){this._initialized.next(),this._initialized.complete(),this._cleanupWindowBlur=this._renderer.listen("window","blur",this._windowBlurHandler)}ngOnChanges(e){e.position&&this._positionStrategy&&(this._setStrategyPositions(this._positionStrategy),this.panelOpen&&this._overlayRef.updatePosition())}ngOnDestroy(){this._cleanupWindowBlur?.(),this._handsetLandscapeSubscription.unsubscribe(),this._viewportSubscription.unsubscribe(),this._componentDestroyed=!0,this._destroyPanel(),this._closeKeyEventStream.complete(),this._clearFromModal()}get panelOpen(){return this._overlayAttached&&this.autocomplete.showPanel}_overlayAttached=!1;openPanel(){this._openPanelInternal()}closePanel(){this._resetLabel(),this._overlayAttached&&(this.panelOpen&&this._zone.run(()=>{this.autocomplete.closed.emit()}),this.autocomplete._latestOpeningTrigger===this&&(this.autocomplete._isOpen=!1,this.autocomplete._latestOpeningTrigger=null),this._overlayAttached=!1,this._pendingAutoselectedOption=null,this._overlayRef&&this._overlayRef.hasAttached()&&(this._overlayRef.detach(),this._closingActionsSubscription.unsubscribe()),this._updatePanelState(),this._componentDestroyed||this._changeDetectorRef.detectChanges(),this._trackedModal&&Mc(this._trackedModal,"aria-owns",this.autocomplete.id))}updatePosition(){this._overlayAttached&&this._overlayRef.updatePosition()}get panelClosingActions(){return it(this.optionSelections,this.autocomplete._keyManager.tabOut.pipe(ce(()=>this._overlayAttached)),this._closeKeyEventStream,this._getOutsideClickStream(),this._overlayRef?this._overlayRef.detachments().pipe(ce(()=>this._overlayAttached)):Q()).pipe(se(e=>e instanceof lh?e:null))}optionSelections=Fn(()=>{let e=this.autocomplete?this.autocomplete.options:null;return e?e.changes.pipe(Ue(e),je(()=>it(...e.map(i=>i.onSelectionChange)))):this._initialized.pipe(je(()=>this.optionSelections))});get activeOption(){return this.autocomplete&&this.autocomplete._keyManager?this.autocomplete._keyManager.activeItem:null}_getOutsideClickStream(){return new Ne(e=>{let i=o=>{let a=or(o),s=this._formField?this._formField.getConnectedOverlayOrigin().nativeElement:null,l=this.connectedTo?this.connectedTo.elementRef.nativeElement:null;this._overlayAttached&&a!==this._element.nativeElement&&!this._hasFocus()&&(!s||!s.contains(a))&&(!l||!l.contains(a))&&this._overlayRef&&!this._overlayRef.overlayElement.contains(a)&&e.next(o)},r=[this._renderer.listen("document","click",i),this._renderer.listen("document","auxclick",i),this._renderer.listen("document","touchend",i)];return()=>{r.forEach(o=>o())}})}writeValue(e){Promise.resolve(null).then(()=>this._assignOptionValue(e))}registerOnChange(e){this._onChange=e}registerOnTouched(e){this._onTouched=e}setDisabledState(e){this._element.nativeElement.disabled=e}_handleKeydown(e){let i=e,r=i.keyCode,o=Gt(i);if(r===27&&!o&&i.preventDefault(),this._valueOnLastKeydown=this._element.nativeElement.value,this.activeOption&&r===13&&this.panelOpen&&!o)this.activeOption._selectViaInteraction(),this._resetActiveItem(),i.preventDefault();else if(this.autocomplete){let a=this.autocomplete._keyManager.activeItem,s=r===38||r===40;r===9||s&&!o&&this.panelOpen?this.autocomplete._keyManager.onKeydown(i):s&&this._canOpen()&&this._openPanelInternal(this._valueOnLastKeydown),(s||this.autocomplete._keyManager.activeItem!==a)&&(this._scrollToOption(this.autocomplete._keyManager.activeItemIndex||0),this.autocomplete.autoSelectActiveOption&&this.activeOption&&(this._pendingAutoselectedOption||(this._valueBeforeAutoSelection=this._valueOnLastKeydown),this._pendingAutoselectedOption=this.activeOption,this._assignOptionValue(this.activeOption.value)))}}_handleInput(e){let i=e.target,r=i.value;if(i.type==="number"&&(r=r==""?null:parseFloat(r)),this._previousValue!==r){if(this._previousValue=r,this._pendingAutoselectedOption=null,(!this.autocomplete||!this.autocomplete.requireSelection)&&this._onChange(r),!r)this._clearPreviousSelectedOption(null,!1);else if(this.panelOpen&&!this.autocomplete.requireSelection){let o=this.autocomplete.options?.find(a=>a.selected);if(o){let a=this._getDisplayValue(o.value);r!==a&&o.deselect(!1)}}if(this._canOpen()&&this._hasFocus()){let o=this._valueOnLastKeydown??this._element.nativeElement.value;this._valueOnLastKeydown=null,this._openPanelInternal(o)}}}_handleFocus(){this._canOpenOnNextFocus?this._canOpen()&&(this._previousValue=this._element.nativeElement.value,this._attachOverlay(this._previousValue),this._floatLabel(!0)):this._canOpenOnNextFocus=!0}_handleClick(){this._canOpen()&&!this.panelOpen&&this._openPanelInternal()}_hasFocus(){return So()===this._element.nativeElement}_floatLabel(e=!1){this._formField&&this._formField.floatLabel==="auto"&&(e?this._formField._animateAndLockLabel():this._formField.floatLabel="always",this._manuallyFloatingLabel=!0)}_resetLabel(){this._manuallyFloatingLabel&&(this._formField&&(this._formField.floatLabel="auto"),this._manuallyFloatingLabel=!1)}_subscribeToClosingActions(){let e=new Ne(r=>{vt(()=>{r.next()},{injector:this._environmentInjector})}),i=this.autocomplete.options?.changes.pipe(He(()=>this._positionStrategy.reapplyLastPosition()),Oa(0))??Q();return it(e,i).pipe(je(()=>this._zone.run(()=>{let r=this.panelOpen;return this._resetActiveItem(),this._updatePanelState(),this._changeDetectorRef.detectChanges(),this.panelOpen&&this._overlayRef.updatePosition(),r!==this.panelOpen&&(this.panelOpen?this._emitOpened():this.autocomplete.closed.emit()),this.panelClosingActions})),mt(1)).subscribe(r=>this._setValueAndClose(r))}_emitOpened(){this.autocomplete.opened.emit()}_destroyPanel(){this._overlayRef&&(this.closePanel(),this._overlayRef.dispose(),this._overlayRef=null)}_getDisplayValue(e){let i=this.autocomplete;return i&&i.displayWith?i.displayWith(e):e}_assignOptionValue(e){let i=this._getDisplayValue(e);e==null&&this._clearPreviousSelectedOption(null,!1),this._updateNativeInputValue(i??"")}_updateNativeInputValue(e){this._formField?this._formField._control.value=e:this._element.nativeElement.value=e,this._previousValue=e}_setValueAndClose(e){let i=this.autocomplete,r=e?e.source:this._pendingAutoselectedOption;r?(this._clearPreviousSelectedOption(r),this._assignOptionValue(r.value),this._onChange(r.value),i._emitSelectEvent(r),this._element.nativeElement.focus()):i.requireSelection&&this._element.nativeElement.value!==this._valueOnAttach&&(this._clearPreviousSelectedOption(null),this._assignOptionValue(null),this._onChange(null)),this.closePanel()}_clearPreviousSelectedOption(e,i){this.autocomplete?.options?.forEach(r=>{r!==e&&r.selected&&r.deselect(i)})}_openPanelInternal(e=this._element.nativeElement.value){if(this._attachOverlay(e),this._floatLabel(),this._trackedModal){let i=this.autocomplete.id;ih(this._trackedModal,"aria-owns",i)}}_attachOverlay(e){this.autocomplete;let i=this._overlayRef;i?(this._positionStrategy.setOrigin(this._getConnectedElement()),i.updateSize({width:this._getPanelWidth()})):(this._portal=new kn(this.autocomplete.template,this._viewContainerRef,{id:this._formField?.getLabelId()}),i=qr(this._injector,this._getOverlayConfig()),this._overlayRef=i,this._viewportSubscription=this._viewportRuler.change().subscribe(()=>{this.panelOpen&&i&&i.updateSize({width:this._getPanelWidth()})}),this._handsetLandscapeSubscription=this._breakpointObserver.observe(jv.HandsetLandscape).subscribe(o=>{o.matches?this._positionStrategy.withFlexibleDimensions(!0).withGrowAfterOpen(!0).withViewportMargin(8):this._positionStrategy.withFlexibleDimensions(!1).withGrowAfterOpen(!1).withViewportMargin(0)})),i&&!i.hasAttached()&&(i.attach(this._portal),this._valueOnAttach=e,this._valueOnLastKeydown=null,this._closingActionsSubscription=this._subscribeToClosingActions());let r=this.panelOpen;this.autocomplete._isOpen=this._overlayAttached=!0,this.autocomplete._latestOpeningTrigger=this,this.autocomplete._setColor(this._formField?.color),this._updatePanelState(),this._applyModalPanelOwnership(),this.panelOpen&&r!==this.panelOpen&&this._emitOpened()}_handlePanelKeydown=e=>{(e.keyCode===27&&!Gt(e)||e.keyCode===38&&Gt(e,"altKey"))&&(this._pendingAutoselectedOption&&(this._updateNativeInputValue(this._valueBeforeAutoSelection??""),this._pendingAutoselectedOption=null),this._closeKeyEventStream.next(),this._resetActiveItem(),e.stopPropagation(),e.preventDefault())};_updatePanelState(){if(this.autocomplete._setVisibility(),this.panelOpen){let e=this._overlayRef;this._keydownSubscription||(this._keydownSubscription=e.keydownEvents().subscribe(this._handlePanelKeydown)),this._outsideClickSubscription||(this._outsideClickSubscription=e.outsidePointerEvents().subscribe())}else this._keydownSubscription?.unsubscribe(),this._outsideClickSubscription?.unsubscribe(),this._keydownSubscription=this._outsideClickSubscription=null}_getOverlayConfig(){return new Gr({positionStrategy:this._getOverlayPosition(),scrollStrategy:this._scrollStrategy(),width:this._getPanelWidth(),direction:this._dir??void 0,hasBackdrop:this._defaults?.hasBackdrop,backdropClass:this._defaults?.backdropClass||"cdk-overlay-transparent-backdrop",panelClass:this._overlayPanelClass,disableAnimations:this._animationsDisabled})}_getOverlayPosition(){let e=Ja(this._injector,this._getConnectedElement()).withFlexibleDimensions(!1).withPush(!1);return this._setStrategyPositions(e),this._positionStrategy=e,e}_setStrategyPositions(e){let i=[{originX:"start",originY:"bottom",overlayX:"start",overlayY:"top"},{originX:"end",originY:"bottom",overlayX:"end",overlayY:"top"}],r=this._aboveClass,o=[{originX:"start",originY:"top",overlayX:"start",overlayY:"bottom",panelClass:r},{originX:"end",originY:"top",overlayX:"end",overlayY:"bottom",panelClass:r}],a;this.position==="above"?a=o:this.position==="below"?a=i:a=[...i,...o],e.withPositions(a)}_getConnectedElement(){return this.connectedTo?this.connectedTo.elementRef:this._formField?this._formField.getConnectedOverlayOrigin():this._element}_getPanelWidth(){return this.autocomplete.panelWidth||this._getHostWidth()}_getHostWidth(){return this._getConnectedElement().nativeElement.getBoundingClientRect().width}_resetActiveItem(){let e=this.autocomplete;if(e.autoActiveFirstOption){let i=-1;for(let r=0;r .cdk-overlay-container [aria-modal="true"]');if(!e)return;let i=this.autocomplete.id;this._trackedModal&&Mc(this._trackedModal,"aria-owns",i),ih(e,"aria-owns",i),this._trackedModal=e}_clearFromModal(){if(this._trackedModal){let e=this.autocomplete.id;Mc(this._trackedModal,"aria-owns",e),this._trackedModal=null}}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["input","matAutocomplete",""],["textarea","matAutocomplete",""]],hostAttrs:[1,"mat-mdc-autocomplete-trigger"],hostVars:7,hostBindings:function(i,r){i&1&&S("focusin",function(){return r._handleFocus()})("blur",function(){return r._onTouched()})("input",function(a){return r._handleInput(a)})("keydown",function(a){return r._handleKeydown(a)})("click",function(){return r._handleClick()}),i&2&&X("autocomplete",r.autocompleteAttribute)("role",r.autocompleteDisabled?null:"combobox")("aria-autocomplete",r.autocompleteDisabled?null:"list")("aria-activedescendant",r.panelOpen&&r.activeOption?r.activeOption.id:null)("aria-expanded",r.autocompleteDisabled?null:r.panelOpen.toString())("aria-controls",r.autocompleteDisabled||!r.panelOpen||r.autocomplete==null?null:r.autocomplete.id)("aria-haspopup",r.autocompleteDisabled?null:"listbox")},inputs:{autocomplete:[0,"matAutocomplete","autocomplete"],position:[0,"matAutocompletePosition","position"],connectedTo:[0,"matAutocompleteConnectedTo","connectedTo"],autocompleteAttribute:[0,"autocomplete","autocompleteAttribute"],autocompleteDisabled:[2,"matAutocompleteDisabled","autocompleteDisabled",B]},exportAs:["matAutocompleteTrigger"],features:[we([Z_e]),Oe]})}return t})(),Mw=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[J_e],imports:[cr,ch,De,Tr,ch,De]})}return t})();var I6=()=>["HRAdmin","Manager"],ebe=()=>[5,10,25,50,100],tbe=()=>["HRAdmin"];function ibe(t,n){if(t&1){let e=q();m(0,"button",22),S("click",function(){k(e);let r=x();return T(r.createEmployee())}),m(1,"mat-icon"),f(2,"add"),h(),f(3," Add Employee "),h()}}function nbe(t,n){if(t&1&&(m(0,"mat-option",23),f(1),h()),t&2){let e=n.$implicit;v("value",e),g(),fe(" ",e," ")}}function rbe(t,n){if(t&1&&(m(0,"mat-option",23),f(1),h()),t&2){let e=n.$implicit;v("value",e),g(),fe(" ",e," ")}}function obe(t,n){if(t&1&&(m(0,"mat-option",23),f(1),h()),t&2){let e=n.$implicit;v("value",e),g(),fe(" ",e," ")}}function abe(t,n){if(t&1&&(m(0,"mat-option",23),f(1),h()),t&2){let e=n.$implicit;v("value",e),g(),fe(" ",e," ")}}function sbe(t,n){if(t&1&&(m(0,"mat-option",23),f(1),h()),t&2){let e=n.$implicit;v("value",e),g(),fe(" ",e," ")}}function lbe(t,n){t&1&&(m(0,"div",24),M(1,"mat-spinner"),h())}function cbe(t,n){t&1&&(m(0,"th",37),f(1,"Employee #"),h())}function dbe(t,n){if(t&1&&(m(0,"td",38),f(1),h()),t&2){let e=n.$implicit;g(),N(e.employeeNumber)}}function ube(t,n){t&1&&(m(0,"th",37),f(1,"Name"),h())}function mbe(t,n){if(t&1&&(m(0,"td",38),f(1),h()),t&2){let e=n.$implicit,i=x(2);g(),N(i.getFullName(e))}}function hbe(t,n){t&1&&(m(0,"th",37),f(1,"Email"),h())}function pbe(t,n){if(t&1&&(m(0,"td",38),f(1),h()),t&2){let e=n.$implicit;g(),N(e.email)}}function fbe(t,n){t&1&&(m(0,"th",37),f(1,"Phone"),h())}function gbe(t,n){if(t&1&&(m(0,"td",38),f(1),h()),t&2){let e=n.$implicit;g(),N(e.phoneNumber||e.phone||"-")}}function _be(t,n){t&1&&(m(0,"th",37),f(1,"Position"),h())}function bbe(t,n){if(t&1&&(m(0,"td",38),f(1),h()),t&2){let e=n.$implicit;g(),N(e.positionTitle||(e.position==null?null:e.position.positionTitle)||"-")}}function vbe(t,n){t&1&&(m(0,"th",37),f(1,"Actions"),h())}function ybe(t,n){if(t&1){let e=q();m(0,"button",43),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.editEmployee(r))}),m(1,"mat-icon"),f(2,"edit"),h()()}}function xbe(t,n){if(t&1){let e=q();m(0,"button",44),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.deleteEmployee(r))}),m(1,"mat-icon"),f(2,"delete"),h()()}}function Cbe(t,n){if(t&1){let e=q();m(0,"td",38)(1,"div",39)(2,"button",40),S("click",function(){let r=k(e).$implicit,o=x(2);return T(o.viewEmployee(r))}),m(3,"mat-icon"),f(4,"visibility"),h()(),A(5,ybe,3,0,"button",41)(6,xbe,3,0,"button",42),h()()}t&2&&(g(5),v("appHasRole",dt(2,I6)),g(),v("appHasRole",dt(3,tbe)))}function wbe(t,n){t&1&&M(0,"tr",45)}function Dbe(t,n){t&1&&M(0,"tr",46)}function Mbe(t,n){if(t&1&&(m(0,"tr",47)(1,"td",48)(2,"div",49)(3,"mat-icon"),f(4,"info"),h(),m(5,"p"),f(6,"No employees found"),h()()()()),t&2){let e=x(2);g(),X("colspan",e.displayedColumns.length)}}function Ebe(t,n){if(t&1&&(m(0,"table",25),lt(1,26),A(2,cbe,2,0,"th",27)(3,dbe,2,1,"td",28),ot(),lt(4,29),A(5,ube,2,0,"th",27)(6,mbe,2,1,"td",28),ot(),lt(7,30),A(8,hbe,2,0,"th",27)(9,pbe,2,1,"td",28),ot(),lt(10,31),A(11,fbe,2,0,"th",27)(12,gbe,2,1,"td",28),ot(),lt(13,32),A(14,_be,2,0,"th",27)(15,bbe,2,1,"td",28),ot(),lt(16,33),A(17,vbe,2,0,"th",27)(18,Cbe,7,4,"td",28),ot(),A(19,wbe,1,0,"tr",34)(20,Dbe,1,0,"tr",35)(21,Mbe,7,1,"tr",36),h()),t&2){let e=x();v("dataSource",e.employees),g(19),v("matHeaderRowDef",e.displayedColumns),g(),v("matRowDefColumns",e.displayedColumns),g(),v("ngIf",e.employees.length===0)}}var A6=(()=>{let n=class n{constructor(){this.employeeService=u(rd),this.authService=u(jt),this.router=u(Ae),this.fb=u(co),this.snackBar=u(_i),this.dialog=u(Rn),this.employees=[],this.loading=!1,this.totalCount=0,this.pageSize=10,this.pageNumber=1,this.destroy$=new z,this.displayedColumns=["employeeNumber","name","email","phone","positionTitle","actions"]}ngOnInit(){this.initSearchForm(),this.setupAutocomplete(),this.setupAutoSubmit(),this.loadEmployees()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}initSearchForm(){this.searchForm=this.fb.group({FirstName:[""],LastName:[""],Email:[""],EmployeeNumber:[""],PositionTitle:[""]})}setupAutocomplete(){this.filteredEmployeeNumbers$=this.searchForm.get("EmployeeNumber").valueChanges.pipe(Ue(""),Dt(300),Nn(),je(i=>this.getAutocompleteOptions("EmployeeNumber",i))),this.filteredFirstNames$=this.searchForm.get("FirstName").valueChanges.pipe(Ue(""),Dt(300),Nn(),je(i=>this.getAutocompleteOptions("FirstName",i))),this.filteredLastNames$=this.searchForm.get("LastName").valueChanges.pipe(Ue(""),Dt(300),Nn(),je(i=>this.getAutocompleteOptions("LastName",i))),this.filteredEmails$=this.searchForm.get("Email").valueChanges.pipe(Ue(""),Dt(300),Nn(),je(i=>this.getAutocompleteOptions("Email",i))),this.filteredPositionTitles$=this.searchForm.get("PositionTitle").valueChanges.pipe(Ue(""),Dt(300),Nn(),je(i=>this.getAutocompleteOptions("PositionTitle",i)))}setupAutoSubmit(){this.searchForm.valueChanges.pipe(Dt(500),Nn((i,r)=>JSON.stringify(i)===JSON.stringify(r)),xe(this.destroy$)).subscribe(()=>{this.pageNumber=1,this.loadEmployees()})}getAutocompleteOptions(i,r){if(!r||r.length<2)return Q([]);let o={PageNumber:1,PageSize:10,[i]:r};return this.employeeService.getAllPaged(o).pipe(se(a=>{let s={EmployeeNumber:c=>c.employeeNumber,FirstName:c=>c.firstName,LastName:c=>c.lastName,Email:c=>c.email,PositionTitle:c=>c.positionTitle||""};return a.value.map(c=>s[i](c)).filter((c,d,p)=>c&&p.indexOf(c)===d)}),ei(()=>Q([])))}loadEmployees(){this.loading=!0;let i=I({PageNumber:this.pageNumber,PageSize:this.pageSize},this.searchForm.value);Object.keys(i).forEach(r=>{(i[r]===""||i[r]===null||i[r]===void 0)&&delete i[r]}),this.employeeService.getAllPaged(i).subscribe({next:r=>{this.employees=r.value,this.totalCount=r.recordsTotal,this.loading=!1},error:r=>{console.error("Error loading employees:",r),this.loading=!1}})}onClearSearch(){this.searchForm.reset(),this.pageNumber=1,this.paginator&&(this.paginator.pageIndex=0),this.loadEmployees()}onPageChange(i){this.pageSize=i.pageSize,this.pageNumber=i.pageIndex+1,this.loadEmployees()}getFullName(i){return[i.prefix,i.firstName,i.middleName,i.lastName].filter(Boolean).join(" ")}viewEmployee(i){this.router.navigate(["/employees",i.id])}editEmployee(i){this.router.navigate(["/employees","edit",i.id])}deleteEmployee(i){let r=this.getFullName(i);this.dialog.open(Fr,{width:"400px",data:{title:"Delete Employee",message:`Are you sure you want to delete ${r}? This action cannot be undone.`,confirmText:"Delete",cancelText:"Cancel"}}).afterClosed().subscribe(a=>{a&&this.employeeService.delete(i.id).subscribe({next:()=>{this.snackBar.open(`${r} has been deleted.`,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"}),this.loadEmployees()},error:s=>{console.error("Error deleting employee:",s),this.snackBar.open("Failed to delete employee. Please try again.","Close",{duration:4e3,horizontalPosition:"end",verticalPosition:"top"})}})})}createEmployee(){this.router.navigate(["/employees","create"])}canEdit(){return this.authService.isHRAdmin()||this.authService.isManager()}canDelete(){return this.authService.isHRAdmin()}canCreate(){return this.authService.isHRAdmin()||this.authService.isManager()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-employee-list"]],viewQuery:function(r,o){if(r&1&&ie(mr,5),r&2){let a;j(a=H())&&(o.paginator=a.first)}},decls:59,vars:30,consts:[["autoEmployeeNumber","matAutocomplete"],["autoFirstName","matAutocomplete"],["autoLastName","matAutocomplete"],["autoEmail","matAutocomplete"],["autoPositionTitle","matAutocomplete"],[1,"flex-spacer"],["mat-raised-button","","color","primary",3,"click",4,"appHasRole"],[1,"search-form",3,"formGroup"],[1,"search-row"],["appearance","outline"],["matInput","","formControlName","EmployeeNumber",3,"matAutocomplete"],[3,"value",4,"ngFor","ngForOf"],["matInput","","formControlName","FirstName",3,"matAutocomplete"],["matInput","","formControlName","LastName",3,"matAutocomplete"],["matInput","","formControlName","Email",3,"matAutocomplete"],["matInput","","formControlName","PositionTitle",3,"matAutocomplete"],[1,"search-buttons"],["mat-raised-button","","color","accent","type","button",3,"click"],[1,"table-container"],["class","loading-spinner",4,"ngIf"],["mat-table","","class","employee-table",3,"dataSource",4,"ngIf"],["showFirstLastButtons","",3,"page","length","pageSize","pageSizeOptions","pageIndex"],["mat-raised-button","","color","primary",3,"click"],[3,"value"],[1,"loading-spinner"],["mat-table","",1,"employee-table",3,"dataSource"],["matColumnDef","employeeNumber"],["mat-header-cell","",4,"matHeaderCellDef"],["mat-cell","",4,"matCellDef"],["matColumnDef","name"],["matColumnDef","email"],["matColumnDef","phone"],["matColumnDef","positionTitle"],["matColumnDef","actions"],["mat-header-row","",4,"matHeaderRowDef"],["mat-row","",4,"matRowDef","matRowDefColumns"],["class","mat-row",4,"ngIf"],["mat-header-cell",""],["mat-cell",""],[1,"action-buttons"],["mat-icon-button","","color","primary","matTooltip","View Details",3,"click"],["mat-icon-button","","color","accent","matTooltip","Edit Employee",3,"click",4,"appHasRole"],["mat-icon-button","","color","warn","matTooltip","Delete Employee",3,"click",4,"appHasRole"],["mat-icon-button","","color","accent","matTooltip","Edit Employee",3,"click"],["mat-icon-button","","color","warn","matTooltip","Delete Employee",3,"click"],["mat-header-row",""],["mat-row",""],[1,"mat-row"],[1,"mat-cell"],[1,"no-data"]],template:function(r,o){if(r&1){let a=q();M(0,"page-header"),m(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4,"Employee Directory"),h(),M(5,"div",5),A(6,ibe,4,0,"button",6),h(),m(7,"mat-card-content")(8,"form",7)(9,"div",8)(10,"mat-form-field",9)(11,"mat-label"),f(12,"Employee Number"),h(),M(13,"input",10),m(14,"mat-autocomplete",null,0),A(16,nbe,2,2,"mat-option",11),me(17,"async"),h()(),m(18,"mat-form-field",9)(19,"mat-label"),f(20,"First Name"),h(),M(21,"input",12),m(22,"mat-autocomplete",null,1),A(24,rbe,2,2,"mat-option",11),me(25,"async"),h()(),m(26,"mat-form-field",9)(27,"mat-label"),f(28,"Last Name"),h(),M(29,"input",13),m(30,"mat-autocomplete",null,2),A(32,obe,2,2,"mat-option",11),me(33,"async"),h()(),m(34,"mat-form-field",9)(35,"mat-label"),f(36,"Email"),h(),M(37,"input",14),m(38,"mat-autocomplete",null,3),A(40,abe,2,2,"mat-option",11),me(41,"async"),h()(),m(42,"mat-form-field",9)(43,"mat-label"),f(44,"Position Title"),h(),M(45,"input",15),m(46,"mat-autocomplete",null,4),A(48,sbe,2,2,"mat-option",11),me(49,"async"),h()(),m(50,"div",16)(51,"button",17),S("click",function(){return k(a),T(o.onClearSearch())}),m(52,"mat-icon"),f(53,"clear"),h(),f(54," Clear Filters "),h()()()(),m(55,"div",18),A(56,lbe,2,0,"div",19)(57,Ebe,22,4,"table",20),h(),m(58,"mat-paginator",21),S("page",function(l){return k(a),T(o.onPageChange(l))}),h()()()}if(r&2){let a=Te(15),s=Te(23),l=Te(31),c=Te(39),d=Te(47);g(6),v("appHasRole",dt(28,I6)),g(2),v("formGroup",o.searchForm),g(5),v("matAutocomplete",a),g(3),v("ngForOf",Re(17,18,o.filteredEmployeeNumbers$)),g(5),v("matAutocomplete",s),g(3),v("ngForOf",Re(25,20,o.filteredFirstNames$)),g(5),v("matAutocomplete",l),g(3),v("ngForOf",Re(33,22,o.filteredLastNames$)),g(5),v("matAutocomplete",c),g(3),v("ngForOf",Re(41,24,o.filteredEmails$)),g(5),v("matAutocomplete",d),g(3),v("ngForOf",Re(49,26,o.filteredPositionTitles$)),g(8),v("ngIf",o.loading),g(),v("ngIf",!o.loading),g(),v("length",o.totalCount)("pageSize",o.pageSize)("pageSizeOptions",dt(29,ebe))("pageIndex",o.pageNumber-1)}},dependencies:[Je,Un,Wt,Zn,lo,di,Pt,so,nn,Yr,Fe,_t,Ft,It,kt,Tt,Rt,Ot,Ge,Ze,Bi,Ci,Xt,gi,ai,ka,ba,ya,Da,xa,va,Ma,Ca,wa,Ea,Sa,Fc,mr,Zt,Kt,An,ur,Mw,Dw,Sn,M_,wi,Pn,Lt,Xn,cn],styles:["mat-card[_ngcontent-%COMP%]{margin:16px}.nl-search-card[_ngcontent-%COMP%]{margin-bottom:0}.nl-search-card[_ngcontent-%COMP%] .nl-search-field[_ngcontent-%COMP%]{width:100%}.nl-search-card[_ngcontent-%COMP%] .nl-parsed-hint[_ngcontent-%COMP%]{display:flex;align-items:center;gap:8px;font-size:13px;color:#0009;margin-top:4px}.nl-search-card[_ngcontent-%COMP%] .nl-parsed-hint[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:16px;height:16px;width:16px;color:#1976d2}.nl-search-card[_ngcontent-%COMP%] .nl-error[_ngcontent-%COMP%]{display:flex;align-items:center;gap:8px;font-size:13px;color:#b00020;margin-top:4px}.nl-search-card[_ngcontent-%COMP%] .nl-error[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:16px;height:16px;width:16px}mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:center;margin-bottom:16px;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-header[_ngcontent-%COMP%] .flex-spacer[_ngcontent-%COMP%]{flex:1 1 auto}mat-card-header[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}mat-card-content[_ngcontent-%COMP%]{padding:0}.search-form[_ngcontent-%COMP%]{padding:16px;background-color:#f5f5f5;margin-bottom:0}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;align-items:start}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%] mat-form-field[_ngcontent-%COMP%]{width:100%}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%] .search-buttons[_ngcontent-%COMP%]{display:flex;gap:8px;align-items:center}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%] .search-buttons[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{white-space:nowrap}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%] .search-buttons[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}.table-container[_ngcontent-%COMP%]{position:relative;min-height:300px;overflow-x:auto}.loading-spinner[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:300px}.employee-table[_ngcontent-%COMP%]{width:100%}.employee-table[_ngcontent-%COMP%] th[_ngcontent-%COMP%]{font-weight:600;font-size:14px;color:#000000de}.employee-table[_ngcontent-%COMP%] td[_ngcontent-%COMP%]{font-size:14px;color:#000000de}.employee-table[_ngcontent-%COMP%] .mat-column-employeeNumber[_ngcontent-%COMP%]{width:120px}.employee-table[_ngcontent-%COMP%] .mat-column-name[_ngcontent-%COMP%], .employee-table[_ngcontent-%COMP%] .mat-column-email[_ngcontent-%COMP%]{min-width:200px}.employee-table[_ngcontent-%COMP%] .mat-column-phone[_ngcontent-%COMP%]{width:150px}.employee-table[_ngcontent-%COMP%] .mat-column-positionTitle[_ngcontent-%COMP%]{min-width:200px}.employee-table[_ngcontent-%COMP%] .mat-column-actions[_ngcontent-%COMP%]{width:120px;text-align:right}.employee-table[_ngcontent-%COMP%] .mat-column-actions[_ngcontent-%COMP%] .action-buttons[_ngcontent-%COMP%]{display:flex;justify-content:flex-end;align-items:center;gap:4px}.no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;color:#0000008a}.no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:16px}.no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:16px}mat-paginator[_ngcontent-%COMP%]{border-top:1px solid rgba(0,0,0,.12)}"]});let t=n;return t})();var Lu=(function(t){return t[t.Male=0]="Male",t[t.Female=1]="Female",t})(Lu||{});var Sbe=()=>["HRAdmin","Manager"],kbe=()=>["HRAdmin"];function Tbe(t,n){t&1&&(m(0,"div",2),M(1,"mat-spinner"),h())}function Ibe(t,n){if(t&1){let e=q();m(0,"button",14),S("click",function(){k(e);let r=x(2);return T(r.editEmployee())}),m(1,"mat-icon"),f(2,"edit"),h(),f(3," Edit "),h()}}function Abe(t,n){if(t&1){let e=q();m(0,"button",15),S("click",function(){k(e);let r=x(2);return T(r.deleteEmployee())}),m(1,"mat-icon"),f(2,"delete"),h(),f(3," Delete "),h()}}function Obe(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",10),f(2,"Address"),h(),m(3,"span",11),f(4),h()()),t&2){let e=x(2);g(4),N(e.employee.address)}}function Rbe(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",10),f(2,"Hire Date"),h(),m(3,"span",11),f(4),me(5,"date"),h()()),t&2){let e=x(2);g(4),N(Re(5,1,e.employee.hireDate))}}function Pbe(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",10),f(2,"Salary Range"),h(),m(3,"span",11),f(4),me(5,"number"),me(6,"number"),h()()),t&2){let e=x(2);g(4),Sm(" ",e.employee.salaryRange.name," ($",Re(5,3,e.employee.salaryRange.minSalary)," - $",Re(6,5,e.employee.salaryRange.maxSalary),") ")}}function Fbe(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",10),f(2,"Created"),h(),m(3,"span",11),f(4),me(5,"date"),h()()),t&2){let e=x(3);g(4),N(Ui(5,1,e.employee.createdAt,"medium"))}}function Nbe(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",10),f(2,"Last Modified"),h(),m(3,"span",11),f(4),me(5,"date"),h()()),t&2){let e=x(3);g(4),N(Ui(5,1,e.employee.lastModifiedAt,"medium"))}}function Lbe(t,n){if(t&1&&(m(0,"div",9)(1,"h3"),f(2,"Audit Information"),h(),M(3,"mat-divider"),m(4,"mat-list"),A(5,Fbe,6,4,"mat-list-item",1)(6,Nbe,6,4,"mat-list-item",1),h()()),t&2){let e=x(2);g(5),v("ngIf",e.employee.createdAt),g(),v("ngIf",e.employee.lastModifiedAt)}}function Vbe(t,n){if(t&1){let e=q();m(0,"mat-card")(1,"mat-card-header")(2,"mat-card-title"),f(3),h(),M(4,"div",3),m(5,"div",4)(6,"button",5),S("click",function(){k(e);let r=x();return T(r.goBack())}),m(7,"mat-icon"),f(8,"arrow_back"),h(),f(9," Back to List "),h(),A(10,Ibe,4,0,"button",6)(11,Abe,4,0,"button",7),h()(),m(12,"mat-card-content")(13,"div",8)(14,"div",9)(15,"h3"),f(16,"Personal Information"),h(),M(17,"mat-divider"),m(18,"mat-list")(19,"mat-list-item")(20,"span",10),f(21,"Employee Number"),h(),m(22,"span",11),f(23),h()(),m(24,"mat-list-item")(25,"span",10),f(26,"Full Name"),h(),m(27,"span",11),f(28),h()(),m(29,"mat-list-item")(30,"span",10),f(31,"Date of Birth"),h(),m(32,"span",11),f(33),me(34,"date"),h()(),m(35,"mat-list-item")(36,"span",10),f(37,"Gender"),h(),m(38,"span",11),f(39),h()()()(),m(40,"div",9)(41,"h3"),f(42,"Contact Information"),h(),M(43,"mat-divider"),m(44,"mat-list")(45,"mat-list-item")(46,"span",10),f(47,"Email"),h(),m(48,"span",11)(49,"a",12),f(50),h()()(),m(51,"mat-list-item")(52,"span",10),f(53,"Phone Number"),h(),m(54,"span",11)(55,"a",12),f(56),h()()(),A(57,Obe,5,1,"mat-list-item",1),h()(),m(58,"div",9)(59,"h3"),f(60,"Employment Information"),h(),M(61,"mat-divider"),m(62,"mat-list"),A(63,Rbe,6,3,"mat-list-item",1),m(64,"mat-list-item")(65,"span",10),f(66,"Salary"),h(),m(67,"span",11),f(68),me(69,"currency"),h()(),m(70,"mat-list-item")(71,"span",10),f(72,"Department"),h(),m(73,"span",11),f(74),h()(),m(75,"mat-list-item")(76,"span",10),f(77,"Position"),h(),m(78,"span",11),f(79),h()(),A(80,Pbe,7,7,"mat-list-item",1),h()(),A(81,Lbe,7,2,"div",13),h()()()}if(t&2){let e=x();g(3),N(e.getFullName()),g(7),v("appHasRole",dt(22,Sbe)),g(),v("appHasRole",dt(23,kbe)),g(12),N(e.employee.employeeNumber),g(5),N(e.getFullName()),g(5),N(Re(34,18,e.employee.dateOfBirth||e.employee.birthday)),g(6),N(e.getGenderLabel(e.employee.gender)),g(10),v("href","mailto:"+e.employee.email,to),g(),N(e.employee.email),g(5),v("href","tel:"+(e.employee.phoneNumber||e.employee.phone),to),g(),N(e.employee.phoneNumber||e.employee.phone),g(),v("ngIf",e.employee.address),g(6),v("ngIf",e.employee.hireDate),g(5),N(Re(69,20,e.employee.salary)),g(6),N(e.employee.departmentName||(e.employee.department==null?null:e.employee.department.name)||"N/A"),g(5),N(e.employee.positionTitle||(e.employee.position==null?null:e.employee.position.positionTitle)||"N/A"),g(),v("ngIf",e.employee.salaryRange),g(),v("ngIf",e.employee.createdAt||e.employee.lastModifiedAt)}}function Bbe(t,n){if(t&1){let e=q();m(0,"mat-card")(1,"mat-card-content")(2,"div",16)(3,"mat-icon"),f(4,"error"),h(),m(5,"p"),f(6,"Employee not found"),h(),m(7,"button",17),S("click",function(){k(e);let r=x();return T(r.goBack())}),f(8," Go Back "),h()()()()}}var O6=(()=>{let n=class n{constructor(){this.employeeService=u(rd),this.authService=u(jt),this.router=u(Ae),this.route=u(Ai),this.snackBar=u(_i),this.dialog=u(Rn),this.loading=!1}ngOnInit(){let i=this.route.snapshot.paramMap.get("id");i&&this.loadEmployee(i)}loadEmployee(i){this.loading=!0,this.employeeService.getById(i).subscribe({next:r=>{this.employee=r,this.loading=!1},error:r=>{console.error("Error loading employee:",r),this.loading=!1,this.router.navigate(["/employees"])}})}getFullName(){return this.employee?[this.employee.prefix,this.employee.firstName,this.employee.middleName,this.employee.lastName,this.employee.suffix].filter(Boolean).join(" "):""}getGenderLabel(i){return i===Lu.Male?"Male":"Female"}editEmployee(){this.employee&&this.router.navigate(["/employees","edit",this.employee.id])}deleteEmployee(){if(!this.employee)return;let i=this.getFullName();this.dialog.open(Fr,{width:"400px",data:{title:"Delete Employee",message:`Are you sure you want to delete ${i}? This action cannot be undone.`,confirmText:"Delete",cancelText:"Cancel"}}).afterClosed().subscribe(o=>{o&&this.employeeService.delete(this.employee.id).subscribe({next:()=>{let a=this.snackBar.open(`${i} has been deleted.`,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"});a.afterDismissed().subscribe(()=>this.router.navigate(["/employees"])),a.onAction().subscribe(()=>this.router.navigate(["/employees"]))},error:a=>{console.error("Error deleting employee:",a),this.snackBar.open("Failed to delete employee. Please try again.","Close",{duration:4e3,horizontalPosition:"end",verticalPosition:"top"})}})})}goBack(){this.router.navigate(["/employees"])}canEdit(){return this.authService.isHRAdmin()||this.authService.isManager()}canDelete(){return this.authService.isHRAdmin()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-employee-detail"]],decls:4,vars:3,consts:[["class","loading-spinner",4,"ngIf"],[4,"ngIf"],[1,"loading-spinner"],[1,"flex-spacer"],[1,"header-actions"],["mat-stroked-button","",3,"click"],["mat-raised-button","","color","accent",3,"click",4,"appHasRole"],["mat-raised-button","","color","warn",3,"click",4,"appHasRole"],[1,"detail-container"],[1,"detail-section"],["matListItemTitle",""],["matListItemLine",""],[3,"href"],["class","detail-section",4,"ngIf"],["mat-raised-button","","color","accent",3,"click"],["mat-raised-button","","color","warn",3,"click"],[1,"no-data"],["mat-raised-button","","color","primary",3,"click"]],template:function(r,o){r&1&&(M(0,"page-header"),A(1,Tbe,2,0,"div",0)(2,Vbe,82,24,"mat-card",1)(3,Bbe,9,0,"mat-card",1)),r&2&&(g(),v("ngIf",o.loading),g(),v("ngIf",!o.loading&&o.employee),g(),v("ngIf",!o.loading&&!o.employee))},dependencies:[Je,Wt,Fe,_t,It,kt,Tt,Rt,Ot,Ge,Ze,Zt,Kt,Nr,Kr,fa,Js,pa,Fu,Hl,wi,Pn,Lt,Xn,xf,yl,Wa],styles:[".loading-spinner[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:400px}mat-card[_ngcontent-%COMP%]{margin:16px;max-width:1200px}mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:center;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:24px;font-weight:500;margin:0}mat-card-header[_ngcontent-%COMP%] .flex-spacer[_ngcontent-%COMP%]{flex:1 1 auto}mat-card-header[_ngcontent-%COMP%] .header-actions[_ngcontent-%COMP%]{display:flex;gap:12px}mat-card-header[_ngcontent-%COMP%] .header-actions[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}mat-card-content[_ngcontent-%COMP%]{padding:24px}.detail-container[_ngcontent-%COMP%]{display:grid;grid-template-columns:1fr 1fr;gap:24px}@media (max-width: 768px){.detail-container[_ngcontent-%COMP%]{grid-template-columns:1fr}}.detail-section[_ngcontent-%COMP%] h3[_ngcontent-%COMP%]{font-size:16px;font-weight:500;margin:0 0 8px;color:#000000de}.detail-section[_ngcontent-%COMP%] mat-divider[_ngcontent-%COMP%]{margin-bottom:16px}.detail-section[_ngcontent-%COMP%] mat-list[_ngcontent-%COMP%]{padding:0}.detail-section[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%]{height:auto;min-height:48px;padding:8px 0}.detail-section[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%] span[matListItemTitle][_ngcontent-%COMP%]{font-weight:500;color:#0009;font-size:12px;text-transform:uppercase;letter-spacing:.5px}.detail-section[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%] span[matListItemLine][_ngcontent-%COMP%]{font-size:14px;color:#000000de;margin-top:4px;white-space:normal;word-break:break-word}.detail-section[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%] span[matListItemLine][_ngcontent-%COMP%] a[_ngcontent-%COMP%]{color:#1976d2;text-decoration:none}.detail-section[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%] span[matListItemLine][_ngcontent-%COMP%] a[_ngcontent-%COMP%]:hover{text-decoration:underline}.no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;color:#0000008a}.no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:64px;width:64px;height:64px;margin-bottom:16px;color:#f44336}.no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0 0 24px;font-size:18px}"]});let t=n;return t})();var Hbe=["mat-calendar-body",""];function zbe(t,n){return this._trackRow(n)}var B6=(t,n)=>n.id;function Ube(t,n){if(t&1&&(m(0,"tr",0)(1,"td",3),f(2),h()()),t&2){let e=x();g(),At("padding-top",e._cellPadding)("padding-bottom",e._cellPadding),X("colspan",e.numCols),g(),fe(" ",e.label," ")}}function $be(t,n){if(t&1&&(m(0,"td",3),f(1),h()),t&2){let e=x(2);At("padding-top",e._cellPadding)("padding-bottom",e._cellPadding),X("colspan",e._firstRowOffset),g(),fe(" ",e._firstRowOffset>=e.labelMinRequiredCells?e.label:""," ")}}function Wbe(t,n){if(t&1){let e=q();m(0,"td",6)(1,"button",7),S("click",function(r){let o=k(e).$implicit,a=x(2);return T(a._cellClicked(o,r))})("focus",function(r){let o=k(e).$implicit,a=x(2);return T(a._emitActiveDateChange(o,r))}),m(2,"span",8),f(3),h(),M(4,"span",9),h()()}if(t&2){let e=n.$implicit,i=n.$index,r=x().$index,o=x();At("width",o._cellWidth)("padding-top",o._cellPadding)("padding-bottom",o._cellPadding),X("data-mat-row",r)("data-mat-col",i),g(),G("mat-calendar-body-disabled",!e.enabled)("mat-calendar-body-active",o._isActiveCell(r,i))("mat-calendar-body-range-start",o._isRangeStart(e.compareValue))("mat-calendar-body-range-end",o._isRangeEnd(e.compareValue))("mat-calendar-body-in-range",o._isInRange(e.compareValue))("mat-calendar-body-comparison-bridge-start",o._isComparisonBridgeStart(e.compareValue,r,i))("mat-calendar-body-comparison-bridge-end",o._isComparisonBridgeEnd(e.compareValue,r,i))("mat-calendar-body-comparison-start",o._isComparisonStart(e.compareValue))("mat-calendar-body-comparison-end",o._isComparisonEnd(e.compareValue))("mat-calendar-body-in-comparison-range",o._isInComparisonRange(e.compareValue))("mat-calendar-body-preview-start",o._isPreviewStart(e.compareValue))("mat-calendar-body-preview-end",o._isPreviewEnd(e.compareValue))("mat-calendar-body-in-preview",o._isInPreview(e.compareValue)),v("ngClass",e.cssClasses)("tabindex",o._isActiveCell(r,i)?0:-1),X("aria-label",e.ariaLabel)("aria-disabled",!e.enabled||null)("aria-pressed",o._isSelected(e.compareValue))("aria-current",o.todayValue===e.compareValue?"date":null)("aria-describedby",o._getDescribedby(e.compareValue)),g(),G("mat-calendar-body-selected",o._isSelected(e.compareValue))("mat-calendar-body-comparison-identical",o._isComparisonIdentical(e.compareValue))("mat-calendar-body-today",o.todayValue===e.compareValue),g(),fe(" ",e.displayValue," ")}}function Gbe(t,n){if(t&1&&(m(0,"tr",1),L(1,$be,2,6,"td",4),Mt(2,Wbe,5,48,"td",5,B6),h()),t&2){let e=n.$implicit,i=n.$index,r=x();g(),V(i===0&&r._firstRowOffset?1:-1),g(),Et(e)}}function qbe(t,n){if(t&1&&(m(0,"th",2)(1,"span",6),f(2),h(),m(3,"span",3),f(4),h()()),t&2){let e=n.$implicit;g(2),N(e.long),g(2),N(e.narrow)}}var Ybe=["*"];function Qbe(t,n){}function Kbe(t,n){if(t&1){let e=q();m(0,"mat-month-view",4),fn("activeDateChange",function(r){k(e);let o=x();return Mn(o.activeDate,r)||(o.activeDate=r),T(r)}),S("_userSelection",function(r){k(e);let o=x();return T(o._dateSelected(r))})("dragStarted",function(r){k(e);let o=x();return T(o._dragStarted(r))})("dragEnded",function(r){k(e);let o=x();return T(o._dragEnded(r))}),h()}if(t&2){let e=x();pn("activeDate",e.activeDate),v("selected",e.selected)("dateFilter",e.dateFilter)("maxDate",e.maxDate)("minDate",e.minDate)("dateClass",e.dateClass)("comparisonStart",e.comparisonStart)("comparisonEnd",e.comparisonEnd)("startDateAccessibleName",e.startDateAccessibleName)("endDateAccessibleName",e.endDateAccessibleName)("activeDrag",e._activeDrag)}}function Zbe(t,n){if(t&1){let e=q();m(0,"mat-year-view",5),fn("activeDateChange",function(r){k(e);let o=x();return Mn(o.activeDate,r)||(o.activeDate=r),T(r)}),S("monthSelected",function(r){k(e);let o=x();return T(o._monthSelectedInYearView(r))})("selectedChange",function(r){k(e);let o=x();return T(o._goToDateInView(r,"month"))}),h()}if(t&2){let e=x();pn("activeDate",e.activeDate),v("selected",e.selected)("dateFilter",e.dateFilter)("maxDate",e.maxDate)("minDate",e.minDate)("dateClass",e.dateClass)}}function Xbe(t,n){if(t&1){let e=q();m(0,"mat-multi-year-view",6),fn("activeDateChange",function(r){k(e);let o=x();return Mn(o.activeDate,r)||(o.activeDate=r),T(r)}),S("yearSelected",function(r){k(e);let o=x();return T(o._yearSelectedInMultiYearView(r))})("selectedChange",function(r){k(e);let o=x();return T(o._goToDateInView(r,"year"))}),h()}if(t&2){let e=x();pn("activeDate",e.activeDate),v("selected",e.selected)("dateFilter",e.dateFilter)("maxDate",e.maxDate)("minDate",e.minDate)("dateClass",e.dateClass)}}function Jbe(t,n){}var e0e=["button"],t0e=[[["","matDatepickerToggleIcon",""]]],i0e=["[matDatepickerToggleIcon]"];function n0e(t,n){t&1&&(ii(),m(0,"svg",2),M(1,"path",3),h())}var gp=(()=>{class t{changes=new z;calendarLabel="Calendar";openCalendarLabel="Open calendar";closeCalendarLabel="Close calendar";prevMonthLabel="Previous month";nextMonthLabel="Next month";prevYearLabel="Previous year";nextYearLabel="Next year";prevMultiYearLabel="Previous 24 years";nextMultiYearLabel="Next 24 years";switchToMonthViewLabel="Choose date";switchToMultiYearViewLabel="Choose month and year";startDateLabel="Start date";endDateLabel="End date";comparisonDateLabel="Comparison range";formatYearRange(e,i){return`${e} \u2013 ${i}`}formatYearRangeLabel(e,i){return`${e} to ${i}`}static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),r0e=0,S_=class{value;displayValue;ariaLabel;enabled;cssClasses;compareValue;rawValue;id=r0e++;constructor(n,e,i,r,o={},a=n,s){this.value=n,this.displayValue=e,this.ariaLabel=i,this.enabled=r,this.cssClasses=o,this.compareValue=a,this.rawValue=s}},o0e={passive:!1,capture:!0},Sw={passive:!0,capture:!0},R6={passive:!0},fp=(()=>{class t{_elementRef=u(Y);_ngZone=u(ae);_platform=u(Ye);_intl=u(gp);_eventCleanups;_skipNextFocus;_focusActiveCellAfterViewChecked=!1;label;rows;todayValue;startValue;endValue;labelMinRequiredCells;numCols=7;activeCell=0;ngAfterViewChecked(){this._focusActiveCellAfterViewChecked&&(this._focusActiveCell(),this._focusActiveCellAfterViewChecked=!1)}isRange=!1;cellAspectRatio=1;comparisonStart;comparisonEnd;previewStart=null;previewEnd=null;startDateAccessibleName;endDateAccessibleName;selectedValueChange=new U;previewChange=new U;activeDateChange=new U;dragStarted=new U;dragEnded=new U;_firstRowOffset;_cellPadding;_cellWidth;_startDateLabelId;_endDateLabelId;_comparisonStartDateLabelId;_comparisonEndDateLabelId;_didDragSinceMouseDown=!1;_injector=u(de);comparisonDateAccessibleName=this._intl.comparisonDateLabel;_trackRow=e=>e;constructor(){let e=u(ze),i=u(et);this._startDateLabelId=i.getId("mat-calendar-body-start-"),this._endDateLabelId=i.getId("mat-calendar-body-end-"),this._comparisonStartDateLabelId=i.getId("mat-calendar-body-comparison-start-"),this._comparisonEndDateLabelId=i.getId("mat-calendar-body-comparison-end-"),u(ft).load(Oi),this._ngZone.runOutsideAngular(()=>{let r=this._elementRef.nativeElement,o=[e.listen(r,"touchmove",this._touchmoveHandler,o0e),e.listen(r,"mouseenter",this._enterHandler,Sw),e.listen(r,"focus",this._enterHandler,Sw),e.listen(r,"mouseleave",this._leaveHandler,Sw),e.listen(r,"blur",this._leaveHandler,Sw),e.listen(r,"mousedown",this._mousedownHandler,R6),e.listen(r,"touchstart",this._mousedownHandler,R6)];this._platform.isBrowser&&o.push(e.listen("window","mouseup",this._mouseupHandler),e.listen("window","touchend",this._touchendHandler)),this._eventCleanups=o})}_cellClicked(e,i){this._didDragSinceMouseDown||e.enabled&&this.selectedValueChange.emit({value:e.value,event:i})}_emitActiveDateChange(e,i){e.enabled&&this.activeDateChange.emit({value:e.value,event:i})}_isSelected(e){return this.startValue===e||this.endValue===e}ngOnChanges(e){let i=e.numCols,{rows:r,numCols:o}=this;(e.rows||i)&&(this._firstRowOffset=r&&r.length&&r[0].length?o-r[0].length:0),(e.cellAspectRatio||i||!this._cellPadding)&&(this._cellPadding=`${50*this.cellAspectRatio/o}%`),(i||!this._cellWidth)&&(this._cellWidth=`${100/o}%`)}ngOnDestroy(){this._eventCleanups.forEach(e=>e())}_isActiveCell(e,i){let r=e*this.numCols+i;return e&&(r-=this._firstRowOffset),r==this.activeCell}_focusActiveCell(e=!0){vt(()=>{setTimeout(()=>{let i=this._elementRef.nativeElement.querySelector(".mat-calendar-body-active");i&&(e||(this._skipNextFocus=!0),i.focus())})},{injector:this._injector})}_scheduleFocusActiveCellAfterViewChecked(){this._focusActiveCellAfterViewChecked=!0}_isRangeStart(e){return oA(e,this.startValue,this.endValue)}_isRangeEnd(e){return aA(e,this.startValue,this.endValue)}_isInRange(e){return sA(e,this.startValue,this.endValue,this.isRange)}_isComparisonStart(e){return oA(e,this.comparisonStart,this.comparisonEnd)}_isComparisonBridgeStart(e,i,r){if(!this._isComparisonStart(e)||this._isRangeStart(e)||!this._isInRange(e))return!1;let o=this.rows[i][r-1];if(!o){let a=this.rows[i-1];o=a&&a[a.length-1]}return o&&!this._isRangeEnd(o.compareValue)}_isComparisonBridgeEnd(e,i,r){if(!this._isComparisonEnd(e)||this._isRangeEnd(e)||!this._isInRange(e))return!1;let o=this.rows[i][r+1];if(!o){let a=this.rows[i+1];o=a&&a[0]}return o&&!this._isRangeStart(o.compareValue)}_isComparisonEnd(e){return aA(e,this.comparisonStart,this.comparisonEnd)}_isInComparisonRange(e){return sA(e,this.comparisonStart,this.comparisonEnd,this.isRange)}_isComparisonIdentical(e){return this.comparisonStart===this.comparisonEnd&&e===this.comparisonStart}_isPreviewStart(e){return oA(e,this.previewStart,this.previewEnd)}_isPreviewEnd(e){return aA(e,this.previewStart,this.previewEnd)}_isInPreview(e){return sA(e,this.previewStart,this.previewEnd,this.isRange)}_getDescribedby(e){if(!this.isRange)return null;if(this.startValue===e&&this.endValue===e)return`${this._startDateLabelId} ${this._endDateLabelId}`;if(this.startValue===e)return this._startDateLabelId;if(this.endValue===e)return this._endDateLabelId;if(this.comparisonStart!==null&&this.comparisonEnd!==null){if(e===this.comparisonStart&&e===this.comparisonEnd)return`${this._comparisonStartDateLabelId} ${this._comparisonEndDateLabelId}`;if(e===this.comparisonStart)return this._comparisonStartDateLabelId;if(e===this.comparisonEnd)return this._comparisonEndDateLabelId}return null}_enterHandler=e=>{if(this._skipNextFocus&&e.type==="focus"){this._skipNextFocus=!1;return}if(e.target&&this.isRange){let i=this._getCellFromElement(e.target);i&&this._ngZone.run(()=>this.previewChange.emit({value:i.enabled?i:null,event:e}))}};_touchmoveHandler=e=>{if(!this.isRange)return;let i=P6(e),r=i?this._getCellFromElement(i):null;i!==e.target&&(this._didDragSinceMouseDown=!0),rA(e.target)&&e.preventDefault(),this._ngZone.run(()=>this.previewChange.emit({value:r?.enabled?r:null,event:e}))};_leaveHandler=e=>{this.previewEnd!==null&&this.isRange&&(e.type!=="blur"&&(this._didDragSinceMouseDown=!0),e.target&&this._getCellFromElement(e.target)&&!(e.relatedTarget&&this._getCellFromElement(e.relatedTarget))&&this._ngZone.run(()=>this.previewChange.emit({value:null,event:e})))};_mousedownHandler=e=>{if(!this.isRange)return;this._didDragSinceMouseDown=!1;let i=e.target&&this._getCellFromElement(e.target);!i||!this._isInRange(i.compareValue)||this._ngZone.run(()=>{this.dragStarted.emit({value:i.rawValue,event:e})})};_mouseupHandler=e=>{if(!this.isRange)return;let i=rA(e.target);if(!i){this._ngZone.run(()=>{this.dragEnded.emit({value:null,event:e})});return}i.closest(".mat-calendar-body")===this._elementRef.nativeElement&&this._ngZone.run(()=>{let r=this._getCellFromElement(i);this.dragEnded.emit({value:r?.rawValue??null,event:e})})};_touchendHandler=e=>{let i=P6(e);i&&this._mouseupHandler({target:i})};_getCellFromElement(e){let i=rA(e);if(i){let r=i.getAttribute("data-mat-row"),o=i.getAttribute("data-mat-col");if(r&&o)return this.rows[parseInt(r)]?.[parseInt(o)]||null}return null}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["","mat-calendar-body",""]],hostAttrs:[1,"mat-calendar-body"],inputs:{label:"label",rows:"rows",todayValue:"todayValue",startValue:"startValue",endValue:"endValue",labelMinRequiredCells:"labelMinRequiredCells",numCols:"numCols",activeCell:"activeCell",isRange:"isRange",cellAspectRatio:"cellAspectRatio",comparisonStart:"comparisonStart",comparisonEnd:"comparisonEnd",previewStart:"previewStart",previewEnd:"previewEnd",startDateAccessibleName:"startDateAccessibleName",endDateAccessibleName:"endDateAccessibleName"},outputs:{selectedValueChange:"selectedValueChange",previewChange:"previewChange",activeDateChange:"activeDateChange",dragStarted:"dragStarted",dragEnded:"dragEnded"},exportAs:["matCalendarBody"],features:[Oe],attrs:Hbe,decls:11,vars:11,consts:[["aria-hidden","true"],["role","row"],[1,"mat-calendar-body-hidden-label",3,"id"],[1,"mat-calendar-body-label"],[1,"mat-calendar-body-label",3,"paddingTop","paddingBottom"],["role","gridcell",1,"mat-calendar-body-cell-container",3,"width","paddingTop","paddingBottom"],["role","gridcell",1,"mat-calendar-body-cell-container"],["type","button",1,"mat-calendar-body-cell",3,"click","focus","ngClass","tabindex"],[1,"mat-calendar-body-cell-content","mat-focus-indicator"],["aria-hidden","true",1,"mat-calendar-body-cell-preview"]],template:function(i,r){i&1&&(L(0,Ube,3,6,"tr",0),Mt(1,Gbe,4,1,"tr",1,zbe,!0),m(3,"span",2),f(4),h(),m(5,"span",2),f(6),h(),m(7,"span",2),f(8),h(),m(9,"span",2),f(10),h()),i&2&&(V(r._firstRowOffset.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){color:var(--mat-datepicker-calendar-date-disabled-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-calendar-body-disabled>.mat-calendar-body-today:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){border-color:var(--mat-datepicker-calendar-date-today-disabled-state-outline-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}@media(forced-colors: active){.mat-calendar-body-disabled{opacity:.5}}.mat-calendar-body-cell-content{top:5%;left:5%;z-index:1;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:90%;height:90%;line-height:1;border-width:1px;border-style:solid;border-radius:999px;color:var(--mat-datepicker-calendar-date-text-color, var(--mat-sys-on-surface));border-color:var(--mat-datepicker-calendar-date-outline-color, transparent)}.mat-calendar-body-cell-content.mat-focus-indicator{position:absolute}@media(forced-colors: active){.mat-calendar-body-cell-content{border:none}}.cdk-keyboard-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical),.cdk-program-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){background-color:var(--mat-datepicker-calendar-date-focus-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent))}@media(hover: hover){.mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){background-color:var(--mat-datepicker-calendar-date-hover-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}}.mat-calendar-body-selected{background-color:var(--mat-datepicker-calendar-date-selected-state-background-color, var(--mat-sys-primary));color:var(--mat-datepicker-calendar-date-selected-state-text-color, var(--mat-sys-on-primary))}.mat-calendar-body-disabled>.mat-calendar-body-selected{background-color:var(--mat-datepicker-calendar-date-selected-disabled-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-calendar-body-selected.mat-calendar-body-today{box-shadow:inset 0 0 0 1px var(--mat-datepicker-calendar-date-today-selected-state-outline-color, var(--mat-sys-primary))}.mat-calendar-body-in-range::before{background:var(--mat-datepicker-calendar-date-in-range-state-background-color, var(--mat-sys-primary-container))}.mat-calendar-body-comparison-identical,.mat-calendar-body-in-comparison-range::before{background:var(--mat-datepicker-calendar-date-in-comparison-range-state-background-color, var(--mat-sys-tertiary-container))}.mat-calendar-body-comparison-identical,.mat-calendar-body-in-comparison-range::before{background:var(--mat-datepicker-calendar-date-in-comparison-range-state-background-color, var(--mat-sys-tertiary-container))}.mat-calendar-body-comparison-bridge-start::before,[dir=rtl] .mat-calendar-body-comparison-bridge-end::before{background:linear-gradient(to right, var(--mat-datepicker-calendar-date-in-range-state-background-color, var(--mat-sys-primary-container)) 50%, var(--mat-datepicker-calendar-date-in-comparison-range-state-background-color, var(--mat-sys-tertiary-container)) 50%)}.mat-calendar-body-comparison-bridge-end::before,[dir=rtl] .mat-calendar-body-comparison-bridge-start::before{background:linear-gradient(to left, var(--mat-datepicker-calendar-date-in-range-state-background-color, var(--mat-sys-primary-container)) 50%, var(--mat-datepicker-calendar-date-in-comparison-range-state-background-color, var(--mat-sys-tertiary-container)) 50%)}.mat-calendar-body-in-range>.mat-calendar-body-comparison-identical,.mat-calendar-body-in-comparison-range.mat-calendar-body-in-range::after{background:var(--mat-datepicker-calendar-date-in-overlap-range-state-background-color, var(--mat-sys-secondary-container))}.mat-calendar-body-comparison-identical.mat-calendar-body-selected,.mat-calendar-body-in-comparison-range>.mat-calendar-body-selected{background:var(--mat-datepicker-calendar-date-in-overlap-range-selected-state-background-color, var(--mat-sys-secondary))}@media(forced-colors: active){.mat-datepicker-popup:not(:empty),.mat-calendar-body-cell:not(.mat-calendar-body-in-range) .mat-calendar-body-selected{outline:solid 1px}.mat-calendar-body-today{outline:dotted 1px}.mat-calendar-body-cell::before,.mat-calendar-body-cell::after,.mat-calendar-body-selected{background:none}.mat-calendar-body-in-range::before,.mat-calendar-body-comparison-bridge-start::before,.mat-calendar-body-comparison-bridge-end::before{border-top:solid 1px;border-bottom:solid 1px}.mat-calendar-body-range-start::before{border-left:solid 1px}[dir=rtl] .mat-calendar-body-range-start::before{border-left:0;border-right:solid 1px}.mat-calendar-body-range-end::before{border-right:solid 1px}[dir=rtl] .mat-calendar-body-range-end::before{border-right:0;border-left:solid 1px}.mat-calendar-body-in-comparison-range::before{border-top:dashed 1px;border-bottom:dashed 1px}.mat-calendar-body-comparison-start::before{border-left:dashed 1px}[dir=rtl] .mat-calendar-body-comparison-start::before{border-left:0;border-right:dashed 1px}.mat-calendar-body-comparison-end::before{border-right:dashed 1px}[dir=rtl] .mat-calendar-body-comparison-end::before{border-right:0;border-left:dashed 1px}} +`],encapsulation:2,changeDetection:0})}return t})();function nA(t){return t?.nodeName==="TD"}function rA(t){let n;return nA(t)?n=t:nA(t.parentNode)?n=t.parentNode:nA(t.parentNode?.parentNode)&&(n=t.parentNode.parentNode),n?.getAttribute("data-mat-row")!=null?n:null}function oA(t,n,e){return e!==null&&n!==e&&t=n&&t===e}function sA(t,n,e,i){return i&&n!==null&&e!==null&&n!==e&&t>=n&&t<=e}function P6(t){let n=t.changedTouches[0];return document.elementFromPoint(n.clientX,n.clientY)}var Ta=class{start;end;_disableStructuralEquivalency;constructor(n,e){this.start=n,this.end=e}},k_=(()=>{class t{selection;_adapter;_selectionChanged=new z;selectionChanged=this._selectionChanged;constructor(e,i){this.selection=e,this._adapter=i,this.selection=e}updateSelection(e,i){let r=this.selection;this.selection=e,this._selectionChanged.next({selection:e,source:i,oldValue:r})}ngOnDestroy(){this._selectionChanged.complete()}_isValidDateInstance(e){return this._adapter.isDateInstance(e)&&this._adapter.isValid(e)}static \u0275fac=function(i){jd()};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})(),a0e=(()=>{class t extends k_{constructor(e){super(null,e)}add(e){super.updateSelection(e,this)}isValid(){return this.selection!=null&&this._isValidDateInstance(this.selection)}isComplete(){return this.selection!=null}clone(){let e=new t(this._adapter);return e.updateSelection(this.selection,this),e}static \u0275fac=function(i){return new(i||t)(pe($i))};static \u0275prov=R({token:t,factory:t.\u0275fac})}return t})();function s0e(t,n){return t||new a0e(n)}var j6={provide:k_,deps:[[new Ds,new dc,k_],$i],useFactory:s0e};var H6=new O("MAT_DATE_RANGE_SELECTION_STRATEGY");var lA=7,l0e=0,F6=(()=>{class t{_changeDetectorRef=u(ye);_dateFormats=u(Vs,{optional:!0});_dateAdapter=u($i,{optional:!0});_dir=u(Yt,{optional:!0});_rangeStrategy=u(H6,{optional:!0});_rerenderSubscription=ke.EMPTY;_selectionKeyPressed;get activeDate(){return this._activeDate}set activeDate(e){let i=this._activeDate,r=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))||this._dateAdapter.today();this._activeDate=this._dateAdapter.clampDate(r,this.minDate,this.maxDate),this._hasSameMonthAndYear(i,this._activeDate)||this._init()}_activeDate;get selected(){return this._selected}set selected(e){e instanceof Ta?this._selected=e:this._selected=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e)),this._setRanges(this._selected)}_selected;get minDate(){return this._minDate}set minDate(e){this._minDate=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_minDate;get maxDate(){return this._maxDate}set maxDate(e){this._maxDate=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_maxDate;dateFilter;dateClass;comparisonStart;comparisonEnd;startDateAccessibleName;endDateAccessibleName;activeDrag=null;selectedChange=new U;_userSelection=new U;dragStarted=new U;dragEnded=new U;activeDateChange=new U;_matCalendarBody;_monthLabel=he("");_weeks=he([]);_firstWeekOffset=he(0);_rangeStart=he(null);_rangeEnd=he(null);_comparisonRangeStart=he(null);_comparisonRangeEnd=he(null);_previewStart=he(null);_previewEnd=he(null);_isRange=he(!1);_todayDate=he(null);_weekdays=he([]);constructor(){u(ft).load(ro),this._activeDate=this._dateAdapter.today()}ngAfterContentInit(){this._rerenderSubscription=this._dateAdapter.localeChanges.pipe(Ue(null)).subscribe(()=>this._init())}ngOnChanges(e){let i=e.comparisonStart||e.comparisonEnd;i&&!i.firstChange&&this._setRanges(this.selected),e.activeDrag&&!this.activeDrag&&this._clearPreview()}ngOnDestroy(){this._rerenderSubscription.unsubscribe()}_dateSelected(e){let i=e.value,r=this._getDateFromDayOfMonth(i),o,a;this._selected instanceof Ta?(o=this._getDateInCurrentMonth(this._selected.start),a=this._getDateInCurrentMonth(this._selected.end)):o=a=this._getDateInCurrentMonth(this._selected),(o!==i||a!==i)&&this.selectedChange.emit(r),this._userSelection.emit({value:r,event:e.event}),this._clearPreview(),this._changeDetectorRef.markForCheck()}_updateActiveDate(e){let i=e.value,r=this._activeDate;this.activeDate=this._getDateFromDayOfMonth(i),this._dateAdapter.compareDate(r,this.activeDate)&&this.activeDateChange.emit(this._activeDate)}_handleCalendarBodyKeydown(e){let i=this._activeDate,r=this._isRtl();switch(e.keyCode){case 37:this.activeDate=this._dateAdapter.addCalendarDays(this._activeDate,r?1:-1);break;case 39:this.activeDate=this._dateAdapter.addCalendarDays(this._activeDate,r?-1:1);break;case 38:this.activeDate=this._dateAdapter.addCalendarDays(this._activeDate,-7);break;case 40:this.activeDate=this._dateAdapter.addCalendarDays(this._activeDate,7);break;case 36:this.activeDate=this._dateAdapter.addCalendarDays(this._activeDate,1-this._dateAdapter.getDate(this._activeDate));break;case 35:this.activeDate=this._dateAdapter.addCalendarDays(this._activeDate,this._dateAdapter.getNumDaysInMonth(this._activeDate)-this._dateAdapter.getDate(this._activeDate));break;case 33:this.activeDate=e.altKey?this._dateAdapter.addCalendarYears(this._activeDate,-1):this._dateAdapter.addCalendarMonths(this._activeDate,-1);break;case 34:this.activeDate=e.altKey?this._dateAdapter.addCalendarYears(this._activeDate,1):this._dateAdapter.addCalendarMonths(this._activeDate,1);break;case 13:case 32:this._selectionKeyPressed=!0,this._canSelect(this._activeDate)&&e.preventDefault();return;case 27:this._previewEnd()!=null&&!Gt(e)&&(this._clearPreview(),this.activeDrag?this.dragEnded.emit({value:null,event:e}):(this.selectedChange.emit(null),this._userSelection.emit({value:null,event:e})),e.preventDefault(),e.stopPropagation());return;default:return}this._dateAdapter.compareDate(i,this.activeDate)&&(this.activeDateChange.emit(this.activeDate),this._focusActiveCellAfterViewChecked()),e.preventDefault()}_handleCalendarBodyKeyup(e){(e.keyCode===32||e.keyCode===13)&&(this._selectionKeyPressed&&this._canSelect(this._activeDate)&&this._dateSelected({value:this._dateAdapter.getDate(this._activeDate),event:e}),this._selectionKeyPressed=!1)}_init(){this._setRanges(this.selected),this._todayDate.set(this._getCellCompareValue(this._dateAdapter.today())),this._monthLabel.set(this._dateFormats.display.monthLabel?this._dateAdapter.format(this.activeDate,this._dateFormats.display.monthLabel):this._dateAdapter.getMonthNames("short")[this._dateAdapter.getMonth(this.activeDate)].toLocaleUpperCase());let e=this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate),this._dateAdapter.getMonth(this.activeDate),1);this._firstWeekOffset.set((lA+this._dateAdapter.getDayOfWeek(e)-this._dateAdapter.getFirstDayOfWeek())%lA),this._initWeekdays(),this._createWeekCells(),this._changeDetectorRef.markForCheck()}_focusActiveCell(e){this._matCalendarBody._focusActiveCell(e)}_focusActiveCellAfterViewChecked(){this._matCalendarBody._scheduleFocusActiveCellAfterViewChecked()}_previewChanged({event:e,value:i}){if(this._rangeStrategy){let r=i?i.rawValue:null,o=this._rangeStrategy.createPreview(r,this.selected,e);if(this._previewStart.set(this._getCellCompareValue(o.start)),this._previewEnd.set(this._getCellCompareValue(o.end)),this.activeDrag&&r){let a=this._rangeStrategy.createDrag?.(this.activeDrag.value,this.selected,r,e);a&&(this._previewStart.set(this._getCellCompareValue(a.start)),this._previewEnd.set(this._getCellCompareValue(a.end)))}}}_dragEnded(e){if(this.activeDrag)if(e.value){let i=this._rangeStrategy?.createDrag?.(this.activeDrag.value,this.selected,e.value,e.event);this.dragEnded.emit({value:i??null,event:e.event})}else this.dragEnded.emit({value:null,event:e.event})}_getDateFromDayOfMonth(e){return this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate),this._dateAdapter.getMonth(this.activeDate),e)}_initWeekdays(){let e=this._dateAdapter.getFirstDayOfWeek(),i=this._dateAdapter.getDayOfWeekNames("narrow"),o=this._dateAdapter.getDayOfWeekNames("long").map((a,s)=>({long:a,narrow:i[s],id:l0e++}));this._weekdays.set(o.slice(e).concat(o.slice(0,e)))}_createWeekCells(){let e=this._dateAdapter.getNumDaysInMonth(this.activeDate),i=this._dateAdapter.getDateNames(),r=[[]];for(let o=0,a=this._firstWeekOffset();o=0)&&(!this.maxDate||this._dateAdapter.compareDate(e,this.maxDate)<=0)&&(!this.dateFilter||this.dateFilter(e))}_getDateInCurrentMonth(e){return e&&this._hasSameMonthAndYear(e,this.activeDate)?this._dateAdapter.getDate(e):null}_hasSameMonthAndYear(e,i){return!!(e&&i&&this._dateAdapter.getMonth(e)==this._dateAdapter.getMonth(i)&&this._dateAdapter.getYear(e)==this._dateAdapter.getYear(i))}_getCellCompareValue(e){if(e){let i=this._dateAdapter.getYear(e),r=this._dateAdapter.getMonth(e),o=this._dateAdapter.getDate(e);return new Date(i,r,o).getTime()}return null}_isRtl(){return this._dir&&this._dir.value==="rtl"}_setRanges(e){e instanceof Ta?(this._rangeStart.set(this._getCellCompareValue(e.start)),this._rangeEnd.set(this._getCellCompareValue(e.end)),this._isRange.set(!0)):(this._rangeStart.set(this._getCellCompareValue(e)),this._rangeEnd.set(this._rangeStart()),this._isRange.set(!1)),this._comparisonRangeStart.set(this._getCellCompareValue(this.comparisonStart)),this._comparisonRangeEnd.set(this._getCellCompareValue(this.comparisonEnd))}_canSelect(e){return!this.dateFilter||this.dateFilter(e)}_clearPreview(){this._previewStart.set(null),this._previewEnd.set(null)}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-month-view"]],viewQuery:function(i,r){if(i&1&&ie(fp,5),i&2){let o;j(o=H())&&(r._matCalendarBody=o.first)}},inputs:{activeDate:"activeDate",selected:"selected",minDate:"minDate",maxDate:"maxDate",dateFilter:"dateFilter",dateClass:"dateClass",comparisonStart:"comparisonStart",comparisonEnd:"comparisonEnd",startDateAccessibleName:"startDateAccessibleName",endDateAccessibleName:"endDateAccessibleName",activeDrag:"activeDrag"},outputs:{selectedChange:"selectedChange",_userSelection:"_userSelection",dragStarted:"dragStarted",dragEnded:"dragEnded",activeDateChange:"activeDateChange"},exportAs:["matMonthView"],features:[Oe],decls:8,vars:14,consts:[["role","grid",1,"mat-calendar-table"],[1,"mat-calendar-table-header"],["scope","col"],["aria-hidden","true"],["colspan","7",1,"mat-calendar-table-header-divider"],["mat-calendar-body","",3,"selectedValueChange","activeDateChange","previewChange","dragStarted","dragEnded","keyup","keydown","label","rows","todayValue","startValue","endValue","comparisonStart","comparisonEnd","previewStart","previewEnd","isRange","labelMinRequiredCells","activeCell","startDateAccessibleName","endDateAccessibleName"],[1,"cdk-visually-hidden"]],template:function(i,r){i&1&&(m(0,"table",0)(1,"thead",1)(2,"tr"),Mt(3,qbe,5,2,"th",2,B6),h(),m(5,"tr",3),M(6,"th",4),h()(),m(7,"tbody",5),S("selectedValueChange",function(a){return r._dateSelected(a)})("activeDateChange",function(a){return r._updateActiveDate(a)})("previewChange",function(a){return r._previewChanged(a)})("dragStarted",function(a){return r.dragStarted.emit(a)})("dragEnded",function(a){return r._dragEnded(a)})("keyup",function(a){return r._handleCalendarBodyKeyup(a)})("keydown",function(a){return r._handleCalendarBodyKeydown(a)}),h()()),i&2&&(g(3),Et(r._weekdays()),g(4),v("label",r._monthLabel())("rows",r._weeks())("todayValue",r._todayDate())("startValue",r._rangeStart())("endValue",r._rangeEnd())("comparisonStart",r._comparisonRangeStart())("comparisonEnd",r._comparisonRangeEnd())("previewStart",r._previewStart())("previewEnd",r._previewEnd())("isRange",r._isRange())("labelMinRequiredCells",3)("activeCell",r._dateAdapter.getDate(r.activeDate)-1)("startDateAccessibleName",r.startDateAccessibleName)("endDateAccessibleName",r.endDateAccessibleName))},dependencies:[fp],encapsulation:2,changeDetection:0})}return t})(),Bo=24,cA=4,N6=(()=>{class t{_changeDetectorRef=u(ye);_dateAdapter=u($i,{optional:!0});_dir=u(Yt,{optional:!0});_rerenderSubscription=ke.EMPTY;_selectionKeyPressed;get activeDate(){return this._activeDate}set activeDate(e){let i=this._activeDate,r=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))||this._dateAdapter.today();this._activeDate=this._dateAdapter.clampDate(r,this.minDate,this.maxDate),z6(this._dateAdapter,i,this._activeDate,this.minDate,this.maxDate)||this._init()}_activeDate;get selected(){return this._selected}set selected(e){e instanceof Ta?this._selected=e:this._selected=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e)),this._setSelectedYear(e)}_selected;get minDate(){return this._minDate}set minDate(e){this._minDate=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_minDate;get maxDate(){return this._maxDate}set maxDate(e){this._maxDate=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_maxDate;dateFilter;dateClass;selectedChange=new U;yearSelected=new U;activeDateChange=new U;_matCalendarBody;_years=he([]);_todayYear=he(0);_selectedYear=he(null);constructor(){this._dateAdapter,this._activeDate=this._dateAdapter.today()}ngAfterContentInit(){this._rerenderSubscription=this._dateAdapter.localeChanges.pipe(Ue(null)).subscribe(()=>this._init())}ngOnDestroy(){this._rerenderSubscription.unsubscribe()}_init(){this._todayYear.set(this._dateAdapter.getYear(this._dateAdapter.today()));let i=this._dateAdapter.getYear(this._activeDate)-E_(this._dateAdapter,this.activeDate,this.minDate,this.maxDate),r=[];for(let o=0,a=[];othis._createCellForYear(s))),a=[]);this._years.set(r),this._changeDetectorRef.markForCheck()}_yearSelected(e){let i=e.value,r=this._dateAdapter.createDate(i,0,1),o=this._getDateFromYear(i);this.yearSelected.emit(r),this.selectedChange.emit(o)}_updateActiveDate(e){let i=e.value,r=this._activeDate;this.activeDate=this._getDateFromYear(i),this._dateAdapter.compareDate(r,this.activeDate)&&this.activeDateChange.emit(this.activeDate)}_handleCalendarBodyKeydown(e){let i=this._activeDate,r=this._isRtl();switch(e.keyCode){case 37:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,r?1:-1);break;case 39:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,r?-1:1);break;case 38:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,-cA);break;case 40:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,cA);break;case 36:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,-E_(this._dateAdapter,this.activeDate,this.minDate,this.maxDate));break;case 35:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,Bo-E_(this._dateAdapter,this.activeDate,this.minDate,this.maxDate)-1);break;case 33:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,e.altKey?-Bo*10:-Bo);break;case 34:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,e.altKey?Bo*10:Bo);break;case 13:case 32:this._selectionKeyPressed=!0;break;default:return}this._dateAdapter.compareDate(i,this.activeDate)&&this.activeDateChange.emit(this.activeDate),this._focusActiveCellAfterViewChecked(),e.preventDefault()}_handleCalendarBodyKeyup(e){(e.keyCode===32||e.keyCode===13)&&(this._selectionKeyPressed&&this._yearSelected({value:this._dateAdapter.getYear(this._activeDate),event:e}),this._selectionKeyPressed=!1)}_getActiveCell(){return E_(this._dateAdapter,this.activeDate,this.minDate,this.maxDate)}_focusActiveCell(){this._matCalendarBody._focusActiveCell()}_focusActiveCellAfterViewChecked(){this._matCalendarBody._scheduleFocusActiveCellAfterViewChecked()}_getDateFromYear(e){let i=this._dateAdapter.getMonth(this.activeDate),r=this._dateAdapter.getNumDaysInMonth(this._dateAdapter.createDate(e,i,1));return this._dateAdapter.createDate(e,i,Math.min(this._dateAdapter.getDate(this.activeDate),r))}_createCellForYear(e){let i=this._dateAdapter.createDate(e,0,1),r=this._dateAdapter.getYearName(i),o=this.dateClass?this.dateClass(i,"multi-year"):void 0;return new S_(e,r,r,this._shouldEnableYear(e),o)}_shouldEnableYear(e){if(e==null||this.maxDate&&e>this._dateAdapter.getYear(this.maxDate)||this.minDate&&e{class t{_changeDetectorRef=u(ye);_dateFormats=u(Vs,{optional:!0});_dateAdapter=u($i,{optional:!0});_dir=u(Yt,{optional:!0});_rerenderSubscription=ke.EMPTY;_selectionKeyPressed;get activeDate(){return this._activeDate}set activeDate(e){let i=this._activeDate,r=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))||this._dateAdapter.today();this._activeDate=this._dateAdapter.clampDate(r,this.minDate,this.maxDate),this._dateAdapter.getYear(i)!==this._dateAdapter.getYear(this._activeDate)&&this._init()}_activeDate;get selected(){return this._selected}set selected(e){e instanceof Ta?this._selected=e:this._selected=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e)),this._setSelectedMonth(e)}_selected;get minDate(){return this._minDate}set minDate(e){this._minDate=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_minDate;get maxDate(){return this._maxDate}set maxDate(e){this._maxDate=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_maxDate;dateFilter;dateClass;selectedChange=new U;monthSelected=new U;activeDateChange=new U;_matCalendarBody;_months=he([]);_yearLabel=he("");_todayMonth=he(null);_selectedMonth=he(null);constructor(){this._activeDate=this._dateAdapter.today()}ngAfterContentInit(){this._rerenderSubscription=this._dateAdapter.localeChanges.pipe(Ue(null)).subscribe(()=>this._init())}ngOnDestroy(){this._rerenderSubscription.unsubscribe()}_monthSelected(e){let i=e.value,r=this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate),i,1);this.monthSelected.emit(r);let o=this._getDateFromMonth(i);this.selectedChange.emit(o)}_updateActiveDate(e){let i=e.value,r=this._activeDate;this.activeDate=this._getDateFromMonth(i),this._dateAdapter.compareDate(r,this.activeDate)&&this.activeDateChange.emit(this.activeDate)}_handleCalendarBodyKeydown(e){let i=this._activeDate,r=this._isRtl();switch(e.keyCode){case 37:this.activeDate=this._dateAdapter.addCalendarMonths(this._activeDate,r?1:-1);break;case 39:this.activeDate=this._dateAdapter.addCalendarMonths(this._activeDate,r?-1:1);break;case 38:this.activeDate=this._dateAdapter.addCalendarMonths(this._activeDate,-4);break;case 40:this.activeDate=this._dateAdapter.addCalendarMonths(this._activeDate,4);break;case 36:this.activeDate=this._dateAdapter.addCalendarMonths(this._activeDate,-this._dateAdapter.getMonth(this._activeDate));break;case 35:this.activeDate=this._dateAdapter.addCalendarMonths(this._activeDate,11-this._dateAdapter.getMonth(this._activeDate));break;case 33:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,e.altKey?-10:-1);break;case 34:this.activeDate=this._dateAdapter.addCalendarYears(this._activeDate,e.altKey?10:1);break;case 13:case 32:this._selectionKeyPressed=!0;break;default:return}this._dateAdapter.compareDate(i,this.activeDate)&&(this.activeDateChange.emit(this.activeDate),this._focusActiveCellAfterViewChecked()),e.preventDefault()}_handleCalendarBodyKeyup(e){(e.keyCode===32||e.keyCode===13)&&(this._selectionKeyPressed&&this._monthSelected({value:this._dateAdapter.getMonth(this._activeDate),event:e}),this._selectionKeyPressed=!1)}_init(){this._setSelectedMonth(this.selected),this._todayMonth.set(this._getMonthInCurrentYear(this._dateAdapter.today())),this._yearLabel.set(this._dateAdapter.getYearName(this.activeDate));let e=this._dateAdapter.getMonthNames("short");this._months.set([[0,1,2,3],[4,5,6,7],[8,9,10,11]].map(i=>i.map(r=>this._createCellForMonth(r,e[r])))),this._changeDetectorRef.markForCheck()}_focusActiveCell(){this._matCalendarBody._focusActiveCell()}_focusActiveCellAfterViewChecked(){this._matCalendarBody._scheduleFocusActiveCellAfterViewChecked()}_getMonthInCurrentYear(e){return e&&this._dateAdapter.getYear(e)==this._dateAdapter.getYear(this.activeDate)?this._dateAdapter.getMonth(e):null}_getDateFromMonth(e){let i=this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate),e,1),r=this._dateAdapter.getNumDaysInMonth(i);return this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate),e,Math.min(this._dateAdapter.getDate(this.activeDate),r))}_createCellForMonth(e,i){let r=this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate),e,1),o=this._dateAdapter.format(r,this._dateFormats.display.monthYearA11yLabel),a=this.dateClass?this.dateClass(r,"year"):void 0;return new S_(e,i.toLocaleUpperCase(),o,this._shouldEnableMonth(e),a)}_shouldEnableMonth(e){let i=this._dateAdapter.getYear(this.activeDate);if(e==null||this._isYearAndMonthAfterMaxDate(i,e)||this._isYearAndMonthBeforeMinDate(i,e))return!1;if(!this.dateFilter)return!0;let r=this._dateAdapter.createDate(i,e,1);for(let o=r;this._dateAdapter.getMonth(o)==e;o=this._dateAdapter.addCalendarDays(o,1))if(this.dateFilter(o))return!0;return!1}_isYearAndMonthAfterMaxDate(e,i){if(this.maxDate){let r=this._dateAdapter.getYear(this.maxDate),o=this._dateAdapter.getMonth(this.maxDate);return e>r||e===r&&i>o}return!1}_isYearAndMonthBeforeMinDate(e,i){if(this.minDate){let r=this._dateAdapter.getYear(this.minDate),o=this._dateAdapter.getMonth(this.minDate);return e{class t{_intl=u(gp);calendar=u(dA);_dateAdapter=u($i,{optional:!0});_dateFormats=u(Vs,{optional:!0});_periodButtonText;_periodButtonDescription;_periodButtonLabel;_prevButtonLabel;_nextButtonLabel;constructor(){u(ft).load(ro);let e=u(ye);this._updateLabels(),this.calendar.stateChanges.subscribe(()=>{this._updateLabels(),e.markForCheck()})}get periodButtonText(){return this._periodButtonText}get periodButtonDescription(){return this._periodButtonDescription}get periodButtonLabel(){return this._periodButtonLabel}get prevButtonLabel(){return this._prevButtonLabel}get nextButtonLabel(){return this._nextButtonLabel}currentPeriodClicked(){this.calendar.currentView=this.calendar.currentView=="month"?"multi-year":"month"}previousClicked(){this.previousEnabled()&&(this.calendar.activeDate=this.calendar.currentView=="month"?this._dateAdapter.addCalendarMonths(this.calendar.activeDate,-1):this._dateAdapter.addCalendarYears(this.calendar.activeDate,this.calendar.currentView=="year"?-1:-Bo))}nextClicked(){this.nextEnabled()&&(this.calendar.activeDate=this.calendar.currentView=="month"?this._dateAdapter.addCalendarMonths(this.calendar.activeDate,1):this._dateAdapter.addCalendarYears(this.calendar.activeDate,this.calendar.currentView=="year"?1:Bo))}previousEnabled(){return this.calendar.minDate?!this.calendar.minDate||!this._isSameView(this.calendar.activeDate,this.calendar.minDate):!0}nextEnabled(){return!this.calendar.maxDate||!this._isSameView(this.calendar.activeDate,this.calendar.maxDate)}_updateLabels(){let e=this.calendar,i=this._intl,r=this._dateAdapter;e.currentView==="month"?(this._periodButtonText=r.format(e.activeDate,this._dateFormats.display.monthYearLabel).toLocaleUpperCase(),this._periodButtonDescription=r.format(e.activeDate,this._dateFormats.display.monthYearLabel).toLocaleUpperCase(),this._periodButtonLabel=i.switchToMultiYearViewLabel,this._prevButtonLabel=i.prevMonthLabel,this._nextButtonLabel=i.nextMonthLabel):e.currentView==="year"?(this._periodButtonText=r.getYearName(e.activeDate),this._periodButtonDescription=r.getYearName(e.activeDate),this._periodButtonLabel=i.switchToMonthViewLabel,this._prevButtonLabel=i.prevYearLabel,this._nextButtonLabel=i.nextYearLabel):(this._periodButtonText=i.formatYearRange(...this._formatMinAndMaxYearLabels()),this._periodButtonDescription=i.formatYearRangeLabel(...this._formatMinAndMaxYearLabels()),this._periodButtonLabel=i.switchToMonthViewLabel,this._prevButtonLabel=i.prevMultiYearLabel,this._nextButtonLabel=i.nextMultiYearLabel)}_isSameView(e,i){return this.calendar.currentView=="month"?this._dateAdapter.getYear(e)==this._dateAdapter.getYear(i)&&this._dateAdapter.getMonth(e)==this._dateAdapter.getMonth(i):this.calendar.currentView=="year"?this._dateAdapter.getYear(e)==this._dateAdapter.getYear(i):z6(this._dateAdapter,e,i,this.calendar.minDate,this.calendar.maxDate)}_formatMinAndMaxYearLabels(){let i=this._dateAdapter.getYear(this.calendar.activeDate)-E_(this._dateAdapter,this.calendar.activeDate,this.calendar.minDate,this.calendar.maxDate),r=i+Bo-1,o=this._dateAdapter.getYearName(this._dateAdapter.createDate(i,0,1)),a=this._dateAdapter.getYearName(this._dateAdapter.createDate(r,0,1));return[o,a]}_periodButtonLabelId=u(et).getId("mat-calendar-period-label-");static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-calendar-header"]],exportAs:["matCalendarHeader"],ngContentSelectors:Ybe,decls:17,vars:13,consts:[[1,"mat-calendar-header"],[1,"mat-calendar-controls"],["aria-live","polite",1,"cdk-visually-hidden",3,"id"],["matButton","","type","button",1,"mat-calendar-period-button",3,"click"],["aria-hidden","true"],["viewBox","0 0 10 5","focusable","false","aria-hidden","true",1,"mat-calendar-arrow"],["points","0,0 5,5 10,0"],[1,"mat-calendar-spacer"],["matIconButton","","type","button","disabledInteractive","",1,"mat-calendar-previous-button",3,"click","disabled","matTooltip"],["viewBox","0 0 24 24","focusable","false","aria-hidden","true"],["d","M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"],["matIconButton","","type","button","disabledInteractive","",1,"mat-calendar-next-button",3,"click","disabled","matTooltip"],["d","M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"]],template:function(i,r){i&1&&(Ee(),m(0,"div",0)(1,"div",1)(2,"span",2),f(3),h(),m(4,"button",3),S("click",function(){return r.currentPeriodClicked()}),m(5,"span",4),f(6),h(),ii(),m(7,"svg",5),M(8,"polygon",6),h()(),Qo(),M(9,"div",7),ne(10),m(11,"button",8),S("click",function(){return r.previousClicked()}),ii(),m(12,"svg",9),M(13,"path",10),h()(),Qo(),m(14,"button",11),S("click",function(){return r.nextClicked()}),ii(),m(15,"svg",9),M(16,"path",12),h()()()()),i&2&&(g(2),v("id",r._periodButtonLabelId),g(),N(r.periodButtonDescription),g(),X("aria-label",r.periodButtonLabel)("aria-describedby",r._periodButtonLabelId),g(2),N(r.periodButtonText),g(),G("mat-calendar-invert",r.calendar.currentView!=="month"),g(4),v("disabled",!r.previousEnabled())("matTooltip",r.prevButtonLabel),X("aria-label",r.prevButtonLabel),g(3),v("disabled",!r.nextEnabled())("matTooltip",r.nextButtonLabel),X("aria-label",r.nextButtonLabel))},dependencies:[_t,Ft,ur],encapsulation:2,changeDetection:0})}return t})(),dA=(()=>{class t{_dateAdapter=u($i,{optional:!0});_dateFormats=u(Vs,{optional:!0});_changeDetectorRef=u(ye);_elementRef=u(Y);headerComponent;_calendarHeaderPortal;_intlChanges;_moveFocusOnNextTick=!1;get startAt(){return this._startAt}set startAt(e){this._startAt=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_startAt;startView="month";get selected(){return this._selected}set selected(e){e instanceof Ta?this._selected=e:this._selected=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_selected;get minDate(){return this._minDate}set minDate(e){this._minDate=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_minDate;get maxDate(){return this._maxDate}set maxDate(e){this._maxDate=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_maxDate;dateFilter;dateClass;comparisonStart;comparisonEnd;startDateAccessibleName;endDateAccessibleName;selectedChange=new U;yearSelected=new U;monthSelected=new U;viewChanged=new U(!0);_userSelection=new U;_userDragDrop=new U;monthView;yearView;multiYearView;get activeDate(){return this._clampedActiveDate}set activeDate(e){this._clampedActiveDate=this._dateAdapter.clampDate(e,this.minDate,this.maxDate),this.stateChanges.next(),this._changeDetectorRef.markForCheck()}_clampedActiveDate;get currentView(){return this._currentView}set currentView(e){let i=this._currentView!==e?e:null;this._currentView=e,this._moveFocusOnNextTick=!0,this._changeDetectorRef.markForCheck(),i&&(this.stateChanges.next(),this.viewChanged.emit(i))}_currentView;_activeDrag=null;stateChanges=new z;constructor(){this._intlChanges=u(gp).changes.subscribe(()=>{this._changeDetectorRef.markForCheck(),this.stateChanges.next()})}ngAfterContentInit(){this._calendarHeaderPortal=new ao(this.headerComponent||$6),this.activeDate=this.startAt||this._dateAdapter.today(),this._currentView=this.startView}ngAfterViewChecked(){this._moveFocusOnNextTick&&(this._moveFocusOnNextTick=!1,this.focusActiveCell())}ngOnDestroy(){this._intlChanges.unsubscribe(),this.stateChanges.complete()}ngOnChanges(e){let i=e.minDate&&!this._dateAdapter.sameDate(e.minDate.previousValue,e.minDate.currentValue)?e.minDate:void 0,r=e.maxDate&&!this._dateAdapter.sameDate(e.maxDate.previousValue,e.maxDate.currentValue)?e.maxDate:void 0,o=i||r||e.dateFilter;if(o&&!o.firstChange){let a=this._getCurrentViewComponent();a&&(this._elementRef.nativeElement.contains(So())&&(this._moveFocusOnNextTick=!0),this._changeDetectorRef.detectChanges(),a._init())}this.stateChanges.next()}focusActiveCell(){this._getCurrentViewComponent()._focusActiveCell(!1)}updateTodaysDate(){this._getCurrentViewComponent()._init()}_dateSelected(e){let i=e.value;(this.selected instanceof Ta||i&&!this._dateAdapter.sameDate(i,this.selected))&&this.selectedChange.emit(i),this._userSelection.emit(e)}_yearSelectedInMultiYearView(e){this.yearSelected.emit(e)}_monthSelectedInYearView(e){this.monthSelected.emit(e)}_goToDateInView(e,i){this.activeDate=e,this.currentView=i}_dragStarted(e){this._activeDrag=e}_dragEnded(e){this._activeDrag&&(e.value&&this._userDragDrop.emit(e),this._activeDrag=null)}_getCurrentViewComponent(){return this.monthView||this.yearView||this.multiYearView}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-calendar"]],viewQuery:function(i,r){if(i&1&&(ie(F6,5),ie(L6,5),ie(N6,5)),i&2){let o;j(o=H())&&(r.monthView=o.first),j(o=H())&&(r.yearView=o.first),j(o=H())&&(r.multiYearView=o.first)}},hostAttrs:[1,"mat-calendar"],inputs:{headerComponent:"headerComponent",startAt:"startAt",startView:"startView",selected:"selected",minDate:"minDate",maxDate:"maxDate",dateFilter:"dateFilter",dateClass:"dateClass",comparisonStart:"comparisonStart",comparisonEnd:"comparisonEnd",startDateAccessibleName:"startDateAccessibleName",endDateAccessibleName:"endDateAccessibleName"},outputs:{selectedChange:"selectedChange",yearSelected:"yearSelected",monthSelected:"monthSelected",viewChanged:"viewChanged",_userSelection:"_userSelection",_userDragDrop:"_userDragDrop"},exportAs:["matCalendar"],features:[we([j6]),Oe],decls:5,vars:2,consts:[[3,"cdkPortalOutlet"],["cdkMonitorSubtreeFocus","","tabindex","-1",1,"mat-calendar-content"],[3,"activeDate","selected","dateFilter","maxDate","minDate","dateClass","comparisonStart","comparisonEnd","startDateAccessibleName","endDateAccessibleName","activeDrag"],[3,"activeDate","selected","dateFilter","maxDate","minDate","dateClass"],[3,"activeDateChange","_userSelection","dragStarted","dragEnded","activeDate","selected","dateFilter","maxDate","minDate","dateClass","comparisonStart","comparisonEnd","startDateAccessibleName","endDateAccessibleName","activeDrag"],[3,"activeDateChange","monthSelected","selectedChange","activeDate","selected","dateFilter","maxDate","minDate","dateClass"],[3,"activeDateChange","yearSelected","selectedChange","activeDate","selected","dateFilter","maxDate","minDate","dateClass"]],template:function(i,r){if(i&1&&(A(0,Qbe,0,0,"ng-template",0),m(1,"div",1),L(2,Kbe,1,11,"mat-month-view",2)(3,Zbe,1,6,"mat-year-view",3)(4,Xbe,1,6,"mat-multi-year-view",3),h()),i&2){let o;v("cdkPortalOutlet",r._calendarHeaderPortal),g(2),V((o=r.currentView)==="month"?2:o==="year"?3:o==="multi-year"?4:-1)}},dependencies:[Ir,Kf,F6,L6,N6],styles:[`.mat-calendar{display:block;line-height:normal;font-family:var(--mat-datepicker-calendar-text-font, var(--mat-sys-body-medium-font));font-size:var(--mat-datepicker-calendar-text-size, var(--mat-sys-body-medium-size))}.mat-calendar-header{padding:8px 8px 0 8px}.mat-calendar-content{padding:0 8px 8px 8px;outline:none}.mat-calendar-controls{display:flex;align-items:center;margin:5% calc(4.7142857143% - 16px)}.mat-calendar-spacer{flex:1 1 auto}.mat-calendar-period-button{min-width:0;margin:0 8px;font-size:var(--mat-datepicker-calendar-period-button-text-size, var(--mat-sys-title-small-size));font-weight:var(--mat-datepicker-calendar-period-button-text-weight, var(--mat-sys-title-small-weight));--mat-button-text-label-text-color: var(--mat-datepicker-calendar-period-button-text-color, var(--mat-sys-on-surface-variant))}.mat-calendar-arrow{display:inline-block;width:10px;height:5px;margin:0 0 0 5px;vertical-align:middle;fill:var(--mat-datepicker-calendar-period-button-icon-color, var(--mat-sys-on-surface-variant))}.mat-calendar-arrow.mat-calendar-invert{transform:rotate(180deg)}[dir=rtl] .mat-calendar-arrow{margin:0 5px 0 0}@media(forced-colors: active){.mat-calendar-arrow{fill:CanvasText}}.mat-datepicker-content .mat-calendar-previous-button:not(.mat-mdc-button-disabled),.mat-datepicker-content .mat-calendar-next-button:not(.mat-mdc-button-disabled){color:var(--mat-datepicker-calendar-navigation-button-icon-color, var(--mat-sys-on-surface-variant))}[dir=rtl] .mat-calendar-previous-button,[dir=rtl] .mat-calendar-next-button{transform:rotate(180deg)}.mat-calendar-table{border-spacing:0;border-collapse:collapse;width:100%}.mat-calendar-table-header th{text-align:center;padding:0 0 8px 0;color:var(--mat-datepicker-calendar-header-text-color, var(--mat-sys-on-surface-variant));font-size:var(--mat-datepicker-calendar-header-text-size, var(--mat-sys-title-small-size));font-weight:var(--mat-datepicker-calendar-header-text-weight, var(--mat-sys-title-small-weight))}.mat-calendar-table-header-divider{position:relative;height:1px}.mat-calendar-table-header-divider::after{content:"";position:absolute;top:0;left:-8px;right:-8px;height:1px;background:var(--mat-datepicker-calendar-header-divider-color, transparent)}.mat-calendar-body-cell-content::before{margin:calc(calc(var(--mat-focus-indicator-border-width, 3px) + 3px)*-1)}.mat-calendar-body-cell:focus .mat-focus-indicator::before{content:""} +`],encapsulation:2,changeDetection:0})}return t})(),W6=new O("mat-datepicker-scroll-strategy",{providedIn:"root",factory:()=>{let t=u(de);return()=>Tn(t)}});function d0e(t){let n=u(de);return()=>Tn(n)}var u0e={provide:W6,deps:[],useFactory:d0e},G6=(()=>{class t{_elementRef=u(Y);_animationsDisabled=Qe();_changeDetectorRef=u(ye);_globalModel=u(k_);_dateAdapter=u($i);_ngZone=u(ae);_rangeSelectionStrategy=u(H6,{optional:!0});_stateChanges;_model;_eventCleanups;_animationFallback;_calendar;color;datepicker;comparisonStart;comparisonEnd;startDateAccessibleName;endDateAccessibleName;_isAbove;_animationDone=new z;_isAnimating=!1;_closeButtonText;_closeButtonFocused;_actionsPortal=null;_dialogLabelId;constructor(){if(u(ft).load(ro),this._closeButtonText=u(gp).closeCalendarLabel,!this._animationsDisabled){let e=this._elementRef.nativeElement,i=u(ze);this._eventCleanups=this._ngZone.runOutsideAngular(()=>[i.listen(e,"animationstart",this._handleAnimationEvent),i.listen(e,"animationend",this._handleAnimationEvent),i.listen(e,"animationcancel",this._handleAnimationEvent)])}}ngAfterViewInit(){this._stateChanges=this.datepicker.stateChanges.subscribe(()=>{this._changeDetectorRef.markForCheck()}),this._calendar.focusActiveCell()}ngOnDestroy(){clearTimeout(this._animationFallback),this._eventCleanups?.forEach(e=>e()),this._stateChanges?.unsubscribe(),this._animationDone.complete()}_handleUserSelection(e){let i=this._model.selection,r=e.value,o=i instanceof Ta;if(o&&this._rangeSelectionStrategy){let a=this._rangeSelectionStrategy.selectionFinished(r,i,e.event);this._model.updateSelection(a,this)}else r&&(o||!this._dateAdapter.sameDate(r,i))&&this._model.add(r);(!this._model||this._model.isComplete())&&!this._actionsPortal&&this.datepicker.close()}_handleUserDragDrop(e){this._model.updateSelection(e.value,this)}_startExitAnimation(){this._elementRef.nativeElement.classList.add("mat-datepicker-content-exit"),this._animationsDisabled?this._animationDone.next():(clearTimeout(this._animationFallback),this._animationFallback=setTimeout(()=>{this._isAnimating||this._animationDone.next()},200))}_handleAnimationEvent=e=>{let i=this._elementRef.nativeElement;e.target!==i||!e.animationName.startsWith("_mat-datepicker-content")||(clearTimeout(this._animationFallback),this._isAnimating=e.type==="animationstart",i.classList.toggle("mat-datepicker-content-animating",this._isAnimating),this._isAnimating||this._animationDone.next())};_getSelected(){return this._model.selection}_applyPendingSelection(){this._model!==this._globalModel&&this._globalModel.updateSelection(this._model.selection,this)}_assignActions(e,i){this._model=e?this._globalModel.clone():this._globalModel,this._actionsPortal=e,i&&this._changeDetectorRef.detectChanges()}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-datepicker-content"]],viewQuery:function(i,r){if(i&1&&ie(dA,5),i&2){let o;j(o=H())&&(r._calendar=o.first)}},hostAttrs:[1,"mat-datepicker-content"],hostVars:6,hostBindings:function(i,r){i&2&&(at(r.color?"mat-"+r.color:""),G("mat-datepicker-content-touch",r.datepicker.touchUi)("mat-datepicker-content-animations-enabled",!r._animationsDisabled))},inputs:{color:"color"},exportAs:["matDatepickerContent"],decls:5,vars:26,consts:[["cdkTrapFocus","","role","dialog",1,"mat-datepicker-content-container"],[3,"yearSelected","monthSelected","viewChanged","_userSelection","_userDragDrop","id","startAt","startView","minDate","maxDate","dateFilter","headerComponent","selected","dateClass","comparisonStart","comparisonEnd","startDateAccessibleName","endDateAccessibleName"],[3,"cdkPortalOutlet"],["type","button","matButton","elevated",1,"mat-datepicker-close-button",3,"focus","blur","click","color"]],template:function(i,r){i&1&&(m(0,"div",0)(1,"mat-calendar",1),S("yearSelected",function(a){return r.datepicker._selectYear(a)})("monthSelected",function(a){return r.datepicker._selectMonth(a)})("viewChanged",function(a){return r.datepicker._viewChanged(a)})("_userSelection",function(a){return r._handleUserSelection(a)})("_userDragDrop",function(a){return r._handleUserDragDrop(a)}),h(),A(2,Jbe,0,0,"ng-template",2),m(3,"button",3),S("focus",function(){return r._closeButtonFocused=!0})("blur",function(){return r._closeButtonFocused=!1})("click",function(){return r.datepicker.close()}),f(4),h()()),i&2&&(G("mat-datepicker-content-container-with-custom-header",r.datepicker.calendarHeaderComponent)("mat-datepicker-content-container-with-actions",r._actionsPortal),X("aria-modal",!0)("aria-labelledby",r._dialogLabelId??void 0),g(),at(r.datepicker.panelClass),v("id",r.datepicker.id)("startAt",r.datepicker.startAt)("startView",r.datepicker.startView)("minDate",r.datepicker._getMinDate())("maxDate",r.datepicker._getMaxDate())("dateFilter",r.datepicker._getDateFilter())("headerComponent",r.datepicker.calendarHeaderComponent)("selected",r._getSelected())("dateClass",r.datepicker.dateClass)("comparisonStart",r.comparisonStart)("comparisonEnd",r.comparisonEnd)("startDateAccessibleName",r.startDateAccessibleName)("endDateAccessibleName",r.endDateAccessibleName),g(),v("cdkPortalOutlet",r._actionsPortal),g(),G("cdk-visually-hidden",!r._closeButtonFocused),v("color",r.color||"primary"),g(),N(r._closeButtonText))},dependencies:[ES,dA,Ir,_t],styles:[`@keyframes _mat-datepicker-content-dropdown-enter{from{opacity:0;transform:scaleY(0.8)}to{opacity:1;transform:none}}@keyframes _mat-datepicker-content-dialog-enter{from{opacity:0;transform:scale(0.8)}to{opacity:1;transform:none}}@keyframes _mat-datepicker-content-exit{from{opacity:1}to{opacity:0}}.mat-datepicker-content{display:block;background-color:var(--mat-datepicker-calendar-container-background-color, var(--mat-sys-surface-container-high));color:var(--mat-datepicker-calendar-container-text-color, var(--mat-sys-on-surface));box-shadow:var(--mat-datepicker-calendar-container-elevation-shadow, 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12));border-radius:var(--mat-datepicker-calendar-container-shape, var(--mat-sys-corner-large))}.mat-datepicker-content.mat-datepicker-content-animations-enabled{animation:_mat-datepicker-content-dropdown-enter 120ms cubic-bezier(0, 0, 0.2, 1)}.mat-datepicker-content .mat-calendar{width:296px;height:354px}.mat-datepicker-content .mat-datepicker-content-container-with-custom-header .mat-calendar{height:auto}.mat-datepicker-content .mat-datepicker-close-button{position:absolute;top:100%;left:0;margin-top:8px}.mat-datepicker-content-animating .mat-datepicker-content .mat-datepicker-close-button{display:none}.mat-datepicker-content-container{display:flex;flex-direction:column;justify-content:space-between}.mat-datepicker-content-touch{display:block;max-height:80vh;box-shadow:var(--mat-datepicker-calendar-container-touch-elevation-shadow, 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12));border-radius:var(--mat-datepicker-calendar-container-touch-shape, var(--mat-sys-corner-extra-large));position:relative;overflow:visible}.mat-datepicker-content-touch.mat-datepicker-content-animations-enabled{animation:_mat-datepicker-content-dialog-enter 150ms cubic-bezier(0, 0, 0.2, 1)}.mat-datepicker-content-touch .mat-datepicker-content-container{min-height:312px;max-height:788px;min-width:250px;max-width:750px}.mat-datepicker-content-touch .mat-calendar{width:100%;height:auto}.mat-datepicker-content-exit.mat-datepicker-content-animations-enabled{animation:_mat-datepicker-content-exit 100ms linear}@media all and (orientation: landscape){.mat-datepicker-content-touch .mat-datepicker-content-container{width:64vh;height:80vh}}@media all and (orientation: portrait){.mat-datepicker-content-touch .mat-datepicker-content-container{width:80vw;height:100vw}.mat-datepicker-content-touch .mat-datepicker-content-container-with-actions{height:115vw}} +`],encapsulation:2,changeDetection:0})}return t})(),V6=(()=>{class t{_injector=u(de);_viewContainerRef=u(st);_dateAdapter=u($i,{optional:!0});_dir=u(Yt,{optional:!0});_model=u(k_);_animationsDisabled=Qe();_scrollStrategy=u(W6);_inputStateChanges=ke.EMPTY;_document=u(_e);calendarHeaderComponent;get startAt(){return this._startAt||(this.datepickerInput?this.datepickerInput.getStartValue():null)}set startAt(e){this._startAt=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e))}_startAt;startView="month";get color(){return this._color||(this.datepickerInput?this.datepickerInput.getThemePalette():void 0)}set color(e){this._color=e}_color;touchUi=!1;get disabled(){return this._disabled===void 0&&this.datepickerInput?this.datepickerInput.disabled:!!this._disabled}set disabled(e){e!==this._disabled&&(this._disabled=e,this.stateChanges.next(void 0))}_disabled;xPosition="start";yPosition="below";restoreFocus=!0;yearSelected=new U;monthSelected=new U;viewChanged=new U(!0);dateClass;openedStream=new U;closedStream=new U;get panelClass(){return this._panelClass}set panelClass(e){this._panelClass=KL(e)}_panelClass;get opened(){return this._opened}set opened(e){e?this.open():this.close()}_opened=!1;id=u(et).getId("mat-datepicker-");_getMinDate(){return this.datepickerInput&&this.datepickerInput.min}_getMaxDate(){return this.datepickerInput&&this.datepickerInput.max}_getDateFilter(){return this.datepickerInput&&this.datepickerInput.dateFilter}_overlayRef;_componentRef;_focusedElementBeforeOpen=null;_backdropHarnessClass=`${this.id}-backdrop`;_actionsPortal;datepickerInput;stateChanges=new z;_changeDetectorRef=u(ye);constructor(){this._dateAdapter,this._model.selectionChanged.subscribe(()=>{this._changeDetectorRef.markForCheck()})}ngOnChanges(e){let i=e.xPosition||e.yPosition;if(i&&!i.firstChange&&this._overlayRef){let r=this._overlayRef.getConfig().positionStrategy;r instanceof Ch&&(this._setConnectedPositions(r),this.opened&&this._overlayRef.updatePosition())}this.stateChanges.next(void 0)}ngOnDestroy(){this._destroyOverlay(),this.close(),this._inputStateChanges.unsubscribe(),this.stateChanges.complete()}select(e){this._model.add(e)}_selectYear(e){this.yearSelected.emit(e)}_selectMonth(e){this.monthSelected.emit(e)}_viewChanged(e){this.viewChanged.emit(e)}registerInput(e){return this.datepickerInput,this._inputStateChanges.unsubscribe(),this.datepickerInput=e,this._inputStateChanges=e.stateChanges.subscribe(()=>this.stateChanges.next(void 0)),this._model}registerActions(e){this._actionsPortal,this._actionsPortal=e,this._componentRef?.instance._assignActions(e,!0)}removeActions(e){e===this._actionsPortal&&(this._actionsPortal=null,this._componentRef?.instance._assignActions(null,!0))}open(){this._opened||this.disabled||this._componentRef?.instance._isAnimating||(this.datepickerInput,this._focusedElementBeforeOpen=So(),this._openOverlay(),this._opened=!0,this.openedStream.emit())}close(){if(!this._opened||this._componentRef?.instance._isAnimating)return;let e=this.restoreFocus&&this._focusedElementBeforeOpen&&typeof this._focusedElementBeforeOpen.focus=="function",i=()=>{this._opened&&(this._opened=!1,this.closedStream.emit())};if(this._componentRef){let{instance:r,location:o}=this._componentRef;r._animationDone.pipe(mt(1)).subscribe(()=>{let a=this._document.activeElement;e&&(!a||a===this._document.activeElement||o.nativeElement.contains(a))&&this._focusedElementBeforeOpen.focus(),this._focusedElementBeforeOpen=null,this._destroyOverlay()}),r._startExitAnimation()}e?setTimeout(i):i()}_applyPendingSelection(){this._componentRef?.instance?._applyPendingSelection()}_forwardContentValues(e){e.datepicker=this,e.color=this.color,e._dialogLabelId=this.datepickerInput.getOverlayLabelId(),e._assignActions(this._actionsPortal,!1)}_openOverlay(){this._destroyOverlay();let e=this.touchUi,i=new ao(G6,this._viewContainerRef),r=this._overlayRef=qr(this._injector,new Gr({positionStrategy:e?this._getDialogStrategy():this._getDropdownStrategy(),hasBackdrop:!0,backdropClass:[e?"cdk-overlay-dark-backdrop":"mat-overlay-transparent-backdrop",this._backdropHarnessClass],direction:this._dir||"ltr",scrollStrategy:e?Tc(this._injector):this._scrollStrategy(),panelClass:`mat-datepicker-${e?"dialog":"popup"}`,disableAnimations:this._animationsDisabled}));this._getCloseStream(r).subscribe(o=>{o&&o.preventDefault(),this.close()}),r.keydownEvents().subscribe(o=>{let a=o.keyCode;(a===38||a===40||a===37||a===39||a===33||a===34)&&o.preventDefault()}),this._componentRef=r.attach(i),this._forwardContentValues(this._componentRef.instance),e||vt(()=>{r.updatePosition()},{injector:this._injector})}_destroyOverlay(){this._overlayRef&&(this._overlayRef.dispose(),this._overlayRef=this._componentRef=null)}_getDialogStrategy(){return Us(this._injector).centerHorizontally().centerVertically()}_getDropdownStrategy(){let e=Ja(this._injector,this.datepickerInput.getConnectedOverlayOrigin()).withTransformOriginOn(".mat-datepicker-content").withFlexibleDimensions(!1).withViewportMargin(8).withLockedPosition();return this._setConnectedPositions(e)}_setConnectedPositions(e){let i=this.xPosition==="end"?"end":"start",r=i==="start"?"end":"start",o=this.yPosition==="above"?"bottom":"top",a=o==="top"?"bottom":"top";return e.withPositions([{originX:i,originY:a,overlayX:i,overlayY:o},{originX:i,originY:o,overlayX:i,overlayY:a},{originX:r,originY:a,overlayX:r,overlayY:o},{originX:r,originY:o,overlayX:r,overlayY:a}])}_getCloseStream(e){let i=["ctrlKey","shiftKey","metaKey"];return it(e.backdropClick(),e.detachments(),e.keydownEvents().pipe(ce(r=>r.keyCode===27&&!Gt(r)||this.datepickerInput&&Gt(r,"altKey")&&r.keyCode===38&&i.every(o=>!Gt(r,o)))))}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,inputs:{calendarHeaderComponent:"calendarHeaderComponent",startAt:"startAt",startView:"startView",color:"color",touchUi:[2,"touchUi","touchUi",B],disabled:[2,"disabled","disabled",B],xPosition:"xPosition",yPosition:"yPosition",restoreFocus:[2,"restoreFocus","restoreFocus",B],dateClass:"dateClass",panelClass:"panelClass",opened:[2,"opened","opened",B]},outputs:{yearSelected:"yearSelected",monthSelected:"monthSelected",viewChanged:"viewChanged",openedStream:"opened",closedStream:"closed"},features:[Oe]})}return t})(),q6=(()=>{class t extends V6{static \u0275fac=(()=>{let e;return function(r){return(e||(e=ge(t)))(r||t)}})();static \u0275cmp=E({type:t,selectors:[["mat-datepicker"]],exportAs:["matDatepicker"],features:[we([j6,{provide:V6,useExisting:t}]),le],decls:0,vars:0,template:function(i,r){},encapsulation:2,changeDetection:0})}return t})(),pp=class{target;targetElement;value;constructor(n,e){this.target=n,this.targetElement=e,this.value=this.target.value}},m0e=(()=>{class t{_elementRef=u(Y);_dateAdapter=u($i,{optional:!0});_dateFormats=u(Vs,{optional:!0});_isInitialized;get value(){return this._model?this._getValueFromModel(this._model.selection):this._pendingValue}set value(e){this._assignValueProgrammatically(e)}_model;get disabled(){return!!this._disabled||this._parentDisabled()}set disabled(e){let i=e,r=this._elementRef.nativeElement;this._disabled!==i&&(this._disabled=i,this.stateChanges.next(void 0)),i&&this._isInitialized&&r.blur&&r.blur()}_disabled;dateChange=new U;dateInput=new U;stateChanges=new z;_onTouched=()=>{};_validatorOnChange=()=>{};_cvaOnChange=()=>{};_valueChangesSubscription=ke.EMPTY;_localeSubscription=ke.EMPTY;_pendingValue;_parseValidator=()=>this._lastValueValid?null:{matDatepickerParse:{text:this._elementRef.nativeElement.value}};_filterValidator=e=>{let i=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e.value));return!i||this._matchesFilter(i)?null:{matDatepickerFilter:!0}};_minValidator=e=>{let i=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e.value)),r=this._getMinDate();return!r||!i||this._dateAdapter.compareDate(r,i)<=0?null:{matDatepickerMin:{min:r,actual:i}}};_maxValidator=e=>{let i=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e.value)),r=this._getMaxDate();return!r||!i||this._dateAdapter.compareDate(r,i)>=0?null:{matDatepickerMax:{max:r,actual:i}}};_getValidators(){return[this._parseValidator,this._minValidator,this._maxValidator,this._filterValidator]}_registerModel(e){this._model=e,this._valueChangesSubscription.unsubscribe(),this._pendingValue&&this._assignValue(this._pendingValue),this._valueChangesSubscription=this._model.selectionChanged.subscribe(i=>{if(this._shouldHandleChangeEvent(i)){let r=this._getValueFromModel(i.selection);this._lastValueValid=this._isValidValue(r),this._cvaOnChange(r),this._onTouched(),this._formatValue(r),this.dateInput.emit(new pp(this,this._elementRef.nativeElement)),this.dateChange.emit(new pp(this,this._elementRef.nativeElement))}})}_lastValueValid=!1;constructor(){this._localeSubscription=this._dateAdapter.localeChanges.subscribe(()=>{this._assignValueProgrammatically(this.value)})}ngAfterViewInit(){this._isInitialized=!0}ngOnChanges(e){h0e(e,this._dateAdapter)&&this.stateChanges.next(void 0)}ngOnDestroy(){this._valueChangesSubscription.unsubscribe(),this._localeSubscription.unsubscribe(),this.stateChanges.complete()}registerOnValidatorChange(e){this._validatorOnChange=e}validate(e){return this._validator?this._validator(e):null}writeValue(e){this._assignValueProgrammatically(e)}registerOnChange(e){this._cvaOnChange=e}registerOnTouched(e){this._onTouched=e}setDisabledState(e){this.disabled=e}_onKeydown(e){let i=["ctrlKey","shiftKey","metaKey"];Gt(e,"altKey")&&e.keyCode===40&&i.every(o=>!Gt(e,o))&&!this._elementRef.nativeElement.readOnly&&(this._openPopup(),e.preventDefault())}_onInput(e){let i=e.target.value,r=this._lastValueValid,o=this._dateAdapter.parse(i,this._dateFormats.parse.dateInput);this._lastValueValid=this._isValidValue(o),o=this._dateAdapter.getValidDateOrNull(o);let a=!this._dateAdapter.sameDate(o,this.value);!o||a?this._cvaOnChange(o):(i&&!this.value&&this._cvaOnChange(o),r!==this._lastValueValid&&this._validatorOnChange()),a&&(this._assignValue(o),this.dateInput.emit(new pp(this,this._elementRef.nativeElement)))}_onChange(){this.dateChange.emit(new pp(this,this._elementRef.nativeElement))}_onBlur(){this.value&&this._formatValue(this.value),this._onTouched()}_formatValue(e){this._elementRef.nativeElement.value=e!=null?this._dateAdapter.format(e,this._dateFormats.display.dateInput):""}_assignValue(e){this._model?(this._assignValueToModel(e),this._pendingValue=null):this._pendingValue=e}_isValidValue(e){return!e||this._dateAdapter.isValid(e)}_parentDisabled(){return!1}_assignValueProgrammatically(e){e=this._dateAdapter.deserialize(e),this._lastValueValid=this._isValidValue(e),e=this._dateAdapter.getValidDateOrNull(e),this._assignValue(e),this._formatValue(e)}_matchesFilter(e){let i=this._getDateFilter();return!i||i(e)}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,inputs:{value:"value",disabled:[2,"disabled","disabled",B]},outputs:{dateChange:"dateChange",dateInput:"dateInput"},features:[Oe]})}return t})();function h0e(t,n){let e=Object.keys(t);for(let i of e){let{previousValue:r,currentValue:o}=t[i];if(n.isDateInstance(r)&&n.isDateInstance(o)){if(!n.sameDate(r,o))return!0}else return!0}return!1}var p0e={provide:dr,useExisting:li(()=>kw),multi:!0},f0e={provide:sa,useExisting:li(()=>kw),multi:!0},kw=(()=>{class t extends m0e{_formField=u(oa,{optional:!0});_closedSubscription=ke.EMPTY;_openedSubscription=ke.EMPTY;set matDatepicker(e){e&&(this._datepicker=e,this._ariaOwns.set(e.opened?e.id:null),this._closedSubscription=e.closedStream.subscribe(()=>{this._onTouched(),this._ariaOwns.set(null)}),this._openedSubscription=e.openedStream.subscribe(()=>{this._ariaOwns.set(e.id)}),this._registerModel(e.registerInput(this)))}_datepicker;_ariaOwns=he(null);get min(){return this._min}set min(e){let i=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e));this._dateAdapter.sameDate(i,this._min)||(this._min=i,this._validatorOnChange())}_min;get max(){return this._max}set max(e){let i=this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(e));this._dateAdapter.sameDate(i,this._max)||(this._max=i,this._validatorOnChange())}_max;get dateFilter(){return this._dateFilter}set dateFilter(e){let i=this._matchesFilter(this.value);this._dateFilter=e,this._matchesFilter(this.value)!==i&&this._validatorOnChange()}_dateFilter;_validator;constructor(){super(),this._validator=Ve.compose(super._getValidators())}getConnectedOverlayOrigin(){return this._formField?this._formField.getConnectedOverlayOrigin():this._elementRef}getOverlayLabelId(){return this._formField?this._formField.getLabelId():this._elementRef.nativeElement.getAttribute("aria-labelledby")}getThemePalette(){return this._formField?this._formField.color:void 0}getStartValue(){return this.value}ngOnDestroy(){super.ngOnDestroy(),this._closedSubscription.unsubscribe(),this._openedSubscription.unsubscribe()}_openPopup(){this._datepicker&&this._datepicker.open()}_getValueFromModel(e){return e}_assignValueToModel(e){this._model&&this._model.updateSelection(e,this)}_getMinDate(){return this._min}_getMaxDate(){return this._max}_getDateFilter(){return this._dateFilter}_shouldHandleChangeEvent(e){return e.source!==this}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["input","matDatepicker",""]],hostAttrs:[1,"mat-datepicker-input"],hostVars:6,hostBindings:function(i,r){i&1&&S("input",function(a){return r._onInput(a)})("change",function(){return r._onChange()})("blur",function(){return r._onBlur()})("keydown",function(a){return r._onKeydown(a)}),i&2&&(pi("disabled",r.disabled),X("aria-haspopup",r._datepicker?"dialog":null)("aria-owns",r._ariaOwns())("min",r.min?r._dateAdapter.toIso8601(r.min):null)("max",r.max?r._dateAdapter.toIso8601(r.max):null)("data-mat-calendar",r._datepicker?r._datepicker.id:null))},inputs:{matDatepicker:"matDatepicker",min:"min",max:"max",dateFilter:[0,"matDatepickerFilter","dateFilter"]},exportAs:["matDatepickerInput"],features:[we([p0e,f0e,{provide:kh,useExisting:t}]),le]})}return t})(),g0e=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matDatepickerToggleIcon",""]]})}return t})(),uA=(()=>{class t{_intl=u(gp);_changeDetectorRef=u(ye);_stateChanges=ke.EMPTY;datepicker;tabIndex;ariaLabel;get disabled(){return this._disabled===void 0&&this.datepicker?this.datepicker.disabled:!!this._disabled}set disabled(e){this._disabled=e}_disabled;disableRipple;_customIcon;_button;constructor(){let e=u(new Li("tabindex"),{optional:!0}),i=Number(e);this.tabIndex=i||i===0?i:null}ngOnChanges(e){e.datepicker&&this._watchStateChanges()}ngOnDestroy(){this._stateChanges.unsubscribe()}ngAfterContentInit(){this._watchStateChanges()}_open(e){this.datepicker&&!this.disabled&&(this.datepicker.open(),e.stopPropagation())}_watchStateChanges(){let e=this.datepicker?this.datepicker.stateChanges:Q(),i=this.datepicker&&this.datepicker.datepickerInput?this.datepicker.datepickerInput.stateChanges:Q(),r=this.datepicker?it(this.datepicker.openedStream,this.datepicker.closedStream):Q();this._stateChanges.unsubscribe(),this._stateChanges=it(this._intl.changes,e,i,r).subscribe(()=>this._changeDetectorRef.markForCheck())}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-datepicker-toggle"]],contentQueries:function(i,r,o){if(i&1&&Ce(o,g0e,5),i&2){let a;j(a=H())&&(r._customIcon=a.first)}},viewQuery:function(i,r){if(i&1&&ie(e0e,5),i&2){let o;j(o=H())&&(r._button=o.first)}},hostAttrs:[1,"mat-datepicker-toggle"],hostVars:8,hostBindings:function(i,r){i&1&&S("click",function(a){return r._open(a)}),i&2&&(X("tabindex",null)("data-mat-calendar",r.datepicker?r.datepicker.id:null),G("mat-datepicker-toggle-active",r.datepicker&&r.datepicker.opened)("mat-accent",r.datepicker&&r.datepicker.color==="accent")("mat-warn",r.datepicker&&r.datepicker.color==="warn"))},inputs:{datepicker:[0,"for","datepicker"],tabIndex:"tabIndex",ariaLabel:[0,"aria-label","ariaLabel"],disabled:[2,"disabled","disabled",B],disableRipple:"disableRipple"},exportAs:["matDatepickerToggle"],features:[Oe],ngContentSelectors:i0e,decls:4,vars:7,consts:[["button",""],["matIconButton","","type","button",3,"tabIndex","disabled","disableRipple"],["viewBox","0 0 24 24","width","24px","height","24px","fill","currentColor","focusable","false","aria-hidden","true",1,"mat-datepicker-toggle-default-icon"],["d","M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"]],template:function(i,r){i&1&&(Ee(t0e),m(0,"button",1,0),L(2,n0e,2,0,":svg:svg",2),ne(3),h()),i&2&&(v("tabIndex",r.disabled?-1:r.tabIndex)("disabled",r.disabled)("disableRipple",r.disableRipple),X("aria-haspopup",r.datepicker?"dialog":null)("aria-label",r.ariaLabel||r._intl.openCalendarLabel)("aria-expanded",r.datepicker?r.datepicker.opened:null),g(2),V(r._customIcon?-1:2))},dependencies:[Ft],styles:[`.mat-datepicker-toggle{pointer-events:auto;color:var(--mat-datepicker-toggle-icon-color, var(--mat-sys-on-surface-variant))}.mat-datepicker-toggle button{color:inherit}.mat-datepicker-toggle-active{color:var(--mat-datepicker-toggle-active-state-icon-color, var(--mat-sys-primary))}@media(forced-colors: active){.mat-datepicker-toggle-default-icon{color:CanvasText}} +`],encapsulation:2,changeDetection:0})}return t})();var Y6=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[gp,u0e],imports:[Fe,cr,El,Oo,De,G6,uA,$6,Tr]})}return t})();function b0e(t,n){t&1&&(m(0,"div",1),M(1,"mat-spinner"),h())}function v0e(t,n){t&1&&(m(0,"mat-error"),f(1," Employee number is required "),h())}function y0e(t,n){t&1&&(m(0,"mat-error"),f(1," First name is required "),h())}function x0e(t,n){t&1&&(m(0,"mat-error"),f(1," Last name is required "),h())}function C0e(t,n){t&1&&(m(0,"mat-error"),f(1," Date of birth is required "),h())}function w0e(t,n){t&1&&(m(0,"mat-error"),f(1," Email is required "),h())}function D0e(t,n){t&1&&(m(0,"mat-error"),f(1," Please enter a valid email "),h())}function M0e(t,n){t&1&&(m(0,"mat-error"),f(1," Phone number is required "),h())}function E0e(t,n){if(t&1&&(m(0,"mat-option",25),f(1),h()),t&2){let e=n.$implicit;v("value",e.value),g(),fe(" ",e.label," ")}}function S0e(t,n){t&1&&(m(0,"mat-error"),f(1," Gender is required "),h())}function k0e(t,n){if(t&1&&(m(0,"mat-option",25),f(1),h()),t&2){let e=n.$implicit;v("value",e.id),g(),fe(" ",e.name," ")}}function T0e(t,n){t&1&&(m(0,"mat-error"),f(1," Department is required "),h())}function I0e(t,n){if(t&1&&(m(0,"mat-option",25),f(1),h()),t&2){let e=n.$implicit;v("value",e.id),g(),fe(" ",e.positionTitle," ")}}function A0e(t,n){t&1&&(m(0,"mat-error"),f(1," Position is required "),h())}function O0e(t,n){t&1&&(m(0,"mat-error"),f(1," Salary is required "),h())}function R0e(t,n){t&1&&(m(0,"mat-error"),f(1," Salary must be greater than 0 "),h())}var mA=(()=>{let n=class n{constructor(){this.fb=u(co),this.router=u(Ae),this.route=u(Ai),this.snackBar=u(_i),this.employeeService=u(rd),this.positionService=u(od),this.departmentService=u(_a),this.loading=!1,this.isEditMode=!1,this.positions=[],this.departments=[],this.genderOptions=[{value:Lu.Male,label:"Male"},{value:Lu.Female,label:"Female"}]}ngOnInit(){this.initForm(),this.loadDependencies(),this.checkEditMode()}initForm(){this.employeeForm=this.fb.group({employeeNumber:["",[Ve.required,Ve.maxLength(50)]],prefix:["",Ve.maxLength(10)],firstName:["",[Ve.required,Ve.maxLength(100)]],middleName:["",Ve.maxLength(100)],lastName:["",[Ve.required,Ve.maxLength(100)]],birthday:[null,Ve.required],gender:[Lu.Male,Ve.required],email:["",[Ve.required,Ve.email,Ve.maxLength(255)]],phone:["",[Ve.required,Ve.maxLength(20)]],salary:[0,[Ve.required,Ve.min(0)]],positionId:["",Ve.required],departmentId:["",Ve.required]})}loadDependencies(){this.departmentService.getAll().subscribe({next:i=>{this.departments=i},error:i=>{console.error("Error loading departments:",i),this.showMessage("Error loading departments")}}),this.positionService.getAll().subscribe({next:i=>{this.positions=i},error:i=>{console.error("Error loading positions:",i),this.showMessage("Error loading positions")}})}checkEditMode(){this.employeeId=this.route.snapshot.paramMap.get("id")||void 0,this.isEditMode=!!this.employeeId,this.isEditMode&&this.employeeId&&this.loadEmployee(this.employeeId)}loadEmployee(i){this.loading=!0,this.employeeService.getById(i).subscribe({next:r=>{this.employeeForm.patchValue({employeeNumber:r.employeeNumber,prefix:r.prefix,firstName:r.firstName,middleName:r.middleName,lastName:r.lastName,birthday:r.birthday||r.dateOfBirth,gender:r.gender,email:r.email,phone:r.phone||r.phoneNumber,salary:r.salary,positionId:r.positionId,departmentId:r.departmentId}),this.loading=!1},error:r=>{console.error("Error loading employee:",r),this.showMessage("Error loading employee"),this.loading=!1}})}onSubmit(){if(this.employeeForm.invalid){this.employeeForm.markAllAsTouched();return}if(this.loading=!0,this.isEditMode&&this.employeeId){let i=I({id:this.employeeId},this.employeeForm.value);this.employeeService.updateEmployee(i).subscribe({next:()=>{this.showMessage("Employee updated successfully"),this.router.navigate(["/employees",this.employeeId])},error:r=>{console.error("Error updating employee:",r),this.showMessage("Error updating employee"),this.loading=!1}})}else{let i=this.employeeForm.value;this.employeeService.createEmployee(i).subscribe({next:r=>{console.log("Employee created - Response:",r),console.log("Employee ID:",r?.id),this.showMessage("Employee created successfully"),r?.id?(console.log("Navigating to detail page:","/employees/"+r.id),this.router.navigate(["/employees",r.id])):(console.warn("No employee ID returned, navigating to list page"),this.router.navigate(["/employees"])),this.loading=!1},error:r=>{console.error("Error creating employee:",r),this.showMessage("Error creating employee"),this.loading=!1}})}}onCancel(){this.router.navigate(["/employees"])}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}getFormTitle(){return this.isEditMode?"Edit Employee":"Create Employee"}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-employee-form"]],decls:94,vars:22,consts:[["dobPicker",""],[1,"loading-overlay"],[3,"ngSubmit","formGroup"],[1,"form-section"],[1,"form-row"],["appearance","outline"],["matInput","","formControlName","employeeNumber"],[4,"ngIf"],["matInput","","formControlName","prefix","placeholder","Mr., Mrs., Dr."],["matInput","","formControlName","firstName"],["matInput","","formControlName","middleName"],["matInput","","formControlName","lastName"],["matInput","","formControlName","birthday",3,"matDatepicker"],["matIconSuffix","",3,"for"],["matInput","","type","email","formControlName","email"],["matInput","","formControlName","phone"],["formControlName","gender"],[3,"value",4,"ngFor","ngForOf"],["formControlName","departmentId"],["formControlName","positionId"],["matInput","","type","number","formControlName","salary"],["matTextPrefix",""],[1,"form-actions"],["type","button","mat-stroked-button","",3,"click"],["type","submit","mat-raised-button","","color","primary",3,"disabled"],[3,"value"]],template:function(r,o){if(r&1){let a=q();M(0,"page-header"),m(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4),h()(),m(5,"mat-card-content"),L(6,b0e,2,0,"div",1),m(7,"form",2),S("ngSubmit",function(){return k(a),T(o.onSubmit())}),m(8,"div",3)(9,"h3"),f(10,"Personal Information"),h(),m(11,"div",4)(12,"mat-form-field",5)(13,"mat-label"),f(14,"Employee Number"),h(),M(15,"input",6),A(16,v0e,2,0,"mat-error",7),h(),m(17,"mat-form-field",5)(18,"mat-label"),f(19,"Prefix"),h(),M(20,"input",8),h()(),m(21,"div",4)(22,"mat-form-field",5)(23,"mat-label"),f(24,"First Name"),h(),M(25,"input",9),A(26,y0e,2,0,"mat-error",7),h(),m(27,"mat-form-field",5)(28,"mat-label"),f(29,"Middle Name"),h(),M(30,"input",10),h()(),m(31,"div",4)(32,"mat-form-field",5)(33,"mat-label"),f(34,"Last Name"),h(),M(35,"input",11),A(36,x0e,2,0,"mat-error",7),h(),m(37,"mat-form-field",5)(38,"mat-label"),f(39,"Date of Birth"),h(),M(40,"input",12)(41,"mat-datepicker-toggle",13)(42,"mat-datepicker",null,0),A(44,C0e,2,0,"mat-error",7),h()(),m(45,"div",4)(46,"mat-form-field",5)(47,"mat-label"),f(48,"Email"),h(),M(49,"input",14),A(50,w0e,2,0,"mat-error",7)(51,D0e,2,0,"mat-error",7),h(),m(52,"mat-form-field",5)(53,"mat-label"),f(54,"Phone Number"),h(),M(55,"input",15),A(56,M0e,2,0,"mat-error",7),h()(),m(57,"div",4)(58,"mat-form-field",5)(59,"mat-label"),f(60,"Gender"),h(),m(61,"mat-select",16),A(62,E0e,2,2,"mat-option",17),h(),A(63,S0e,2,0,"mat-error",7),h()()(),m(64,"div",3)(65,"h3"),f(66,"Employment Information"),h(),m(67,"div",4)(68,"mat-form-field",5)(69,"mat-label"),f(70,"Department"),h(),m(71,"mat-select",18),A(72,k0e,2,2,"mat-option",17),h(),A(73,T0e,2,0,"mat-error",7),h(),m(74,"mat-form-field",5)(75,"mat-label"),f(76,"Position"),h(),m(77,"mat-select",19),A(78,I0e,2,2,"mat-option",17),h(),A(79,A0e,2,0,"mat-error",7),h()(),m(80,"div",4)(81,"mat-form-field",5)(82,"mat-label"),f(83,"Salary"),h(),M(84,"input",20),m(85,"span",21),f(86,"$\xA0"),h(),A(87,O0e,2,0,"mat-error",7)(88,R0e,2,0,"mat-error",7),h()()(),m(89,"div",22)(90,"button",23),S("click",function(){return k(a),T(o.onCancel())}),f(91," Cancel "),h(),m(92,"button",24),f(93),h()()()()()}if(r&2){let a,s,l,c,d,p,_,b,y,w,C,D,F=Te(43);g(4),N(o.getFormTitle()),g(2),V(o.loading?6:-1),g(),v("formGroup",o.employeeForm),g(9),v("ngIf",(a=o.employeeForm.get("employeeNumber"))==null?null:a.hasError("required")),g(10),v("ngIf",(s=o.employeeForm.get("firstName"))==null?null:s.hasError("required")),g(10),v("ngIf",(l=o.employeeForm.get("lastName"))==null?null:l.hasError("required")),g(4),v("matDatepicker",F),g(),v("for",F),g(3),v("ngIf",(c=o.employeeForm.get("birthday"))==null?null:c.hasError("required")),g(6),v("ngIf",(d=o.employeeForm.get("email"))==null?null:d.hasError("required")),g(),v("ngIf",(p=o.employeeForm.get("email"))==null?null:p.hasError("email")),g(5),v("ngIf",(_=o.employeeForm.get("phone"))==null?null:_.hasError("required")),g(6),v("ngForOf",o.genderOptions),g(),v("ngIf",(b=o.employeeForm.get("gender"))==null?null:b.hasError("required")),g(9),v("ngForOf",o.departments),g(),v("ngIf",(y=o.employeeForm.get("departmentId"))==null?null:y.hasError("required")),g(5),v("ngForOf",o.positions),g(),v("ngIf",(w=o.employeeForm.get("positionId"))==null?null:w.hasError("required")),g(8),v("ngIf",(C=o.employeeForm.get("salary"))==null?null:C.hasError("required")),g(),v("ngIf",(D=o.employeeForm.get("salary"))==null?null:D.hasError("min")),g(4),v("disabled",o.loading),g(),fe(" ",o.isEditMode?"Update":"Create"," ")}},dependencies:[Je,Un,Wt,Zn,lo,di,gu,Pt,so,nn,Yr,Fe,_t,It,kt,Tt,Rt,Ot,ai,Xt,gi,Ao,uu,Ka,Bi,Ci,Rc,es,Sn,Y6,q6,kw,uA,nV,Zt,Kt,wi,Lt],styles:["mat-card[_ngcontent-%COMP%]{margin:16px;max-width:1200px}mat-card-header[_ngcontent-%COMP%]{padding:16px;border-bottom:1px solid rgba(0,0,0,.12);margin-bottom:24px}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-content[_ngcontent-%COMP%]{padding:24px;position:relative}.loading-overlay[_ngcontent-%COMP%]{position:absolute;inset:0;background-color:#fffc;display:flex;align-items:center;justify-content:center;z-index:1000}form[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:24px}.form-section[_ngcontent-%COMP%] h3[_ngcontent-%COMP%]{font-size:16px;font-weight:500;margin:0 0 16px;color:#000000de}.form-row[_ngcontent-%COMP%]{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:8px}@media (max-width: 768px){.form-row[_ngcontent-%COMP%]{grid-template-columns:1fr}}mat-form-field[_ngcontent-%COMP%]{width:100%}mat-form-field.full-width[_ngcontent-%COMP%]{grid-column:1/-1}.form-actions[_ngcontent-%COMP%]{display:flex;justify-content:flex-end;gap:12px;margin-top:24px;padding-top:24px;border-top:1px solid rgba(0,0,0,.12)}.form-actions[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{min-width:120px}"]});let t=n;return t})();var Q6=()=>["HRAdmin","Manager"],P0e=()=>[5,10,25,50,100],F0e=()=>["HRAdmin"];function N0e(t,n){if(t&1){let e=q();m(0,"button",14),S("click",function(){k(e);let r=x();return T(r.createDepartment())}),m(1,"mat-icon"),f(2,"add"),h(),f(3," Add Department "),h()}}function L0e(t,n){if(t&1&&(m(0,"mat-option",15),f(1),h()),t&2){let e=n.$implicit;v("value",e),g(),fe(" ",e," ")}}function V0e(t,n){t&1&&(m(0,"div",16),M(1,"mat-spinner"),h())}function B0e(t,n){t&1&&(m(0,"th",25),f(1,"Name"),h())}function j0e(t,n){if(t&1&&(m(0,"td",26),f(1),h()),t&2){let e=n.$implicit;g(),N(e.name)}}function H0e(t,n){t&1&&(m(0,"th",25),f(1,"Actions"),h())}function z0e(t,n){if(t&1){let e=q();m(0,"button",31),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.editDepartment(r))}),m(1,"mat-icon"),f(2,"edit"),h()()}}function U0e(t,n){if(t&1){let e=q();m(0,"button",32),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.deleteDepartment(r))}),m(1,"mat-icon"),f(2,"delete"),h()()}}function $0e(t,n){if(t&1){let e=q();m(0,"td",26)(1,"div",27)(2,"button",28),S("click",function(){let r=k(e).$implicit,o=x(2);return T(o.viewDepartment(r))}),m(3,"mat-icon"),f(4,"visibility"),h()(),A(5,z0e,3,0,"button",29)(6,U0e,3,0,"button",30),h()()}t&2&&(g(5),v("appHasRole",dt(2,Q6)),g(),v("appHasRole",dt(3,F0e)))}function W0e(t,n){t&1&&M(0,"tr",33)}function G0e(t,n){t&1&&M(0,"tr",34)}function q0e(t,n){if(t&1&&(m(0,"tr",35)(1,"td",36)(2,"div",37)(3,"mat-icon"),f(4,"info"),h(),m(5,"p"),f(6,"No departments found"),h()()()()),t&2){let e=x(2);g(),X("colspan",e.displayedColumns.length)}}function Y0e(t,n){if(t&1&&(m(0,"table",17),lt(1,18),A(2,B0e,2,0,"th",19)(3,j0e,2,1,"td",20),ot(),lt(4,21),A(5,H0e,2,0,"th",19)(6,$0e,7,4,"td",20),ot(),A(7,W0e,1,0,"tr",22)(8,G0e,1,0,"tr",23)(9,q0e,7,1,"tr",24),h()),t&2){let e=x();v("dataSource",e.departments),g(7),v("matHeaderRowDef",e.displayedColumns),g(),v("matRowDefColumns",e.displayedColumns),g(),v("ngIf",e.departments.length===0)}}var K6=(()=>{let n=class n{constructor(){this.departmentService=u(_a),this.authService=u(jt),this.router=u(Ae),this.fb=u(co),this.snackBar=u(_i),this.dialog=u(Rn),this.departments=[],this.loading=!1,this.totalCount=0,this.pageSize=10,this.pageNumber=1,this.destroy$=new z,this.displayedColumns=["name","actions"]}ngOnInit(){this.initSearchForm(),this.setupAutocomplete(),this.setupAutoSubmit(),this.loadDepartments()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}initSearchForm(){this.searchForm=this.fb.group({Name:[""]})}setupAutocomplete(){this.filteredNames$=this.searchForm.get("Name").valueChanges.pipe(Ue(""),Dt(300),Nn(),je(i=>this.getAutocompleteOptions("Name",i)))}setupAutoSubmit(){this.searchForm.valueChanges.pipe(Dt(500),Nn((i,r)=>JSON.stringify(i)===JSON.stringify(r)),xe(this.destroy$)).subscribe(()=>{this.pageNumber=1,this.loadDepartments()})}getAutocompleteOptions(i,r){if(!r||r.length<2)return Q([]);let o={PageNumber:1,PageSize:10,[i]:r};return this.departmentService.getAllPaged(o).pipe(se(a=>a.value.map(l=>l.name).filter((l,c,d)=>l&&d.indexOf(l)===c)),ei(()=>Q([])))}loadDepartments(){this.loading=!0;let i=I({PageNumber:this.pageNumber,PageSize:this.pageSize},this.searchForm.value);Object.keys(i).forEach(r=>{(i[r]===""||i[r]===null||i[r]===void 0)&&delete i[r]}),this.departmentService.getAllPaged(i).subscribe({next:r=>{this.departments=r.value,this.totalCount=r.recordsTotal,this.loading=!1},error:r=>{console.error("Error loading departments:",r),this.loading=!1}})}onClearSearch(){this.searchForm.reset(),this.pageNumber=1,this.paginator&&(this.paginator.pageIndex=0),this.loadDepartments()}onPageChange(i){this.pageSize=i.pageSize,this.pageNumber=i.pageIndex+1,this.loadDepartments()}viewDepartment(i){this.router.navigate(["/departments",i.id])}editDepartment(i){this.router.navigate(["/departments","edit",i.id])}deleteDepartment(i){this.dialog.open(Fr,{width:"400px",data:{title:"Delete Department",message:`Are you sure you want to delete "${i.name}"? This action cannot be undone.`,confirmText:"Delete",cancelText:"Cancel"}}).afterClosed().subscribe(o=>{o&&this.departmentService.delete(i.id).subscribe({next:()=>{this.showMessage(`"${i.name}" has been deleted.`),this.loadDepartments()},error:a=>{console.error("Error deleting department:",a),this.showMessage("Failed to delete department. Please try again.")}})})}createDepartment(){this.router.navigate(["/departments","create"])}canEdit(){return this.authService.isHRAdmin()||this.authService.isManager()}canDelete(){return this.authService.isHRAdmin()}canCreate(){return this.authService.isHRAdmin()||this.authService.isManager()}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-department-list"]],viewQuery:function(r,o){if(r&1&&ie(mr,5),r&2){let a;j(a=H())&&(o.paginator=a.first)}},decls:27,vars:14,consts:[["autoName","matAutocomplete"],[1,"flex-spacer"],["mat-raised-button","","color","primary",3,"click",4,"appHasRole"],[1,"search-form",3,"formGroup"],[1,"search-row"],["appearance","outline"],["matInput","","formControlName","Name",3,"matAutocomplete"],[3,"value",4,"ngFor","ngForOf"],[1,"search-buttons"],["mat-raised-button","","color","accent","type","button",3,"click"],[1,"table-container"],["class","loading-spinner",4,"ngIf"],["mat-table","","class","department-table",3,"dataSource",4,"ngIf"],["showFirstLastButtons","",3,"page","length","pageSize","pageSizeOptions","pageIndex"],["mat-raised-button","","color","primary",3,"click"],[3,"value"],[1,"loading-spinner"],["mat-table","",1,"department-table",3,"dataSource"],["matColumnDef","name"],["mat-header-cell","",4,"matHeaderCellDef"],["mat-cell","",4,"matCellDef"],["matColumnDef","actions"],["mat-header-row","",4,"matHeaderRowDef"],["mat-row","",4,"matRowDef","matRowDefColumns"],["class","mat-row",4,"ngIf"],["mat-header-cell",""],["mat-cell",""],[1,"action-buttons"],["mat-icon-button","","color","primary","matTooltip","View Details",3,"click"],["mat-icon-button","","color","accent","matTooltip","Edit Department",3,"click",4,"appHasRole"],["mat-icon-button","","color","warn","matTooltip","Delete Department",3,"click",4,"appHasRole"],["mat-icon-button","","color","accent","matTooltip","Edit Department",3,"click"],["mat-icon-button","","color","warn","matTooltip","Delete Department",3,"click"],["mat-header-row",""],["mat-row",""],[1,"mat-row"],[1,"mat-cell"],[1,"no-data"]],template:function(r,o){if(r&1){let a=q();M(0,"page-header"),m(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4,"Department Directory"),h(),M(5,"div",1),A(6,N0e,4,0,"button",2),h(),m(7,"mat-card-content")(8,"form",3)(9,"div",4)(10,"mat-form-field",5)(11,"mat-label"),f(12,"Department Name"),h(),M(13,"input",6),m(14,"mat-autocomplete",null,0),A(16,L0e,2,2,"mat-option",7),me(17,"async"),h()(),m(18,"div",8)(19,"button",9),S("click",function(){return k(a),T(o.onClearSearch())}),m(20,"mat-icon"),f(21,"clear"),h(),f(22," Clear Filters "),h()()()(),m(23,"div",10),A(24,V0e,2,0,"div",11)(25,Y0e,10,4,"table",12),h(),m(26,"mat-paginator",13),S("page",function(l){return k(a),T(o.onPageChange(l))}),h()()()}if(r&2){let a=Te(15);g(6),v("appHasRole",dt(12,Q6)),g(2),v("formGroup",o.searchForm),g(5),v("matAutocomplete",a),g(3),v("ngForOf",Re(17,10,o.filteredNames$)),g(8),v("ngIf",o.loading),g(),v("ngIf",!o.loading),g(),v("length",o.totalCount)("pageSize",o.pageSize)("pageSizeOptions",dt(13,P0e))("pageIndex",o.pageNumber-1)}},dependencies:[Je,Un,Wt,Zn,lo,di,Pt,so,nn,Yr,Fe,_t,Ft,It,kt,Tt,Rt,Ot,Ge,Ze,Bi,Ci,Xt,gi,ai,ka,ba,ya,Da,xa,va,Ma,Ca,wa,Ea,Sa,Fc,mr,Zt,Kt,An,ur,Mw,Dw,Sn,M_,wi,Pn,Lt,Xn,cn],styles:["mat-card[_ngcontent-%COMP%]{margin:16px}mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:center;margin-bottom:16px;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-header[_ngcontent-%COMP%] .flex-spacer[_ngcontent-%COMP%]{flex:1 1 auto}mat-card-header[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}mat-card-content[_ngcontent-%COMP%]{padding:0}.search-form[_ngcontent-%COMP%]{padding:16px;background-color:#f5f5f5;margin-bottom:0}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%]{display:grid;grid-template-columns:1fr auto;gap:16px;align-items:start}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%] mat-form-field[_ngcontent-%COMP%]{width:100%}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%] .search-buttons[_ngcontent-%COMP%]{display:flex;gap:8px;align-items:center}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%] .search-buttons[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{white-space:nowrap}.search-form[_ngcontent-%COMP%] .search-row[_ngcontent-%COMP%] .search-buttons[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}.table-container[_ngcontent-%COMP%]{position:relative;min-height:300px;overflow-x:auto}.loading-spinner[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:300px}.department-table[_ngcontent-%COMP%]{width:100%}.department-table[_ngcontent-%COMP%] th[_ngcontent-%COMP%]{font-weight:600;font-size:14px;color:#000000de}.department-table[_ngcontent-%COMP%] td[_ngcontent-%COMP%]{font-size:14px;color:#000000de}.department-table[_ngcontent-%COMP%] .mat-column-name[_ngcontent-%COMP%]{min-width:200px}.department-table[_ngcontent-%COMP%] .mat-column-actions[_ngcontent-%COMP%]{width:120px;text-align:right}.department-table[_ngcontent-%COMP%] .mat-column-actions[_ngcontent-%COMP%] .action-buttons[_ngcontent-%COMP%]{display:flex;justify-content:flex-end;align-items:center;gap:4px}.no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;color:#0000008a}.no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:16px}.no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:16px}mat-paginator[_ngcontent-%COMP%]{border-top:1px solid rgba(0,0,0,.12)}"]});let t=n;return t})();var Q0e=()=>["HRAdmin","Manager"],K0e=()=>["HRAdmin"];function Z0e(t,n){if(t&1){let e=q();m(0,"button",10),S("click",function(){k(e);let r=x(2);return T(r.editDepartment())}),m(1,"mat-icon"),f(2,"edit"),h(),f(3," Edit "),h()}}function X0e(t,n){if(t&1){let e=q();m(0,"button",11),S("click",function(){k(e);let r=x(2);return T(r.deleteDepartment())}),m(1,"mat-icon"),f(2,"delete"),h(),f(3," Delete "),h()}}function J0e(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",7),f(2,"Created At"),h(),m(3,"span",8),f(4),me(5,"date"),h()()),t&2){let e=x(3);g(4),N(Ui(5,1,e.department.createdAt,"medium"))}}function eve(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",7),f(2,"Last Modified At"),h(),m(3,"span",8),f(4),me(5,"date"),h()()),t&2){let e=x(3);g(4),N(Ui(5,1,e.department.lastModifiedAt,"medium"))}}function tve(t,n){if(t&1&&(m(0,"div",6)(1,"h3"),f(2,"Audit Information"),h(),M(3,"mat-divider"),m(4,"mat-list"),A(5,J0e,6,4,"mat-list-item",0)(6,eve,6,4,"mat-list-item",0),h()()),t&2){let e=x(2);g(5),v("ngIf",e.department.createdAt),g(),v("ngIf",e.department.lastModifiedAt)}}function ive(t,n){if(t&1){let e=q();m(0,"mat-card")(1,"mat-card-header")(2,"mat-card-title"),f(3,"Department Details"),h(),M(4,"div",1),A(5,Z0e,4,0,"button",2)(6,X0e,4,0,"button",3),m(7,"button",4),S("click",function(){k(e);let r=x();return T(r.goBack())}),m(8,"mat-icon"),f(9,"arrow_back"),h(),f(10," Back to List "),h()(),m(11,"mat-card-content")(12,"div",5)(13,"div",6)(14,"h3"),f(15,"Basic Information"),h(),M(16,"mat-divider"),m(17,"mat-list")(18,"mat-list-item")(19,"span",7),f(20,"Department Name"),h(),m(21,"span",8),f(22),h()()()(),A(23,tve,7,2,"div",9),h()()()}if(t&2){let e=x();g(5),v("appHasRole",dt(4,Q0e)),g(),v("appHasRole",dt(5,K0e)),g(16),N(e.department.name),g(),v("ngIf",e.department.createdAt||e.department.lastModifiedAt)}}function nve(t,n){t&1&&(m(0,"mat-card")(1,"mat-card-content")(2,"div",12),M(3,"mat-spinner"),h()()())}function rve(t,n){if(t&1){let e=q();m(0,"mat-card")(1,"mat-card-content")(2,"div",13)(3,"mat-icon"),f(4,"error"),h(),m(5,"p"),f(6,"Department not found"),h(),m(7,"button",14),S("click",function(){k(e);let r=x();return T(r.goBack())}),f(8," Go Back "),h()()()()}}var Z6=(()=>{let n=class n{constructor(){this.departmentService=u(_a),this.authService=u(jt),this.route=u(Ai),this.router=u(Ae),this.snackBar=u(_i),this.dialog=u(Rn),this.loading=!1}ngOnInit(){let i=this.route.snapshot.paramMap.get("id");i&&this.loadDepartment(i)}loadDepartment(i){this.loading=!0,this.departmentService.getById(i).subscribe({next:r=>{this.department=r,this.loading=!1},error:r=>{console.error("Error loading department:",r),this.showMessage("Error loading department"),this.loading=!1,this.router.navigate(["/departments"])}})}editDepartment(){this.router.navigate(["/departments","edit",this.department.id])}deleteDepartment(){this.dialog.open(Fr,{width:"400px",data:{title:"Delete Department",message:`Are you sure you want to delete "${this.department.name}"? This action cannot be undone.`,confirmText:"Delete",cancelText:"Cancel"}}).afterClosed().subscribe(r=>{r&&this.departmentService.delete(this.department.id).subscribe({next:()=>{let o=this.snackBar.open(`"${this.department.name}" has been deleted.`,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"});o.afterDismissed().subscribe(()=>this.router.navigate(["/departments"])),o.onAction().subscribe(()=>this.router.navigate(["/departments"]))},error:o=>{console.error("Error deleting department:",o),this.showMessage("Failed to delete department. Please try again.")}})})}goBack(){this.router.navigate(["/departments"])}canEdit(){return this.authService.isHRAdmin()||this.authService.isManager()}canDelete(){return this.authService.isHRAdmin()}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-department-detail"]],decls:4,vars:3,consts:[[4,"ngIf"],[1,"flex-spacer"],["mat-raised-button","","color","accent",3,"click",4,"appHasRole"],["mat-raised-button","","color","warn",3,"click",4,"appHasRole"],["mat-stroked-button","",3,"click"],[1,"detail-grid"],[1,"detail-section"],["matListItemTitle",""],["matListItemLine",""],["class","detail-section",4,"ngIf"],["mat-raised-button","","color","accent",3,"click"],["mat-raised-button","","color","warn",3,"click"],[1,"loading-container"],[1,"no-data"],["mat-raised-button","","color","primary",3,"click"]],template:function(r,o){r&1&&(M(0,"page-header"),A(1,ive,24,6,"mat-card",0)(2,nve,4,0,"mat-card",0)(3,rve,9,0,"mat-card",0)),r&2&&(g(),v("ngIf",!o.loading&&o.department),g(),v("ngIf",o.loading),g(),v("ngIf",!o.loading&&!o.department))},dependencies:[Je,Wt,Fe,_t,It,kt,Tt,Rt,Ot,Ge,Ze,fa,Js,pa,Kr,Fu,Hl,Nr,Zt,Kt,wi,Pn,Lt,Xn,Wa],styles:["mat-card[_ngcontent-%COMP%]{margin:16px}mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:center;margin-bottom:16px;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-header[_ngcontent-%COMP%] .flex-spacer[_ngcontent-%COMP%]{flex:1 1 auto}mat-card-header[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{margin-left:8px}mat-card-header[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}mat-card-content[_ngcontent-%COMP%]{padding:16px}.detail-grid[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));gap:24px}.detail-section[_ngcontent-%COMP%] h3[_ngcontent-%COMP%]{margin:0 0 8px;font-size:16px;font-weight:500;color:#000000de}.detail-section[_ngcontent-%COMP%] mat-divider[_ngcontent-%COMP%]{margin-bottom:16px}.detail-section[_ngcontent-%COMP%] mat-list[_ngcontent-%COMP%]{padding:0}.detail-section[_ngcontent-%COMP%] mat-list-item[_ngcontent-%COMP%]{height:auto;min-height:48px;padding:8px 0}.loading-container[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:300px}.no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;color:#0000008a}.no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:16px}.no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0 0 16px;font-size:16px}"]});let t=n;return t})();function ove(t,n){t&1&&(m(0,"div",0),M(1,"mat-spinner"),h())}function ave(t,n){t&1&&(m(0,"mat-error"),f(1," Department name is required "),h())}function sve(t,n){t&1&&(m(0,"mat-error"),f(1," Department name cannot exceed 100 characters "),h())}var hA=(()=>{let n=class n{constructor(){this.departmentService=u(_a),this.route=u(Ai),this.router=u(Ae),this.fb=u(co),this.snackBar=u(_i),this.loading=!1,this.isEditMode=!1}ngOnInit(){this.initForm();let i=this.route.snapshot.paramMap.get("id");i&&(this.isEditMode=!0,this.departmentId=i,this.loadDepartment(i))}initForm(){this.departmentForm=this.fb.group({name:["",[Ve.required,Ve.maxLength(100)]]})}loadDepartment(i){this.loading=!0,this.departmentService.getById(i).subscribe({next:r=>{this.departmentForm.patchValue({name:r.name}),this.loading=!1},error:r=>{console.error("Error loading department:",r),this.showMessage("Error loading department"),this.loading=!1}})}onSubmit(){if(!this.departmentForm.invalid)if(this.loading=!0,this.isEditMode&&this.departmentId){let i={id:this.departmentId,name:this.departmentForm.value.name};this.departmentService.updateDepartment(i).subscribe({next:()=>{this.showMessage("Department updated successfully"),this.router.navigate(["/departments",this.departmentId])},error:r=>{console.error("Error updating department:",r),this.showMessage("Error updating department"),this.loading=!1}})}else{let i={name:this.departmentForm.value.name};this.departmentService.createDepartment(i).subscribe({next:r=>{console.log("Department created - Response:",r),console.log("Department ID:",r?.id),this.showMessage("Department created successfully"),r?.id?(console.log("Navigating to detail page:","/departments/"+r.id),this.router.navigate(["/departments",r.id])):(console.warn("No department ID returned, navigating to list page"),this.router.navigate(["/departments"])),this.loading=!1},error:r=>{console.error("Error creating department:",r),this.showMessage("Error creating department"),this.loading=!1}})}}onCancel(){this.isEditMode&&this.departmentId?this.router.navigate(["/departments",this.departmentId]):this.router.navigate(["/departments"])}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-department-form"]],decls:23,vars:7,consts:[[1,"loading-overlay"],[3,"ngSubmit","formGroup"],[1,"form-section"],[1,"form-row"],["appearance","outline"],["matInput","","formControlName","name"],[4,"ngIf"],[1,"form-actions"],["type","button","mat-stroked-button","",3,"click"],["type","submit","mat-raised-button","","color","primary",3,"disabled"]],template:function(r,o){if(r&1&&(M(0,"page-header"),m(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4),h()(),m(5,"mat-card-content"),L(6,ove,2,0,"div",0),m(7,"form",1),S("ngSubmit",function(){return o.onSubmit()}),m(8,"div",2)(9,"h3"),f(10,"Department Information"),h(),m(11,"div",3)(12,"mat-form-field",4)(13,"mat-label"),f(14,"Department Name"),h(),M(15,"input",5),A(16,ave,2,0,"mat-error",6)(17,sve,2,0,"mat-error",6),h()()(),m(18,"div",7)(19,"button",8),S("click",function(){return o.onCancel()}),f(20," Cancel "),h(),m(21,"button",9),f(22),h()()()()()),r&2){let a,s;g(4),N(o.isEditMode?"Edit Department":"Create Department"),g(2),V(o.loading?6:-1),g(),v("formGroup",o.departmentForm),g(9),v("ngIf",(a=o.departmentForm.get("name"))==null?null:a.hasError("required")),g(),v("ngIf",(s=o.departmentForm.get("name"))==null?null:s.hasError("maxlength")),g(4),v("disabled",o.loading),g(),fe(" ",o.isEditMode?"Update":"Create"," ")}},dependencies:[Je,Wt,Zn,lo,di,Pt,so,nn,Yr,Fe,_t,It,kt,Tt,Rt,Ot,Ge,Bi,Ci,Xt,gi,Ao,ai,Zt,Kt,wi,Lt],styles:["mat-card[_ngcontent-%COMP%]{margin:16px;max-width:800px}mat-card-header[_ngcontent-%COMP%]{padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-content[_ngcontent-%COMP%]{padding:24px;position:relative}.form-section[_ngcontent-%COMP%]{margin-bottom:24px}.form-section[_ngcontent-%COMP%] h3[_ngcontent-%COMP%]{margin:0 0 16px;font-size:16px;font-weight:500;color:#000000de}.form-section[_ngcontent-%COMP%] .form-row[_ngcontent-%COMP%]{display:grid;grid-template-columns:1fr;gap:16px;margin-bottom:16px}.form-section[_ngcontent-%COMP%] .form-row[_ngcontent-%COMP%] mat-form-field[_ngcontent-%COMP%]{width:100%}.form-actions[_ngcontent-%COMP%]{display:flex;justify-content:flex-end;gap:8px;padding-top:16px;border-top:1px solid rgba(0,0,0,.12)}.loading-overlay[_ngcontent-%COMP%]{position:absolute;inset:0;background-color:#fffc;display:flex;align-items:center;justify-content:center;z-index:1000}"]});let t=n;return t})();var lve=["mat-sort-header",""],cve=["*"];function dve(t,n){t&1&&(gt(0,"div",2),ii(),gt(1,"svg",3),ni(2,"path",4),yt()())}var X6=new O("MAT_SORT_DEFAULT_OPTIONS"),$l=(()=>{class t{_defaultOptions;_initializedStream=new ss(1);sortables=new Map;_stateChanges=new z;active;start="asc";get direction(){return this._direction}set direction(e){this._direction=e}_direction="";disableClear;disabled=!1;sortChange=new U;initialized=this._initializedStream;constructor(e){this._defaultOptions=e}register(e){this.sortables.set(e.id,e)}deregister(e){this.sortables.delete(e.id)}sort(e){this.active!=e.id?(this.active=e.id,this.direction=e.start?e.start:this.start):this.direction=this.getNextSortDirection(e),this.sortChange.emit({active:this.active,direction:this.direction})}getNextSortDirection(e){if(!e)return"";let i=e?.disableClear??this.disableClear??!!this._defaultOptions?.disableClear,r=uve(e.start||this.start,i),o=r.indexOf(this.direction)+1;return o>=r.length&&(o=0),r[o]}ngOnInit(){this._initializedStream.next()}ngOnChanges(){this._stateChanges.next()}ngOnDestroy(){this._stateChanges.complete(),this._initializedStream.complete()}static \u0275fac=function(i){return new(i||t)(be(X6,8))};static \u0275dir=P({type:t,selectors:[["","matSort",""]],hostAttrs:[1,"mat-sort"],inputs:{active:[0,"matSortActive","active"],start:[0,"matSortStart","start"],direction:[0,"matSortDirection","direction"],disableClear:[2,"matSortDisableClear","disableClear",B],disabled:[2,"matSortDisabled","disabled",B]},outputs:{sortChange:"matSortChange"},exportAs:["matSort"],features:[Oe]})}return t})();function uve(t,n){let e=["asc","desc"];return t=="desc"&&e.reverse(),n||e.push(""),e}var Tw=(()=>{class t{changes=new z;static \u0275fac=function(i){return new(i||t)};static \u0275prov=R({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function mve(t){return t||new Tw}var hve={provide:Tw,deps:[[new Ds,new dc,Tw]],useFactory:mve},Iw=(()=>{class t{_intl=u(Tw);_sort=u($l,{optional:!0});_columnDef=u("MAT_SORT_HEADER_COLUMN_DEF",{optional:!0});_changeDetectorRef=u(ye);_focusMonitor=u(oi);_elementRef=u(Y);_ariaDescriber=u(nh,{optional:!0});_renderChanges;_animationsDisabled=Qe();_recentlyCleared=he(null);_sortButton;id;arrowPosition="after";start;disabled=!1;get sortActionDescription(){return this._sortActionDescription}set sortActionDescription(e){this._updateSortActionDescription(e)}_sortActionDescription="Sort";disableClear;constructor(){u(ft).load(Oi);let e=u(X6,{optional:!0});this._sort,e?.arrowPosition&&(this.arrowPosition=e?.arrowPosition)}ngOnInit(){!this.id&&this._columnDef&&(this.id=this._columnDef.name),this._sort.register(this),this._renderChanges=it(this._sort._stateChanges,this._sort.sortChange).subscribe(()=>this._changeDetectorRef.markForCheck()),this._sortButton=this._elementRef.nativeElement.querySelector(".mat-sort-header-container"),this._updateSortActionDescription(this._sortActionDescription)}ngAfterViewInit(){this._focusMonitor.monitor(this._elementRef,!0).subscribe(()=>{Promise.resolve().then(()=>this._recentlyCleared.set(null))})}ngOnDestroy(){this._focusMonitor.stopMonitoring(this._elementRef),this._sort.deregister(this),this._renderChanges?.unsubscribe(),this._sortButton&&this._ariaDescriber?.removeDescription(this._sortButton,this._sortActionDescription)}_toggleOnInteraction(){if(!this._isDisabled()){let e=this._isSorted(),i=this._sort.direction;this._sort.sort(this),this._recentlyCleared.set(e&&!this._isSorted()?i:null)}}_handleKeydown(e){(e.keyCode===32||e.keyCode===13)&&(e.preventDefault(),this._toggleOnInteraction())}_isSorted(){return this._sort.active==this.id&&(this._sort.direction==="asc"||this._sort.direction==="desc")}_isDisabled(){return this._sort.disabled||this.disabled}_getAriaSortAttribute(){return this._isSorted()?this._sort.direction=="asc"?"ascending":"descending":"none"}_renderArrow(){return!this._isDisabled()||this._isSorted()}_updateSortActionDescription(e){this._sortButton&&(this._ariaDescriber?.removeDescription(this._sortButton,this._sortActionDescription),this._ariaDescriber?.describe(this._sortButton,e)),this._sortActionDescription=e}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["","mat-sort-header",""]],hostAttrs:[1,"mat-sort-header"],hostVars:3,hostBindings:function(i,r){i&1&&S("click",function(){return r._toggleOnInteraction()})("keydown",function(a){return r._handleKeydown(a)})("mouseleave",function(){return r._recentlyCleared.set(null)}),i&2&&(X("aria-sort",r._getAriaSortAttribute()),G("mat-sort-header-disabled",r._isDisabled()))},inputs:{id:[0,"mat-sort-header","id"],arrowPosition:"arrowPosition",start:"start",disabled:[2,"disabled","disabled",B],sortActionDescription:"sortActionDescription",disableClear:[2,"disableClear","disableClear",B]},exportAs:["matSortHeader"],attrs:lve,ngContentSelectors:cve,decls:4,vars:17,consts:[[1,"mat-sort-header-container","mat-focus-indicator"],[1,"mat-sort-header-content"],[1,"mat-sort-header-arrow"],["viewBox","0 -960 960 960","focusable","false","aria-hidden","true"],["d","M440-240v-368L296-464l-56-56 240-240 240 240-56 56-144-144v368h-80Z"]],template:function(i,r){i&1&&(Ee(),gt(0,"div",0)(1,"div",1),ne(2),yt(),L(3,dve,3,0,"div",2),yt()),i&2&&(G("mat-sort-header-sorted",r._isSorted())("mat-sort-header-position-before",r.arrowPosition==="before")("mat-sort-header-descending",r._sort.direction==="desc")("mat-sort-header-ascending",r._sort.direction==="asc")("mat-sort-header-recently-cleared-ascending",r._recentlyCleared()==="asc")("mat-sort-header-recently-cleared-descending",r._recentlyCleared()==="desc")("mat-sort-header-animations-disabled",r._animationsDisabled),X("tabindex",r._isDisabled()?null:0)("role",r._isDisabled()?null:"button"),g(3),V(r._renderArrow()?3:-1))},styles:[`.mat-sort-header{cursor:pointer}.mat-sort-header-disabled{cursor:default}.mat-sort-header-container{display:flex;align-items:center;letter-spacing:normal;outline:0}[mat-sort-header].cdk-keyboard-focused .mat-sort-header-container,[mat-sort-header].cdk-program-focused .mat-sort-header-container{border-bottom:solid 1px currentColor}.mat-sort-header-container::before{margin:calc(calc(var(--mat-focus-indicator-border-width, 3px) + 2px)*-1)}.mat-sort-header-content{display:flex;align-items:center}.mat-sort-header-position-before{flex-direction:row-reverse}@keyframes _mat-sort-header-recently-cleared-ascending{from{transform:translateY(0);opacity:1}to{transform:translateY(-25%);opacity:0}}@keyframes _mat-sort-header-recently-cleared-descending{from{transform:translateY(0) rotate(180deg);opacity:1}to{transform:translateY(25%) rotate(180deg);opacity:0}}.mat-sort-header-arrow{height:12px;width:12px;position:relative;transition:transform 225ms cubic-bezier(0.4, 0, 0.2, 1),opacity 225ms cubic-bezier(0.4, 0, 0.2, 1);opacity:0;overflow:visible;color:var(--mat-sort-arrow-color, var(--mat-sys-on-surface))}.mat-sort-header.cdk-keyboard-focused .mat-sort-header-arrow,.mat-sort-header.cdk-program-focused .mat-sort-header-arrow,.mat-sort-header:hover .mat-sort-header-arrow{opacity:.54}.mat-sort-header .mat-sort-header-sorted .mat-sort-header-arrow{opacity:1}.mat-sort-header-descending .mat-sort-header-arrow{transform:rotate(180deg)}.mat-sort-header-recently-cleared-ascending .mat-sort-header-arrow{transform:translateY(-25%)}.mat-sort-header-recently-cleared-ascending .mat-sort-header-arrow{transition:none;animation:_mat-sort-header-recently-cleared-ascending 225ms cubic-bezier(0.4, 0, 0.2, 1) forwards}.mat-sort-header-recently-cleared-descending .mat-sort-header-arrow{transition:none;animation:_mat-sort-header-recently-cleared-descending 225ms cubic-bezier(0.4, 0, 0.2, 1) forwards}.mat-sort-header-animations-disabled .mat-sort-header-arrow{transition-duration:0ms;animation-duration:0ms}.mat-sort-header-arrow svg{width:24px;height:24px;fill:currentColor;position:absolute;top:50%;left:50%;margin:-12px 0 0 -12px;transform:translateZ(0)}.mat-sort-header-arrow,[dir=rtl] .mat-sort-header-position-before .mat-sort-header-arrow{margin:0 0 0 6px}.mat-sort-header-position-before .mat-sort-header-arrow,[dir=rtl] .mat-sort-header-arrow{margin:0 6px 0 0} +`],encapsulation:2,changeDetection:0})}return t})(),Aw=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[hve],imports:[De]})}return t})();var eU=()=>["HRAdmin","Manager"],tU=()=>["HRAdmin"];function pve(t,n){if(t&1){let e=q();m(0,"button",14),S("click",function(){k(e);let r=x();return T(r.createPosition())}),m(1,"mat-icon"),f(2,"add"),h(),f(3," Add Position "),h()}}function fve(t,n){if(t&1){let e=q();m(0,"button",15),S("click",function(){k(e);let r=x();return T(r.addMockData())}),m(1,"mat-icon"),f(2,"data_object"),h(),f(3," Add Mock Data "),h()}}function gve(t,n){t&1&&(m(0,"div",16),M(1,"mat-spinner"),h())}function _ve(t,n){t&1&&(m(0,"th",30),f(1,"Position Number"),h())}function bve(t,n){if(t&1&&(m(0,"td",31),f(1),h()),t&2){let e=n.$implicit;g(),N(e.positionNumber)}}function vve(t,n){t&1&&(m(0,"th",30),f(1,"Position Title"),h())}function yve(t,n){if(t&1&&(m(0,"td",31),f(1),h()),t&2){let e=n.$implicit;g(),N(e.positionTitle)}}function xve(t,n){t&1&&(m(0,"th",30),f(1,"Department"),h())}function Cve(t,n){if(t&1&&(m(0,"td",31),f(1),h()),t&2){let e=n.$implicit;g(),N(e.department==null?null:e.department.name)}}function wve(t,n){t&1&&(m(0,"th",30),f(1,"Salary Range"),h())}function Dve(t,n){if(t&1&&(m(0,"td",31),f(1),h()),t&2){let e=n.$implicit;g(),N(e.salaryRange==null?null:e.salaryRange.name)}}function Mve(t,n){t&1&&(m(0,"th",32),f(1,"Actions"),h())}function Eve(t,n){if(t&1){let e=q();m(0,"button",38),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.editPosition(r))}),m(1,"mat-icon"),f(2,"edit"),h()()}}function Sve(t,n){if(t&1){let e=q();m(0,"button",39),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.deletePosition(r))}),m(1,"mat-icon"),f(2,"delete"),h()()}}function kve(t,n){if(t&1){let e=q();m(0,"td",33)(1,"div",34)(2,"button",35),S("click",function(){let r=k(e).$implicit,o=x(2);return T(o.viewPosition(r))}),m(3,"mat-icon"),f(4,"visibility"),h()(),A(5,Eve,3,0,"button",36)(6,Sve,3,0,"button",37),h()()}t&2&&(g(5),v("appHasRole",dt(2,eU)),g(),v("appHasRole",dt(3,tU)))}function Tve(t,n){t&1&&M(0,"tr",40)}function Ive(t,n){t&1&&M(0,"tr",41)}function Ave(t,n){if(t&1&&(m(0,"tr",42)(1,"td",43)(2,"div",44)(3,"mat-icon"),f(4,"work_off"),h(),m(5,"p"),f(6,"No positions found"),h()()()()),t&2){let e=x(2);g(),X("colspan",e.displayedColumns.length)}}function Ove(t,n){if(t&1){let e=q();m(0,"table",17),S("matSortChange",function(r){k(e);let o=x();return T(o.onSortChange(r))}),lt(1,18),A(2,_ve,2,0,"th",19)(3,bve,2,1,"td",20),ot(),lt(4,21),A(5,vve,2,0,"th",19)(6,yve,2,1,"td",20),ot(),lt(7,22),A(8,xve,2,0,"th",19)(9,Cve,2,1,"td",20),ot(),lt(10,23),A(11,wve,2,0,"th",19)(12,Dve,2,1,"td",20),ot(),lt(13,24),A(14,Mve,2,0,"th",25)(15,kve,7,4,"td",26),ot(),A(16,Tve,1,0,"tr",27)(17,Ive,1,0,"tr",28)(18,Ave,7,1,"tr",29),h()}if(t&2){let e=x();v("dataSource",e.positions),g(16),v("matHeaderRowDef",e.displayedColumns),g(),v("matRowDefColumns",e.displayedColumns)}}var iU=(()=>{let n=class n{constructor(){this.positionService=u(od),this.authService=u(jt),this.router=u(Ae),this.dialog=u(Rn),this.snackBar=u(_i),this.searchSubject=new z,this.destroy$=new z,this.positions=[],this.loading=!1,this.displayedColumns=["positionNumber","positionTitle","departmentId","salaryRangeId","actions"],this.totalCount=0,this.pageNumber=1,this.pageSize=10,this.pageSizeOptions=[5,10,25,50,100],this.searchPositionNumber="",this.searchPositionTitle="",this.searchDepartment=""}ngOnInit(){this.searchSubject.pipe(Dt(500)).subscribe(()=>{this.pageNumber=1,this.loadPositions()}),this.loadPositions()}ngAfterViewInit(){}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}loadPositions(){this.loading=!0;let i={pageNumber:this.pageNumber,pageSize:this.pageSize};this.searchPositionNumber&&(i.PositionNumber=this.searchPositionNumber),this.searchPositionTitle&&(i.PositionTitle=this.searchPositionTitle),this.searchDepartment&&(i.Department=this.searchDepartment),this.sort?.active&&this.sort?.direction&&(i.orderBy=`${this.sort.active} ${this.sort.direction}`),this.positionService.getAllPaged(i).subscribe({next:r=>{this.positions=r.value,this.totalCount=r.recordsTotal,this.loading=!1},error:r=>{console.error("Error loading positions:",r),this.loading=!1}})}onSearch(){this.searchSubject.next()}onPageChange(i){this.pageNumber=i.pageIndex+1,this.pageSize=i.pageSize,this.loadPositions()}onSortChange(i){this.loadPositions()}clearFilters(){this.searchPositionNumber="",this.searchPositionTitle="",this.searchDepartment="",this.pageNumber=1,this.loadPositions()}createPosition(){this.router.navigate(["/positions/create"])}viewPosition(i){this.router.navigate(["/positions",i.id])}editPosition(i){this.router.navigate(["/positions/edit",i.id])}deletePosition(i){this.dialog.open(Fr,{width:"400px",data:{title:"Delete Position",message:`Are you sure you want to delete "${i.positionTitle}"? This action cannot be undone.`,confirmText:"Delete",cancelText:"Cancel"}}).afterClosed().subscribe(o=>{o&&this.positionService.delete(i.id).subscribe({next:()=>{this.showMessage(`"${i.positionTitle}" has been deleted.`),this.loadPositions()},error:a=>{console.error("Error deleting position:",a),this.showMessage("Failed to delete position. Please try again.")}})})}addMockData(){let i=prompt("How many mock positions would you like to add?","10");if(i){let r=parseInt(i,10);r>0&&r<=100?(this.loading=!0,this.positionService.addMockPositions({rowCount:r}).subscribe({next:()=>{this.showMessage(`${r} mock positions added successfully`),this.loadPositions()},error:o=>{console.error("Error adding mock positions:",o),this.showMessage("Error adding mock positions"),this.loading=!1}})):this.showMessage("Please enter a number between 1 and 100")}}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}canEdit(){return this.authService.hasRole("HRAdmin")||this.authService.hasRole("Manager")}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-position-list"]],viewQuery:function(r,o){if(r&1&&(ie(mr,5),ie($l,5)),r&2){let a;j(a=H())&&(o.paginator=a.first),j(a=H())&&(o.sort=a.first)}},decls:36,vars:13,consts:[[1,"flex-spacer"],["mat-raised-button","","color","primary",3,"click",4,"appHasRole"],["mat-stroked-button","","color","accent","style","margin-left: 8px;",3,"click",4,"appHasRole"],[1,"search-filters",2,"margin-bottom","16px","display","flex","gap","16px","flex-wrap","wrap"],["appearance","outline",2,"flex","1","min-width","200px"],["matInput","","placeholder","Search by position number",3,"ngModelChange","input","ngModel"],["matSuffix",""],["matInput","","placeholder","Search by position title",3,"ngModelChange","input","ngModel"],["matInput","","placeholder","Search by department",3,"ngModelChange","input","ngModel"],["mat-stroked-button","",2,"height","56px",3,"click"],[1,"table-container"],["class","loading-spinner",4,"ngIf"],["mat-table","","matSort","","class","position-table",3,"dataSource","matSortChange",4,"ngIf"],["showFirstLastButtons","",3,"page","length","pageSize","pageSizeOptions","pageIndex"],["mat-raised-button","","color","primary",3,"click"],["mat-stroked-button","","color","accent",2,"margin-left","8px",3,"click"],[1,"loading-spinner"],["mat-table","","matSort","",1,"position-table",3,"matSortChange","dataSource"],["matColumnDef","positionNumber"],["mat-header-cell","","mat-sort-header","",4,"matHeaderCellDef"],["mat-cell","",4,"matCellDef"],["matColumnDef","positionTitle"],["matColumnDef","departmentId"],["matColumnDef","salaryRangeId"],["matColumnDef","actions"],["mat-header-cell","","class","mat-column-actions",4,"matHeaderCellDef"],["mat-cell","","class","mat-column-actions",4,"matCellDef"],["mat-header-row","",4,"matHeaderRowDef"],["mat-row","",4,"matRowDef","matRowDefColumns"],["class","mat-row",4,"matNoDataRow"],["mat-header-cell","","mat-sort-header",""],["mat-cell",""],["mat-header-cell","",1,"mat-column-actions"],["mat-cell","",1,"mat-column-actions"],[1,"action-buttons"],["mat-icon-button","","color","primary","matTooltip","View Details",3,"click"],["mat-icon-button","","color","accent","matTooltip","Edit Position",3,"click",4,"appHasRole"],["mat-icon-button","","color","warn","matTooltip","Delete Position",3,"click",4,"appHasRole"],["mat-icon-button","","color","accent","matTooltip","Edit Position",3,"click"],["mat-icon-button","","color","warn","matTooltip","Delete Position",3,"click"],["mat-header-row",""],["mat-row",""],[1,"mat-row"],[1,"mat-cell"],[1,"no-data"]],template:function(r,o){r&1&&(M(0,"page-header"),m(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4,"Positions"),h(),M(5,"span",0),A(6,pve,4,0,"button",1)(7,fve,4,0,"button",2),h(),m(8,"mat-card-content")(9,"div",3)(10,"mat-form-field",4)(11,"mat-label"),f(12,"Position Number"),h(),m(13,"input",5),fn("ngModelChange",function(s){return Mn(o.searchPositionNumber,s)||(o.searchPositionNumber=s),s}),S("input",function(){return o.onSearch()}),h(),m(14,"mat-icon",6),f(15,"search"),h()(),m(16,"mat-form-field",4)(17,"mat-label"),f(18,"Position Title"),h(),m(19,"input",7),fn("ngModelChange",function(s){return Mn(o.searchPositionTitle,s)||(o.searchPositionTitle=s),s}),S("input",function(){return o.onSearch()}),h(),m(20,"mat-icon",6),f(21,"search"),h()(),m(22,"mat-form-field",4)(23,"mat-label"),f(24,"Department"),h(),m(25,"input",8),fn("ngModelChange",function(s){return Mn(o.searchDepartment,s)||(o.searchDepartment=s),s}),S("input",function(){return o.onSearch()}),h(),m(26,"mat-icon",6),f(27,"search"),h()(),m(28,"button",9),S("click",function(){return o.clearFilters()}),m(29,"mat-icon"),f(30,"clear"),h(),f(31," Clear Filters "),h()(),m(32,"div",10),A(33,gve,2,0,"div",11)(34,Ove,19,3,"table",12),h(),m(35,"mat-paginator",13),S("page",function(s){return o.onPageChange(s)}),h()()()),r&2&&(g(6),v("appHasRole",dt(11,eU)),g(),v("appHasRole",dt(12,tU)),g(6),pn("ngModel",o.searchPositionNumber),g(6),pn("ngModel",o.searchPositionTitle),g(6),pn("ngModel",o.searchDepartment),g(8),v("ngIf",o.loading),g(),v("ngIf",!o.loading),g(),v("length",o.totalCount)("pageSize",o.pageSize)("pageSizeOptions",o.pageSizeOptions)("pageIndex",o.pageNumber-1))},dependencies:[Je,Wt,Fe,_t,Ft,It,kt,Tt,Rt,Ot,Ge,Ze,ka,ba,ya,Da,xa,va,Ma,Ca,wa,Ea,Sa,ww,Fc,mr,Aw,$l,Iw,Zt,Kt,An,ur,Pn,ai,Xt,gi,Ka,Bi,Ci,wi,Qr,di,Pt,Ro,Lt,Xn],styles:["mat-card[_ngcontent-%COMP%]{margin:16px}.nl-search-card[_ngcontent-%COMP%]{margin:16px 16px 0}.nl-search-card[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{display:flex;align-items:center;font-size:18px}.nl-parsed-hint[_ngcontent-%COMP%]{font-size:13px;color:#0000008a}.nl-error[_ngcontent-%COMP%]{color:#f44336;font-size:13px;margin-top:8px}mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:center;margin-bottom:16px;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-header[_ngcontent-%COMP%] .flex-spacer[_ngcontent-%COMP%]{flex:1 1 auto}mat-card-header[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}mat-card-content[_ngcontent-%COMP%]{padding:0}.table-container[_ngcontent-%COMP%]{position:relative;min-height:300px;overflow-x:auto}.loading-spinner[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:300px}.position-table[_ngcontent-%COMP%]{width:100%}.position-table[_ngcontent-%COMP%] th[_ngcontent-%COMP%]{font-weight:600;font-size:14px;color:#000000de}.position-table[_ngcontent-%COMP%] td[_ngcontent-%COMP%]{font-size:14px;color:#000000de}.position-table[_ngcontent-%COMP%] .mat-column-positionNumber[_ngcontent-%COMP%]{min-width:150px}.position-table[_ngcontent-%COMP%] .mat-column-positionTitle[_ngcontent-%COMP%]{min-width:200px}.position-table[_ngcontent-%COMP%] .mat-column-departmentId[_ngcontent-%COMP%], .position-table[_ngcontent-%COMP%] .mat-column-salaryRangeId[_ngcontent-%COMP%]{min-width:150px}.position-table[_ngcontent-%COMP%] .mat-column-actions[_ngcontent-%COMP%]{width:150px;text-align:right}.position-table[_ngcontent-%COMP%] .action-buttons[_ngcontent-%COMP%]{display:flex;justify-content:flex-end;gap:4px}.position-table[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{margin-left:4px}.no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;color:#0000008a}.no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:16px}.no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:16px}mat-paginator[_ngcontent-%COMP%]{border-top:1px solid rgba(0,0,0,.12)}"]});let t=n;return t})();var nU=()=>["HRAdmin","Manager"];function Rve(t,n){t&1&&(m(0,"div",2),M(1,"mat-spinner"),h())}function Pve(t,n){if(t&1){let e=q();m(0,"button",9),S("click",function(){k(e);let r=x(2);return T(r.editPosition())}),m(1,"mat-icon"),f(2,"edit"),h()()}}function Fve(t,n){if(t&1){let e=q();m(0,"button",10),S("click",function(){k(e);let r=x(2);return T(r.deletePosition())}),m(1,"mat-icon"),f(2,"delete"),h()()}}function Nve(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",7),f(2,"Description:"),h(),m(3,"span",8),f(4),h()()),t&2){let e=x(2);g(4),N(e.position.positionDescription)}}function Lve(t,n){t&1&&M(0,"mat-divider")}function Vve(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",7),f(2,"Created:"),h(),m(3,"span",8),f(4),me(5,"date"),h()()),t&2){let e=x(2);g(4),N(Ui(5,1,e.position.createdAt,"medium"))}}function Bve(t,n){t&1&&M(0,"mat-divider")}function jve(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",7),f(2,"Last Modified:"),h(),m(3,"span",8),f(4),me(5,"date"),h()()),t&2){let e=x(2);g(4),N(Ui(5,1,e.position.lastModifiedAt,"medium"))}}function Hve(t,n){if(t&1){let e=q();m(0,"mat-card")(1,"mat-card-header")(2,"mat-card-title"),f(3),h(),m(4,"div",3),A(5,Pve,3,0,"button",4)(6,Fve,3,0,"button",5),m(7,"button",6),S("click",function(){k(e);let r=x();return T(r.goBack())}),m(8,"mat-icon"),f(9,"arrow_back"),h()()()(),m(10,"mat-card-content")(11,"mat-list")(12,"mat-list-item")(13,"span",7),f(14,"Position Number:"),h(),m(15,"span",8),f(16),h()(),M(17,"mat-divider"),m(18,"mat-list-item")(19,"span",7),f(20,"Position Title:"),h(),m(21,"span",8),f(22),h()(),M(23,"mat-divider"),A(24,Nve,5,1,"mat-list-item",1)(25,Lve,1,0,"mat-divider",1),m(26,"mat-list-item")(27,"span",7),f(28,"Department:"),h(),m(29,"span",8),f(30),h()(),M(31,"mat-divider"),m(32,"mat-list-item")(33,"span",7),f(34,"Salary Range:"),h(),m(35,"span",8),f(36),h()(),M(37,"mat-divider"),A(38,Vve,6,4,"mat-list-item",1)(39,Bve,1,0,"mat-divider",1)(40,jve,6,4,"mat-list-item",1),h()()()}if(t&2){let e=x();g(3),N(e.position.positionTitle),g(2),v("appHasRole",dt(12,nU)),g(),v("appHasRole",dt(13,nU)),g(10),N(e.position.positionNumber),g(6),N(e.position.positionTitle),g(2),v("ngIf",e.position.positionDescription),g(),v("ngIf",e.position.positionDescription),g(5),N((e.position.department==null?null:e.position.department.name)||e.position.departmentId),g(6),N((e.position.salaryRange==null?null:e.position.salaryRange.name)||e.position.salaryRangeId),g(2),v("ngIf",e.position.createdAt),g(),v("ngIf",e.position.createdAt),g(),v("ngIf",e.position.lastModifiedAt)}}function zve(t,n){t&1&&(m(0,"mat-card")(1,"mat-card-content")(2,"p"),f(3,"Position not found."),h()()())}var rU=(()=>{let n=class n{constructor(){this.positionService=u(od),this.authService=u(jt),this.route=u(Ai),this.router=u(Ae),this.snackBar=u(_i),this.dialog=u(Rn),this.loading=!1}ngOnInit(){let i=this.route.snapshot.paramMap.get("id");i&&this.loadPosition(i)}loadPosition(i){this.loading=!0,this.positionService.getById(i).subscribe({next:r=>{this.position=r,this.loading=!1},error:r=>{console.error("Error loading position:",r),this.showMessage("Error loading position"),this.loading=!1,this.router.navigate(["/positions"])}})}editPosition(){this.router.navigate(["/positions","edit",this.position.id])}deletePosition(){this.dialog.open(Fr,{width:"400px",data:{title:"Delete Position",message:`Are you sure you want to delete "${this.position.positionTitle}"? This action cannot be undone.`,confirmText:"Delete",cancelText:"Cancel"}}).afterClosed().subscribe(r=>{r&&this.positionService.delete(this.position.id).subscribe({next:()=>{let o=this.snackBar.open(`"${this.position.positionTitle}" has been deleted.`,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"});o.afterDismissed().subscribe(()=>this.router.navigate(["/positions"])),o.onAction().subscribe(()=>this.router.navigate(["/positions"]))},error:o=>{console.error("Error deleting position:",o),this.showMessage("Failed to delete position. Please try again.")}})})}goBack(){this.router.navigate(["/positions"])}canEdit(){return this.authService.isHRAdmin()||this.authService.isManager()}canDelete(){return this.authService.isHRAdmin()||this.authService.isManager()}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-position-detail"]],decls:4,vars:3,consts:[["class","loading-spinner",4,"ngIf"],[4,"ngIf"],[1,"loading-spinner"],[1,"header-actions"],["mat-icon-button","","color","primary","matTooltip","Edit Position",3,"click",4,"appHasRole"],["mat-icon-button","","color","warn","matTooltip","Delete Position",3,"click",4,"appHasRole"],["mat-icon-button","","matTooltip","Back to List",3,"click"],[1,"label"],[1,"value"],["mat-icon-button","","color","primary","matTooltip","Edit Position",3,"click"],["mat-icon-button","","color","warn","matTooltip","Delete Position",3,"click"]],template:function(r,o){r&1&&(M(0,"page-header"),A(1,Rve,2,0,"div",0)(2,Hve,41,14,"mat-card",1)(3,zve,4,0,"mat-card",1)),r&2&&(g(),v("ngIf",o.loading),g(),v("ngIf",!o.loading&&o.position),g(),v("ngIf",!o.loading&&!o.position))},dependencies:[Je,Wt,Fe,Ft,It,kt,Tt,Rt,Ot,Ge,Ze,fa,Js,pa,Kr,Nr,Zt,Kt,wi,Pn,Lt,Xn,Wa],styles:[".loading-spinner[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:400px}mat-card[_ngcontent-%COMP%]{margin:16px;max-width:800px}mat-card-header[_ngcontent-%COMP%]{padding:16px;border-bottom:1px solid rgba(0,0,0,.12);display:flex;justify-content:space-between;align-items:center}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:24px;font-weight:500;margin:0}mat-card-header[_ngcontent-%COMP%] .header-actions[_ngcontent-%COMP%]{display:flex;gap:8px}mat-card-content[_ngcontent-%COMP%]{padding:0}mat-list[_ngcontent-%COMP%]{padding:0}mat-list-item[_ngcontent-%COMP%]{height:auto!important;min-height:48px;padding:12px 16px;display:flex;justify-content:space-between;align-items:flex-start}mat-list-item[_ngcontent-%COMP%] .label[_ngcontent-%COMP%]{font-weight:500;color:#0009;min-width:180px}mat-list-item[_ngcontent-%COMP%] .value[_ngcontent-%COMP%]{flex:1;text-align:right;word-break:break-word}"]});let t=n;return t})();function Uve(t,n){t&1&&(m(0,"div",0),M(1,"mat-spinner"),h())}function $ve(t,n){t&1&&(m(0,"mat-error"),f(1," Position title is required "),h())}function Wve(t,n){t&1&&(m(0,"mat-error"),f(1," Position title cannot exceed 100 characters "),h())}function Gve(t,n){t&1&&(m(0,"mat-error"),f(1," Position number is required "),h())}function qve(t,n){t&1&&(m(0,"mat-error"),f(1," Position number cannot exceed 50 characters "),h())}function Yve(t,n){t&1&&(m(0,"mat-error"),f(1," Position description cannot exceed 500 characters "),h())}function Qve(t,n){if(t&1&&(m(0,"mat-option",14),f(1),h()),t&2){let e=n.$implicit;v("value",e.id),g(),fe(" ",e.name," ")}}function Kve(t,n){t&1&&(m(0,"mat-error"),f(1," Department is required "),h())}function Zve(t,n){if(t&1&&(m(0,"mat-option",14),f(1),me(2,"number"),me(3,"number"),h()),t&2){let e=n.$implicit;v("value",e.id),g(),Sm(" ",e.name," ($",Ui(2,4,e.minSalary,"1.0-0")," - $",Ui(3,7,e.maxSalary,"1.0-0"),") ")}}function Xve(t,n){t&1&&(m(0,"mat-error"),f(1," Salary range is required "),h())}var pA=(()=>{let n=class n{constructor(){this.fb=u(co),this.router=u(Ae),this.route=u(Ai),this.snackBar=u(_i),this.positionService=u(od),this.departmentService=u(_a),this.salaryRangeService=u(ad),this.loading=!1,this.isEditMode=!1,this.departments=[],this.salaryRanges=[]}ngOnInit(){this.initForm(),this.loadDepartments(),this.loadSalaryRanges(),this.checkEditMode()}initForm(){this.positionForm=this.fb.group({positionTitle:["",[Ve.required,Ve.maxLength(100)]],positionNumber:["",[Ve.required,Ve.maxLength(50)]],positionDescription:["",[Ve.maxLength(500)]],departmentId:["",Ve.required],salaryRangeId:["",Ve.required]})}loadDepartments(){this.departmentService.getAll().subscribe({next:i=>{this.departments=i},error:i=>{console.error("Error loading departments:",i),this.showMessage("Error loading departments")}})}loadSalaryRanges(){this.salaryRangeService.getAll().subscribe({next:i=>{this.salaryRanges=i},error:i=>{console.error("Error loading salary ranges:",i),this.showMessage("Error loading salary ranges")}})}checkEditMode(){this.positionId=this.route.snapshot.paramMap.get("id")||void 0,this.isEditMode=!!this.positionId,this.isEditMode&&this.positionId&&this.loadPosition(this.positionId)}loadPosition(i){this.loading=!0,this.positionService.getById(i).subscribe({next:r=>{this.positionForm.patchValue({positionTitle:r.positionTitle,positionNumber:r.positionNumber,positionDescription:r.positionDescription,departmentId:r.departmentId,salaryRangeId:r.salaryRangeId}),this.loading=!1},error:r=>{console.error("Error loading position:",r),this.showMessage("Error loading position"),this.loading=!1}})}onSubmit(){if(this.positionForm.invalid){this.positionForm.markAllAsTouched();return}if(this.loading=!0,this.isEditMode&&this.positionId){let i=I({id:this.positionId},this.positionForm.value);this.positionService.updatePosition(i).subscribe({next:()=>{this.showMessage("Position updated successfully"),this.router.navigate(["/positions",this.positionId])},error:r=>{console.error("Error updating position:",r),this.showMessage("Error updating position"),this.loading=!1}})}else{let i=this.positionForm.value;this.positionService.createPosition(i).subscribe({next:r=>{console.log("Position created - Response:",r),console.log("Position ID:",r?.id),this.showMessage("Position created successfully"),r?.id?(console.log("Navigating to detail page:","/positions/"+r.id),this.router.navigate(["/positions",r.id])):(console.warn("No position ID returned, navigating to list page"),this.router.navigate(["/positions"])),this.loading=!1},error:r=>{console.error("Error creating position:",r),this.showMessage("Error creating position"),this.loading=!1}})}}onCancel(){this.router.navigate(["/positions"])}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}getFormTitle(){return this.isEditMode?"Edit Position":"Create Position"}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-position-form"]],decls:43,vars:14,consts:[[1,"loading-overlay"],[3,"ngSubmit","formGroup"],[1,"form-section"],["appearance","outline",1,"full-width"],["matInput","","formControlName","positionTitle"],[4,"ngIf"],["matInput","","formControlName","positionNumber"],["matInput","","formControlName","positionDescription","rows","4"],["formControlName","departmentId"],[3,"value",4,"ngFor","ngForOf"],["formControlName","salaryRangeId"],[1,"form-actions"],["type","button","mat-stroked-button","",3,"click"],["type","submit","mat-raised-button","","color","primary",3,"disabled"],[3,"value"]],template:function(r,o){if(r&1&&(M(0,"page-header"),m(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4),h()(),m(5,"mat-card-content"),L(6,Uve,2,0,"div",0),m(7,"form",1),S("ngSubmit",function(){return o.onSubmit()}),m(8,"div",2)(9,"mat-form-field",3)(10,"mat-label"),f(11,"Position Title"),h(),M(12,"input",4),A(13,$ve,2,0,"mat-error",5)(14,Wve,2,0,"mat-error",5),h(),m(15,"mat-form-field",3)(16,"mat-label"),f(17,"Position Number"),h(),M(18,"input",6),A(19,Gve,2,0,"mat-error",5)(20,qve,2,0,"mat-error",5),h(),m(21,"mat-form-field",3)(22,"mat-label"),f(23,"Position Description"),h(),M(24,"textarea",7),A(25,Yve,2,0,"mat-error",5),h(),m(26,"mat-form-field",3)(27,"mat-label"),f(28,"Department"),h(),m(29,"mat-select",8),A(30,Qve,2,2,"mat-option",9),h(),A(31,Kve,2,0,"mat-error",5),h(),m(32,"mat-form-field",3)(33,"mat-label"),f(34,"Salary Range"),h(),m(35,"mat-select",10),A(36,Zve,4,10,"mat-option",9),h(),A(37,Xve,2,0,"mat-error",5),h()(),m(38,"div",11)(39,"button",12),S("click",function(){return o.onCancel()}),f(40," Cancel "),h(),m(41,"button",13),f(42),h()()()()()),r&2){let a,s,l,c,d,p,_;g(4),N(o.getFormTitle()),g(2),V(o.loading?6:-1),g(),v("formGroup",o.positionForm),g(6),v("ngIf",(a=o.positionForm.get("positionTitle"))==null?null:a.hasError("required")),g(),v("ngIf",(s=o.positionForm.get("positionTitle"))==null?null:s.hasError("maxlength")),g(5),v("ngIf",(l=o.positionForm.get("positionNumber"))==null?null:l.hasError("required")),g(),v("ngIf",(c=o.positionForm.get("positionNumber"))==null?null:c.hasError("maxlength")),g(5),v("ngIf",(d=o.positionForm.get("positionDescription"))==null?null:d.hasError("maxlength")),g(5),v("ngForOf",o.departments),g(),v("ngIf",(p=o.positionForm.get("departmentId"))==null?null:p.hasError("required")),g(5),v("ngForOf",o.salaryRanges),g(),v("ngIf",(_=o.positionForm.get("salaryRangeId"))==null?null:_.hasError("required")),g(4),v("disabled",o.loading),g(),fe(" ",o.isEditMode?"Update":"Create"," ")}},dependencies:[Je,Un,Wt,Zn,lo,di,Pt,so,nn,Yr,Fe,_t,It,kt,Tt,Rt,Ot,ai,Xt,gi,Ao,Bi,Ci,Rc,es,Sn,Zt,Kt,wi,Lt,xf],styles:["mat-card[_ngcontent-%COMP%]{margin:16px;max-width:800px}mat-card-header[_ngcontent-%COMP%]{margin-bottom:16px;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-content[_ngcontent-%COMP%]{padding:16px;position:relative}.loading-overlay[_ngcontent-%COMP%]{position:absolute;inset:0;background-color:#fffc;display:flex;align-items:center;justify-content:center;z-index:1000}.form-section[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:16px;margin-bottom:24px}.form-section[_ngcontent-%COMP%] .full-width[_ngcontent-%COMP%]{width:100%}.form-actions[_ngcontent-%COMP%]{display:flex;gap:12px;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(0,0,0,.12)}"]});let t=n;return t})();var oU=()=>["HRAdmin","Manager"],Jve=()=>[5,10,25,50],eye=()=>["HRAdmin"];function tye(t,n){if(t&1){let e=q();m(0,"button",6),S("click",function(){k(e);let r=x();return T(r.createSalaryRange())}),m(1,"mat-icon"),f(2,"add"),h(),f(3," Add Salary Range "),h()}}function iye(t,n){t&1&&(m(0,"div",7),M(1,"mat-spinner"),h())}function nye(t,n){t&1&&(m(0,"th",20),f(1,"Range Name"),h())}function rye(t,n){if(t&1&&(m(0,"td",21),f(1),h()),t&2){let e=n.$implicit;g(),N(e.name)}}function oye(t,n){t&1&&(m(0,"th",20),f(1,"Minimum Salary"),h())}function aye(t,n){if(t&1&&(m(0,"td",21),f(1),me(2,"currency"),h()),t&2){let e=n.$implicit;g(),N(Tm(2,1,e.minSalary,"USD","symbol","1.0-0"))}}function sye(t,n){t&1&&(m(0,"th",20),f(1,"Maximum Salary"),h())}function lye(t,n){if(t&1&&(m(0,"td",21),f(1),me(2,"currency"),h()),t&2){let e=n.$implicit;g(),N(Tm(2,1,e.maxSalary,"USD","symbol","1.0-0"))}}function cye(t,n){t&1&&(m(0,"th",22),f(1,"Actions"),h())}function dye(t,n){if(t&1){let e=q();m(0,"button",28),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.editSalaryRange(r))}),m(1,"mat-icon"),f(2,"edit"),h()()}}function uye(t,n){if(t&1){let e=q();m(0,"button",29),S("click",function(){k(e);let r=x().$implicit,o=x(2);return T(o.deleteSalaryRange(r))}),m(1,"mat-icon"),f(2,"delete"),h()()}}function mye(t,n){if(t&1){let e=q();m(0,"td",23)(1,"div",24)(2,"button",25),S("click",function(){let r=k(e).$implicit,o=x(2);return T(o.viewSalaryRange(r))}),m(3,"mat-icon"),f(4,"visibility"),h()(),A(5,dye,3,0,"button",26)(6,uye,3,0,"button",27),h()()}t&2&&(g(5),v("appHasRole",dt(2,oU)),g(),v("appHasRole",dt(3,eye)))}function hye(t,n){t&1&&M(0,"tr",30)}function pye(t,n){t&1&&M(0,"tr",31)}function fye(t,n){if(t&1&&(m(0,"tr",32)(1,"td",33)(2,"div",34)(3,"mat-icon"),f(4,"money_off"),h(),m(5,"p"),f(6,"No salary ranges found"),h()()()()),t&2){let e=x(2);g(),X("colspan",e.displayedColumns.length)}}function gye(t,n){if(t&1&&(m(0,"table",8),lt(1,9),A(2,nye,2,0,"th",10)(3,rye,2,1,"td",11),ot(),lt(4,12),A(5,oye,2,0,"th",10)(6,aye,3,6,"td",11),ot(),lt(7,13),A(8,sye,2,0,"th",10)(9,lye,3,6,"td",11),ot(),lt(10,14),A(11,cye,2,0,"th",15)(12,mye,7,4,"td",16),ot(),A(13,hye,1,0,"tr",17)(14,pye,1,0,"tr",18)(15,fye,7,1,"tr",19),h()),t&2){let e=x();v("dataSource",e.dataSource),g(13),v("matHeaderRowDef",e.displayedColumns),g(),v("matRowDefColumns",e.displayedColumns)}}var aU=(()=>{let n=class n{constructor(){this.salaryRangeService=u(ad),this.authService=u(jt),this.router=u(Ae),this.snackBar=u(_i),this.dialog=u(Rn),this.dataSource=new Cw([]),this.loading=!1,this.displayedColumns=["name","minSalary","maxSalary","actions"]}ngOnInit(){this.loadSalaryRanges()}ngAfterViewInit(){this.dataSource.paginator=this.paginator,this.dataSource.sort=this.sort}loadSalaryRanges(){this.loading=!0,this.salaryRangeService.getAll().subscribe({next:i=>{this.dataSource.data=i,this.loading=!1},error:i=>{console.error("Error loading salary ranges:",i),this.loading=!1}})}createSalaryRange(){this.router.navigate(["/salary-ranges/create"])}viewSalaryRange(i){this.router.navigate(["/salary-ranges",i.id])}editSalaryRange(i){this.router.navigate(["/salary-ranges/edit",i.id])}deleteSalaryRange(i){this.dialog.open(Fr,{width:"400px",data:{title:"Delete Salary Range",message:`Are you sure you want to delete "${i.name}"? This action cannot be undone.`,confirmText:"Delete",cancelText:"Cancel"}}).afterClosed().subscribe(o=>{o&&this.salaryRangeService.delete(i.id).subscribe({next:()=>{this.showMessage(`"${i.name}" has been deleted.`),this.loadSalaryRanges()},error:a=>{console.error("Error deleting salary range:",a),this.showMessage("Failed to delete salary range. Please try again.")}})})}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}canEdit(){return this.authService.hasRole("HRAdmin")||this.authService.hasRole("Manager")}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-salary-range-list"]],viewQuery:function(r,o){if(r&1&&(ie(mr,5),ie($l,5)),r&2){let a;j(a=H())&&(o.paginator=a.first),j(a=H())&&(o.sort=a.first)}},decls:12,vars:6,consts:[[1,"flex-spacer"],["mat-raised-button","","color","primary",3,"click",4,"appHasRole"],[1,"table-container"],["class","loading-spinner",4,"ngIf"],["mat-table","","matSort","","class","salary-range-table",3,"dataSource",4,"ngIf"],["showFirstLastButtons","",3,"pageSizeOptions"],["mat-raised-button","","color","primary",3,"click"],[1,"loading-spinner"],["mat-table","","matSort","",1,"salary-range-table",3,"dataSource"],["matColumnDef","name"],["mat-header-cell","","mat-sort-header","",4,"matHeaderCellDef"],["mat-cell","",4,"matCellDef"],["matColumnDef","minSalary"],["matColumnDef","maxSalary"],["matColumnDef","actions"],["mat-header-cell","","class","mat-column-actions",4,"matHeaderCellDef"],["mat-cell","","class","mat-column-actions",4,"matCellDef"],["mat-header-row","",4,"matHeaderRowDef"],["mat-row","",4,"matRowDef","matRowDefColumns"],["class","mat-row",4,"matNoDataRow"],["mat-header-cell","","mat-sort-header",""],["mat-cell",""],["mat-header-cell","",1,"mat-column-actions"],["mat-cell","",1,"mat-column-actions"],[1,"action-buttons"],["mat-icon-button","","color","primary","matTooltip","View Details",3,"click"],["mat-icon-button","","color","accent","matTooltip","Edit Salary Range",3,"click",4,"appHasRole"],["mat-icon-button","","color","warn","matTooltip","Delete Salary Range",3,"click",4,"appHasRole"],["mat-icon-button","","color","accent","matTooltip","Edit Salary Range",3,"click"],["mat-icon-button","","color","warn","matTooltip","Delete Salary Range",3,"click"],["mat-header-row",""],["mat-row",""],[1,"mat-row"],[1,"mat-cell"],[1,"no-data"]],template:function(r,o){r&1&&(M(0,"page-header"),m(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4,"Salary Ranges"),h(),M(5,"span",0),A(6,tye,4,0,"button",1),h(),m(7,"mat-card-content")(8,"div",2),A(9,iye,2,0,"div",3)(10,gye,16,3,"table",4),h(),M(11,"mat-paginator",5),h()()),r&2&&(g(6),v("appHasRole",dt(4,oU)),g(3),v("ngIf",o.loading),g(),v("ngIf",!o.loading),g(),v("pageSizeOptions",dt(5,Jve)))},dependencies:[Je,Wt,Fe,_t,Ft,It,kt,Tt,Rt,Ot,Ge,Ze,ka,ba,ya,Da,xa,va,Ma,Ca,wa,Ea,Sa,ww,Fc,mr,Aw,$l,Iw,Zt,Kt,An,ur,wi,Pn,Lt,Xn,yl],styles:["mat-card[_ngcontent-%COMP%]{margin:16px}mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:center;margin-bottom:16px;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-header[_ngcontent-%COMP%] .flex-spacer[_ngcontent-%COMP%]{flex:1 1 auto}mat-card-header[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}mat-card-content[_ngcontent-%COMP%]{padding:0}.table-container[_ngcontent-%COMP%]{position:relative;min-height:300px;overflow-x:auto}.loading-spinner[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:300px}.salary-range-table[_ngcontent-%COMP%]{width:100%}.salary-range-table[_ngcontent-%COMP%] th[_ngcontent-%COMP%]{font-weight:600;font-size:14px;color:#000000de}.salary-range-table[_ngcontent-%COMP%] td[_ngcontent-%COMP%]{font-size:14px;color:#000000de}.salary-range-table[_ngcontent-%COMP%] .mat-column-name[_ngcontent-%COMP%]{min-width:200px}.salary-range-table[_ngcontent-%COMP%] .mat-column-minSalary[_ngcontent-%COMP%], .salary-range-table[_ngcontent-%COMP%] .mat-column-maxSalary[_ngcontent-%COMP%]{min-width:150px}.salary-range-table[_ngcontent-%COMP%] .mat-column-actions[_ngcontent-%COMP%]{width:150px;text-align:right}.salary-range-table[_ngcontent-%COMP%] .action-buttons[_ngcontent-%COMP%]{display:flex;justify-content:flex-end;gap:4px}.salary-range-table[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{margin-left:4px}.no-data[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;color:#0000008a}.no-data[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:16px}.no-data[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:16px}mat-paginator[_ngcontent-%COMP%]{border-top:1px solid rgba(0,0,0,.12)}"]});let t=n;return t})();var sU=()=>["HRAdmin","Manager"];function _ye(t,n){t&1&&(m(0,"div",2),M(1,"mat-spinner"),h())}function bye(t,n){if(t&1){let e=q();m(0,"button",9),S("click",function(){k(e);let r=x(2);return T(r.editSalaryRange())}),m(1,"mat-icon"),f(2,"edit"),h()()}}function vye(t,n){if(t&1){let e=q();m(0,"button",10),S("click",function(){k(e);let r=x(2);return T(r.deleteSalaryRange())}),m(1,"mat-icon"),f(2,"delete"),h()()}}function yye(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",7),f(2,"Created:"),h(),m(3,"span",8),f(4),me(5,"date"),h()()),t&2){let e=x(2);g(4),N(Ui(5,1,e.salaryRange.createdAt,"medium"))}}function xye(t,n){t&1&&M(0,"mat-divider")}function Cye(t,n){if(t&1&&(m(0,"mat-list-item")(1,"span",7),f(2,"Last Modified:"),h(),m(3,"span",8),f(4),me(5,"date"),h()()),t&2){let e=x(2);g(4),N(Ui(5,1,e.salaryRange.lastModifiedAt,"medium"))}}function wye(t,n){if(t&1){let e=q();m(0,"mat-card")(1,"mat-card-header")(2,"mat-card-title"),f(3),h(),m(4,"div",3),A(5,bye,3,0,"button",4)(6,vye,3,0,"button",5),m(7,"button",6),S("click",function(){k(e);let r=x();return T(r.goBack())}),m(8,"mat-icon"),f(9,"arrow_back"),h()()()(),m(10,"mat-card-content")(11,"mat-list")(12,"mat-list-item")(13,"span",7),f(14,"Name:"),h(),m(15,"span",8),f(16),h()(),M(17,"mat-divider"),m(18,"mat-list-item")(19,"span",7),f(20,"Minimum Salary:"),h(),m(21,"span",8),f(22),me(23,"currency"),h()(),M(24,"mat-divider"),m(25,"mat-list-item")(26,"span",7),f(27,"Maximum Salary:"),h(),m(28,"span",8),f(29),me(30,"currency"),h()(),M(31,"mat-divider"),A(32,yye,6,4,"mat-list-item",1)(33,xye,1,0,"mat-divider",1)(34,Cye,6,4,"mat-list-item",1),h()()()}if(t&2){let e=x();g(3),N(e.salaryRange.name),g(2),v("appHasRole",dt(13,sU)),g(),v("appHasRole",dt(14,sU)),g(10),N(e.salaryRange.name),g(6),N(Re(23,9,e.salaryRange.minSalary)),g(7),N(Re(30,11,e.salaryRange.maxSalary)),g(3),v("ngIf",e.salaryRange.createdAt),g(),v("ngIf",e.salaryRange.createdAt),g(),v("ngIf",e.salaryRange.lastModifiedAt)}}function Dye(t,n){t&1&&(m(0,"mat-card")(1,"mat-card-content")(2,"p"),f(3,"Salary range not found."),h()()())}var lU=(()=>{let n=class n{constructor(){this.salaryRangeService=u(ad),this.authService=u(jt),this.route=u(Ai),this.router=u(Ae),this.snackBar=u(_i),this.dialog=u(Rn),this.loading=!1}ngOnInit(){let i=this.route.snapshot.paramMap.get("id");i&&this.loadSalaryRange(i)}loadSalaryRange(i){this.loading=!0,this.salaryRangeService.getById(i).subscribe({next:r=>{this.salaryRange=r,this.loading=!1},error:r=>{console.error("Error loading salary range:",r),this.showMessage("Error loading salary range"),this.loading=!1,this.router.navigate(["/salary-ranges"])}})}editSalaryRange(){this.router.navigate(["/salary-ranges","edit",this.salaryRange.id])}deleteSalaryRange(){this.dialog.open(Fr,{width:"400px",data:{title:"Delete Salary Range",message:`Are you sure you want to delete "${this.salaryRange.name}"? This action cannot be undone.`,confirmText:"Delete",cancelText:"Cancel"}}).afterClosed().subscribe(r=>{r&&this.salaryRangeService.delete(this.salaryRange.id).subscribe({next:()=>{let o=this.snackBar.open(`"${this.salaryRange.name}" has been deleted.`,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"});o.afterDismissed().subscribe(()=>this.router.navigate(["/salary-ranges"])),o.onAction().subscribe(()=>this.router.navigate(["/salary-ranges"]))},error:o=>{console.error("Error deleting salary range:",o),this.showMessage("Failed to delete salary range. Please try again.")}})})}goBack(){this.router.navigate(["/salary-ranges"])}canEdit(){return this.authService.isHRAdmin()||this.authService.isManager()}canDelete(){return this.authService.isHRAdmin()||this.authService.isManager()}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-salary-range-detail"]],decls:4,vars:3,consts:[["class","loading-spinner",4,"ngIf"],[4,"ngIf"],[1,"loading-spinner"],[1,"header-actions"],["mat-icon-button","","color","primary","matTooltip","Edit Salary Range",3,"click",4,"appHasRole"],["mat-icon-button","","color","warn","matTooltip","Delete Salary Range",3,"click",4,"appHasRole"],["mat-icon-button","","matTooltip","Back to List",3,"click"],[1,"label"],[1,"value"],["mat-icon-button","","color","primary","matTooltip","Edit Salary Range",3,"click"],["mat-icon-button","","color","warn","matTooltip","Delete Salary Range",3,"click"]],template:function(r,o){r&1&&(M(0,"page-header"),A(1,_ye,2,0,"div",0)(2,wye,35,15,"mat-card",1)(3,Dye,4,0,"mat-card",1)),r&2&&(g(),v("ngIf",o.loading),g(),v("ngIf",!o.loading&&o.salaryRange),g(),v("ngIf",!o.loading&&!o.salaryRange))},dependencies:[Je,Wt,Fe,Ft,It,kt,Tt,Rt,Ot,Ge,Ze,fa,Js,pa,Kr,Nr,Zt,Kt,wi,Pn,Lt,Xn,yl,Wa],styles:[".loading-spinner[_ngcontent-%COMP%]{display:flex;justify-content:center;align-items:center;min-height:400px}mat-card[_ngcontent-%COMP%]{margin:16px;max-width:800px}mat-card-header[_ngcontent-%COMP%]{padding:16px;border-bottom:1px solid rgba(0,0,0,.12);display:flex;justify-content:space-between;align-items:center}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:24px;font-weight:500;margin:0}mat-card-header[_ngcontent-%COMP%] .header-actions[_ngcontent-%COMP%]{display:flex;gap:8px}mat-card-content[_ngcontent-%COMP%]{padding:0}mat-list[_ngcontent-%COMP%]{padding:0}mat-list-item[_ngcontent-%COMP%]{height:auto!important;min-height:48px;padding:12px 16px;display:flex;justify-content:space-between;align-items:flex-start}mat-list-item[_ngcontent-%COMP%] .label[_ngcontent-%COMP%]{font-weight:500;color:#0009;min-width:180px}mat-list-item[_ngcontent-%COMP%] .value[_ngcontent-%COMP%]{flex:1;text-align:right;word-break:break-word}"]});let t=n;return t})();function Mye(t,n){t&1&&(m(0,"div",0),M(1,"mat-spinner"),h())}function Eye(t,n){t&1&&(m(0,"mat-error"),f(1," Range name is required "),h())}function Sye(t,n){t&1&&(m(0,"mat-error"),f(1," Range name cannot exceed 100 characters "),h())}function kye(t,n){t&1&&(m(0,"mat-error"),f(1," Minimum salary is required "),h())}function Tye(t,n){t&1&&(m(0,"mat-error"),f(1," Minimum salary must be at least 0 "),h())}function Iye(t,n){t&1&&(m(0,"mat-error"),f(1," Maximum salary is required "),h())}function Aye(t,n){t&1&&(m(0,"mat-error"),f(1," Maximum salary must be at least 0 "),h())}function Oye(t,n){t&1&&(m(0,"mat-error",13),f(1," Maximum salary must be greater than minimum salary "),h())}var fA=(()=>{let n=class n{constructor(){this.fb=u(co),this.router=u(Ae),this.route=u(Ai),this.snackBar=u(_i),this.salaryRangeService=u(ad),this.loading=!1,this.isEditMode=!1}ngOnInit(){this.initForm(),this.checkEditMode()}initForm(){this.salaryRangeForm=this.fb.group({name:["",[Ve.required,Ve.maxLength(100)]],minSalary:["",[Ve.required,Ve.min(0)]],maxSalary:["",[Ve.required,Ve.min(0)]]},{validators:this.salaryRangeValidator})}salaryRangeValidator(i){let r=i.get("minSalary")?.value,o=i.get("maxSalary")?.value;return r&&o&&parseFloat(r)>=parseFloat(o)?{salaryRangeInvalid:!0}:null}checkEditMode(){this.salaryRangeId=this.route.snapshot.paramMap.get("id")||void 0,this.isEditMode=!!this.salaryRangeId,this.isEditMode&&this.salaryRangeId&&this.loadSalaryRange(this.salaryRangeId)}loadSalaryRange(i){this.loading=!0,this.salaryRangeService.getById(i).subscribe({next:r=>{this.salaryRangeForm.patchValue({name:r.name,minSalary:r.minSalary,maxSalary:r.maxSalary}),this.loading=!1},error:r=>{console.error("Error loading salary range:",r),this.showMessage("Error loading salary range"),this.loading=!1}})}onSubmit(){if(this.salaryRangeForm.invalid){this.salaryRangeForm.markAllAsTouched();return}if(this.loading=!0,this.isEditMode&&this.salaryRangeId){let i={id:this.salaryRangeId,name:this.salaryRangeForm.value.name,minSalary:parseFloat(this.salaryRangeForm.value.minSalary),maxSalary:parseFloat(this.salaryRangeForm.value.maxSalary)};this.salaryRangeService.updateSalaryRange(i).subscribe({next:()=>{this.showMessage("Salary range updated successfully"),this.router.navigate(["/salary-ranges",this.salaryRangeId])},error:r=>{console.error("Error updating salary range:",r),this.showMessage("Error updating salary range"),this.loading=!1}})}else{let i={name:this.salaryRangeForm.value.name,minSalary:parseFloat(this.salaryRangeForm.value.minSalary),maxSalary:parseFloat(this.salaryRangeForm.value.maxSalary)};this.salaryRangeService.createSalaryRange(i).subscribe({next:r=>{console.log("Salary range created - Response:",r),console.log("Salary range ID:",r?.id),this.showMessage("Salary range created successfully"),r?.id?(console.log("Navigating to detail page:","/salary-ranges/"+r.id),this.router.navigate(["/salary-ranges",r.id])):(console.warn("No salary range ID returned, navigating to list page"),this.router.navigate(["/salary-ranges"])),this.loading=!1},error:r=>{console.error("Error creating salary range:",r),this.showMessage("Error creating salary range"),this.loading=!1}})}}onCancel(){this.router.navigate(["/salary-ranges"])}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}getFormTitle(){return this.isEditMode?"Edit Salary Range":"Create Salary Range"}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-salary-range-form"]],decls:37,vars:12,consts:[[1,"loading-overlay"],[3,"ngSubmit","formGroup"],[1,"form-section"],["appearance","outline",1,"full-width"],["matInput","","formControlName","name"],[4,"ngIf"],["matInput","","type","number","formControlName","minSalary"],["matTextPrefix",""],["matInput","","type","number","formControlName","maxSalary"],["class","range-error",4,"ngIf"],[1,"form-actions"],["type","button","mat-stroked-button","",3,"click"],["type","submit","mat-raised-button","","color","primary",3,"disabled"],[1,"range-error"]],template:function(r,o){if(r&1&&(M(0,"page-header"),m(1,"mat-card")(2,"mat-card-header")(3,"mat-card-title"),f(4),h()(),m(5,"mat-card-content"),L(6,Mye,2,0,"div",0),m(7,"form",1),S("ngSubmit",function(){return o.onSubmit()}),m(8,"div",2)(9,"mat-form-field",3)(10,"mat-label"),f(11,"Range Name"),h(),M(12,"input",4),A(13,Eye,2,0,"mat-error",5)(14,Sye,2,0,"mat-error",5),h(),m(15,"mat-form-field",3)(16,"mat-label"),f(17,"Minimum Salary"),h(),M(18,"input",6),m(19,"span",7),f(20,"$\xA0"),h(),A(21,kye,2,0,"mat-error",5)(22,Tye,2,0,"mat-error",5),h(),m(23,"mat-form-field",3)(24,"mat-label"),f(25,"Maximum Salary"),h(),M(26,"input",8),m(27,"span",7),f(28,"$\xA0"),h(),A(29,Iye,2,0,"mat-error",5)(30,Aye,2,0,"mat-error",5),h(),A(31,Oye,2,0,"mat-error",9),h(),m(32,"div",10)(33,"button",11),S("click",function(){return o.onCancel()}),f(34," Cancel "),h(),m(35,"button",12),f(36),h()()()()()),r&2){let a,s,l,c,d,p;g(4),N(o.getFormTitle()),g(2),V(o.loading?6:-1),g(),v("formGroup",o.salaryRangeForm),g(6),v("ngIf",(a=o.salaryRangeForm.get("name"))==null?null:a.hasError("required")),g(),v("ngIf",(s=o.salaryRangeForm.get("name"))==null?null:s.hasError("maxlength")),g(7),v("ngIf",(l=o.salaryRangeForm.get("minSalary"))==null?null:l.hasError("required")),g(),v("ngIf",(c=o.salaryRangeForm.get("minSalary"))==null?null:c.hasError("min")),g(7),v("ngIf",(d=o.salaryRangeForm.get("maxSalary"))==null?null:d.hasError("required")),g(),v("ngIf",(p=o.salaryRangeForm.get("maxSalary"))==null?null:p.hasError("min")),g(),v("ngIf",o.salaryRangeForm.hasError("salaryRangeInvalid")&&o.salaryRangeForm.touched),g(4),v("disabled",o.loading),g(),fe(" ",o.isEditMode?"Update":"Create"," ")}},dependencies:[Je,Wt,Zn,lo,di,gu,Pt,so,nn,Yr,Fe,_t,It,kt,Tt,Rt,Ot,ai,Xt,gi,Ao,uu,Bi,Ci,Zt,Kt,wi,Lt],styles:["mat-card[_ngcontent-%COMP%]{margin:16px;max-width:800px}mat-card-header[_ngcontent-%COMP%]{margin-bottom:16px;padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}mat-card-content[_ngcontent-%COMP%]{padding:16px;position:relative}.loading-overlay[_ngcontent-%COMP%]{position:absolute;inset:0;background-color:#fffc;display:flex;align-items:center;justify-content:center;z-index:1000}.form-section[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:16px;margin-bottom:24px}.form-section[_ngcontent-%COMP%] .full-width[_ngcontent-%COMP%]{width:100%}.form-section[_ngcontent-%COMP%] .range-error[_ngcontent-%COMP%]{color:#f44336;font-size:12px;margin-top:-8px}.form-actions[_ngcontent-%COMP%]{display:flex;gap:12px;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(0,0,0,.12)}"]});let t=n;return t})();var Rye=["*",[["mat-chip-avatar"],["","matChipAvatar",""]],[["mat-chip-trailing-icon"],["","matChipRemove",""],["","matChipTrailingIcon",""]]],Pye=["*","mat-chip-avatar, [matChipAvatar]","mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"];function Fye(t,n){t&1&&(m(0,"span",3),ne(1,1),h())}function Nye(t,n){t&1&&(m(0,"span",6),ne(1,2),h())}var Lye=["*"];var Vye=new O("mat-chips-default-options",{providedIn:"root",factory:()=>({separatorKeyCodes:[13]})}),cU=new O("MatChipAvatar"),dU=new O("MatChipTrailingIcon"),uU=new O("MatChipEdit"),mU=new O("MatChipRemove"),pU=new O("MatChip"),hU=(()=>{class t{_elementRef=u(Y);_parentChip=u(pU);isInteractive=!0;_isPrimary=!0;_isLeading=!1;get disabled(){return this._disabled||this._parentChip?.disabled||!1}set disabled(e){this._disabled=e}_disabled=!1;tabIndex=-1;_allowFocusWhenDisabled=!1;_getDisabledAttribute(){return this.disabled&&!this._allowFocusWhenDisabled?"":null}_getTabindex(){return this.disabled&&!this._allowFocusWhenDisabled||!this.isInteractive?null:this.tabIndex.toString()}constructor(){u(ft).load(Oi),this._elementRef.nativeElement.nodeName==="BUTTON"&&this._elementRef.nativeElement.setAttribute("type","button")}focus(){this._elementRef.nativeElement.focus()}_handleClick(e){!this.disabled&&this.isInteractive&&this._isPrimary&&(e.preventDefault(),this._parentChip._handlePrimaryActionInteraction())}_handleKeydown(e){(e.keyCode===13||e.keyCode===32)&&!this.disabled&&this.isInteractive&&this._isPrimary&&!this._parentChip._isEditing&&(e.preventDefault(),this._parentChip._handlePrimaryActionInteraction())}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["","matChipAction",""]],hostAttrs:[1,"mdc-evolution-chip__action","mat-mdc-chip-action"],hostVars:11,hostBindings:function(i,r){i&1&&S("click",function(a){return r._handleClick(a)})("keydown",function(a){return r._handleKeydown(a)}),i&2&&(X("tabindex",r._getTabindex())("disabled",r._getDisabledAttribute())("aria-disabled",r.disabled),G("mdc-evolution-chip__action--primary",r._isPrimary)("mdc-evolution-chip__action--presentational",!r.isInteractive)("mdc-evolution-chip__action--secondary",!r._isPrimary)("mdc-evolution-chip__action--trailing",!r._isPrimary&&!r._isLeading))},inputs:{isInteractive:"isInteractive",disabled:[2,"disabled","disabled",B],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?-1:ht(e)],_allowFocusWhenDisabled:"_allowFocusWhenDisabled"}})}return t})();var gA=(()=>{class t{_changeDetectorRef=u(ye);_elementRef=u(Y);_tagName=u(rE);_ngZone=u(ae);_focusMonitor=u(oi);_globalRippleOptions=u(Bs,{optional:!0});_document=u(_e);_onFocus=new z;_onBlur=new z;_isBasicChip;role=null;_hasFocusInternal=!1;_pendingFocus;_actionChanges;_animationsDisabled=Qe();_allLeadingIcons;_allTrailingIcons;_allEditIcons;_allRemoveIcons;_hasFocus(){return this._hasFocusInternal}id=u(et).getId("mat-mdc-chip-");ariaLabel=null;ariaDescription=null;_chipListDisabled=!1;_hadFocusOnRemove=!1;_textElement;get value(){return this._value!==void 0?this._value:this._textElement.textContent.trim()}set value(e){this._value=e}_value;color;removable=!0;highlighted=!1;disableRipple=!1;get disabled(){return this._disabled||this._chipListDisabled}set disabled(e){this._disabled=e}_disabled=!1;removed=new U;destroyed=new U;basicChipAttrName="mat-basic-chip";leadingIcon;editIcon;trailingIcon;removeIcon;primaryAction;_rippleLoader=u(iy);_injector=u(de);constructor(){let e=u(ft);e.load(Oi),e.load(ro),this._monitorFocus(),this._rippleLoader?.configureRipple(this._elementRef.nativeElement,{className:"mat-mdc-chip-ripple",disabled:this._isRippleDisabled()})}ngOnInit(){this._isBasicChip=this._elementRef.nativeElement.hasAttribute(this.basicChipAttrName)||this._tagName.toLowerCase()===this.basicChipAttrName}ngAfterViewInit(){this._textElement=this._elementRef.nativeElement.querySelector(".mat-mdc-chip-action-label"),this._pendingFocus&&(this._pendingFocus=!1,this.focus())}ngAfterContentInit(){this._actionChanges=it(this._allLeadingIcons.changes,this._allTrailingIcons.changes,this._allEditIcons.changes,this._allRemoveIcons.changes).subscribe(()=>this._changeDetectorRef.markForCheck())}ngDoCheck(){this._rippleLoader.setDisabled(this._elementRef.nativeElement,this._isRippleDisabled())}ngOnDestroy(){this._focusMonitor.stopMonitoring(this._elementRef),this._rippleLoader?.destroyRipple(this._elementRef.nativeElement),this._actionChanges?.unsubscribe(),this.destroyed.emit({chip:this}),this.destroyed.complete()}remove(){this.removable&&(this._hadFocusOnRemove=this._hasFocus(),this.removed.emit({chip:this}))}_isRippleDisabled(){return this.disabled||this.disableRipple||this._animationsDisabled||this._isBasicChip||!this._hasInteractiveActions()||!!this._globalRippleOptions?.disabled}_hasTrailingIcon(){return!!(this.trailingIcon||this.removeIcon)}_handleKeydown(e){(e.keyCode===8&&!e.repeat||e.keyCode===46)&&(e.preventDefault(),this.remove())}focus(){this.disabled||(this.primaryAction?this.primaryAction.focus():this._pendingFocus=!0)}_getSourceAction(e){return this._getActions().find(i=>{let r=i._elementRef.nativeElement;return r===e||r.contains(e)})}_getActions(){let e=[];return this.editIcon&&e.push(this.editIcon),this.primaryAction&&e.push(this.primaryAction),this.removeIcon&&e.push(this.removeIcon),this.trailingIcon&&e.push(this.trailingIcon),e}_handlePrimaryActionInteraction(){}_hasInteractiveActions(){return this._getActions().some(e=>e.isInteractive)}_edit(e){}_monitorFocus(){this._focusMonitor.monitor(this._elementRef,!0).subscribe(e=>{let i=e!==null;i!==this._hasFocusInternal&&(this._hasFocusInternal=i,i?this._onFocus.next({chip:this}):(this._changeDetectorRef.markForCheck(),setTimeout(()=>this._ngZone.run(()=>this._onBlur.next({chip:this})))))})}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-basic-chip"],["","mat-basic-chip",""],["mat-chip"],["","mat-chip",""]],contentQueries:function(i,r,o){if(i&1&&(Ce(o,cU,5),Ce(o,uU,5),Ce(o,dU,5),Ce(o,mU,5),Ce(o,cU,5),Ce(o,dU,5),Ce(o,uU,5),Ce(o,mU,5)),i&2){let a;j(a=H())&&(r.leadingIcon=a.first),j(a=H())&&(r.editIcon=a.first),j(a=H())&&(r.trailingIcon=a.first),j(a=H())&&(r.removeIcon=a.first),j(a=H())&&(r._allLeadingIcons=a),j(a=H())&&(r._allTrailingIcons=a),j(a=H())&&(r._allEditIcons=a),j(a=H())&&(r._allRemoveIcons=a)}},viewQuery:function(i,r){if(i&1&&ie(hU,5),i&2){let o;j(o=H())&&(r.primaryAction=o.first)}},hostAttrs:[1,"mat-mdc-chip"],hostVars:31,hostBindings:function(i,r){i&1&&S("keydown",function(a){return r._handleKeydown(a)}),i&2&&(pi("id",r.id),X("role",r.role)("aria-label",r.ariaLabel),at("mat-"+(r.color||"primary")),G("mdc-evolution-chip",!r._isBasicChip)("mdc-evolution-chip--disabled",r.disabled)("mdc-evolution-chip--with-trailing-action",r._hasTrailingIcon())("mdc-evolution-chip--with-primary-graphic",r.leadingIcon)("mdc-evolution-chip--with-primary-icon",r.leadingIcon)("mdc-evolution-chip--with-avatar",r.leadingIcon)("mat-mdc-chip-with-avatar",r.leadingIcon)("mat-mdc-chip-highlighted",r.highlighted)("mat-mdc-chip-disabled",r.disabled)("mat-mdc-basic-chip",r._isBasicChip)("mat-mdc-standard-chip",!r._isBasicChip)("mat-mdc-chip-with-trailing-icon",r._hasTrailingIcon())("_mat-animation-noopable",r._animationsDisabled))},inputs:{role:"role",id:"id",ariaLabel:[0,"aria-label","ariaLabel"],ariaDescription:[0,"aria-description","ariaDescription"],value:"value",color:"color",removable:[2,"removable","removable",B],highlighted:[2,"highlighted","highlighted",B],disableRipple:[2,"disableRipple","disableRipple",B],disabled:[2,"disabled","disabled",B]},outputs:{removed:"removed",destroyed:"destroyed"},exportAs:["matChip"],features:[we([{provide:pU,useExisting:t}])],ngContentSelectors:Pye,decls:8,vars:3,consts:[[1,"mat-mdc-chip-focus-overlay"],[1,"mdc-evolution-chip__cell","mdc-evolution-chip__cell--primary"],["matChipAction","",3,"isInteractive"],[1,"mdc-evolution-chip__graphic","mat-mdc-chip-graphic"],[1,"mdc-evolution-chip__text-label","mat-mdc-chip-action-label"],[1,"mat-mdc-chip-primary-focus-indicator","mat-focus-indicator"],[1,"mdc-evolution-chip__cell","mdc-evolution-chip__cell--trailing"]],template:function(i,r){i&1&&(Ee(Rye),M(0,"span",0),m(1,"span",1)(2,"span",2),L(3,Fye,2,0,"span",3),m(4,"span",4),ne(5),M(6,"span",5),h()()(),L(7,Nye,2,0,"span",6)),i&2&&(g(2),v("isInteractive",!1),g(),V(r.leadingIcon?3:-1),g(4),V(r._hasTrailingIcon()?7:-1))},dependencies:[hU],styles:[`.mdc-evolution-chip,.mdc-evolution-chip__cell,.mdc-evolution-chip__action{display:inline-flex;align-items:center}.mdc-evolution-chip{position:relative;max-width:100%}.mdc-evolution-chip__cell,.mdc-evolution-chip__action{height:100%}.mdc-evolution-chip__cell--primary{flex-basis:100%;overflow-x:hidden}.mdc-evolution-chip__cell--trailing{flex:1 0 auto}.mdc-evolution-chip__action{align-items:center;background:none;border:none;box-sizing:content-box;cursor:pointer;display:inline-flex;justify-content:center;outline:none;padding:0;text-decoration:none;color:inherit}.mdc-evolution-chip__action--presentational{cursor:auto}.mdc-evolution-chip--disabled,.mdc-evolution-chip__action:disabled{pointer-events:none}@media(forced-colors: active){.mdc-evolution-chip--disabled,.mdc-evolution-chip__action:disabled{forced-color-adjust:none}}.mdc-evolution-chip__action--primary{font:inherit;letter-spacing:inherit;white-space:inherit;overflow-x:hidden}.mat-mdc-standard-chip .mdc-evolution-chip__action--primary::before{border-width:var(--mat-chip-outline-width, 1px);border-radius:var(--mat-chip-container-shape-radius, 8px);box-sizing:border-box;content:"";height:100%;left:0;position:absolute;pointer-events:none;top:0;width:100%;z-index:1;border-style:solid}.mat-mdc-standard-chip .mdc-evolution-chip__action--primary{padding-left:12px;padding-right:12px}.mat-mdc-standard-chip.mdc-evolution-chip--with-primary-graphic .mdc-evolution-chip__action--primary{padding-left:0;padding-right:12px}[dir=rtl] .mat-mdc-standard-chip.mdc-evolution-chip--with-primary-graphic .mdc-evolution-chip__action--primary{padding-left:12px;padding-right:0}.mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) .mdc-evolution-chip__action--primary::before{border-color:var(--mat-chip-outline-color, var(--mat-sys-outline))}.mdc-evolution-chip__action--primary:not(.mdc-evolution-chip__action--presentational):not(.mdc-ripple-upgraded):focus::before{border-color:var(--mat-chip-focus-outline-color, var(--mat-sys-on-surface-variant))}.mat-mdc-standard-chip.mdc-evolution-chip--disabled .mdc-evolution-chip__action--primary::before{border-color:var(--mat-chip-disabled-outline-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}.mat-mdc-standard-chip.mdc-evolution-chip--selected .mdc-evolution-chip__action--primary::before{border-width:var(--mat-chip-flat-selected-outline-width, 0)}.mat-mdc-basic-chip .mdc-evolution-chip__action--primary{font:inherit}.mat-mdc-standard-chip.mdc-evolution-chip--with-leading-action .mdc-evolution-chip__action--primary{padding-left:0;padding-right:12px}[dir=rtl] .mat-mdc-standard-chip.mdc-evolution-chip--with-leading-action .mdc-evolution-chip__action--primary{padding-left:12px;padding-right:0}.mat-mdc-standard-chip.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--primary{padding-left:12px;padding-right:0}[dir=rtl] .mat-mdc-standard-chip.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--primary{padding-left:0;padding-right:12px}.mat-mdc-standard-chip.mdc-evolution-chip--with-leading-action.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--primary{padding-left:0;padding-right:0}.mat-mdc-standard-chip.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--primary{padding-left:0;padding-right:0}[dir=rtl] .mat-mdc-standard-chip.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--primary{padding-left:0;padding-right:0}.mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic .mdc-evolution-chip__action--primary{padding-left:0;padding-right:12px}[dir=rtl] .mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic .mdc-evolution-chip__action--primary{padding-left:12px;padding-right:0}.mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--primary{padding-left:0;padding-right:0}[dir=rtl] .mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--primary{padding-left:0;padding-right:0}.mdc-evolution-chip__action--secondary{position:relative;overflow:visible}.mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) .mdc-evolution-chip__action--secondary{color:var(--mat-chip-with-trailing-icon-trailing-icon-color, var(--mat-sys-on-surface-variant))}.mat-mdc-standard-chip.mdc-evolution-chip--disabled .mdc-evolution-chip__action--secondary{color:var(--mat-chip-with-trailing-icon-disabled-trailing-icon-color, var(--mat-sys-on-surface))}.mat-mdc-standard-chip.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--secondary{padding-left:8px;padding-right:8px}.mat-mdc-standard-chip.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--secondary{padding-left:8px;padding-right:8px}.mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--secondary{padding-left:8px;padding-right:8px}[dir=rtl] .mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__action--secondary{padding-left:8px;padding-right:8px}.mdc-evolution-chip__text-label{-webkit-user-select:none;user-select:none;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.mat-mdc-standard-chip .mdc-evolution-chip__text-label{font-family:var(--mat-chip-label-text-font, var(--mat-sys-label-large-font));line-height:var(--mat-chip-label-text-line-height, var(--mat-sys-label-large-line-height));font-size:var(--mat-chip-label-text-size, var(--mat-sys-label-large-size));font-weight:var(--mat-chip-label-text-weight, var(--mat-sys-label-large-weight));letter-spacing:var(--mat-chip-label-text-tracking, var(--mat-sys-label-large-tracking))}.mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) .mdc-evolution-chip__text-label{color:var(--mat-chip-label-text-color, var(--mat-sys-on-surface-variant))}.mat-mdc-standard-chip.mdc-evolution-chip--selected:not(.mdc-evolution-chip--disabled) .mdc-evolution-chip__text-label{color:var(--mat-chip-selected-label-text-color, var(--mat-sys-on-secondary-container))}.mat-mdc-standard-chip.mdc-evolution-chip--disabled .mdc-evolution-chip__text-label,.mat-mdc-standard-chip.mdc-evolution-chip--selected.mdc-evolution-chip--disabled .mdc-evolution-chip__text-label{color:var(--mat-chip-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mdc-evolution-chip__graphic{align-items:center;display:inline-flex;justify-content:center;overflow:hidden;pointer-events:none;position:relative;flex:1 0 auto}.mat-mdc-standard-chip .mdc-evolution-chip__graphic{width:var(--mat-chip-with-avatar-avatar-size, 24px);height:var(--mat-chip-with-avatar-avatar-size, 24px);font-size:var(--mat-chip-with-avatar-avatar-size, 24px)}.mdc-evolution-chip--selecting .mdc-evolution-chip__graphic{transition:width 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-evolution-chip--selectable:not(.mdc-evolution-chip--selected):not(.mdc-evolution-chip--with-primary-icon) .mdc-evolution-chip__graphic{width:0}.mat-mdc-standard-chip.mdc-evolution-chip--with-primary-graphic .mdc-evolution-chip__graphic{padding-left:6px;padding-right:6px}.mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic .mdc-evolution-chip__graphic{padding-left:4px;padding-right:8px}[dir=rtl] .mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic .mdc-evolution-chip__graphic{padding-left:8px;padding-right:4px}.mat-mdc-standard-chip.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__graphic{padding-left:6px;padding-right:6px}.mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__graphic{padding-left:4px;padding-right:8px}[dir=rtl] .mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-trailing-action .mdc-evolution-chip__graphic{padding-left:8px;padding-right:4px}.mdc-evolution-chip--with-avatar.mdc-evolution-chip--with-primary-graphic.mdc-evolution-chip--with-leading-action .mdc-evolution-chip__graphic{padding-left:0}.mdc-evolution-chip__checkmark{position:absolute;opacity:0;top:50%;left:50%;height:20px;width:20px}.mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) .mdc-evolution-chip__checkmark{color:var(--mat-chip-with-icon-selected-icon-color, var(--mat-sys-on-secondary-container))}.mat-mdc-standard-chip.mdc-evolution-chip--disabled .mdc-evolution-chip__checkmark{color:var(--mat-chip-with-icon-disabled-icon-color, var(--mat-sys-on-surface))}.mdc-evolution-chip--selecting .mdc-evolution-chip__checkmark{transition:transform 150ms 0ms cubic-bezier(0.4, 0, 0.2, 1);transform:translate(-75%, -50%)}.mdc-evolution-chip--selected .mdc-evolution-chip__checkmark{transform:translate(-50%, -50%);opacity:1}.mdc-evolution-chip__checkmark-svg{display:block}.mdc-evolution-chip__checkmark-path{stroke-width:2px;stroke-dasharray:29.7833385;stroke-dashoffset:29.7833385;stroke:currentColor}.mdc-evolution-chip--selecting .mdc-evolution-chip__checkmark-path{transition:stroke-dashoffset 150ms 45ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-evolution-chip--selected .mdc-evolution-chip__checkmark-path{stroke-dashoffset:0}@media(forced-colors: active){.mdc-evolution-chip__checkmark-path{stroke:CanvasText !important}}.mat-mdc-standard-chip .mdc-evolution-chip__icon--trailing{height:18px;width:18px;font-size:18px}.mdc-evolution-chip--disabled .mdc-evolution-chip__icon--trailing.mat-mdc-chip-remove{opacity:calc(var(--mat-chip-trailing-action-opacity, 1)*var(--mat-chip-with-trailing-icon-disabled-trailing-icon-opacity, 0.38))}.mdc-evolution-chip--disabled .mdc-evolution-chip__icon--trailing.mat-mdc-chip-remove:focus{opacity:calc(var(--mat-chip-trailing-action-focus-opacity, 1)*var(--mat-chip-with-trailing-icon-disabled-trailing-icon-opacity, 0.38))}.mat-mdc-standard-chip{border-radius:var(--mat-chip-container-shape-radius, 8px);height:var(--mat-chip-container-height, 32px)}.mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled){background-color:var(--mat-chip-elevated-container-color, transparent)}.mat-mdc-standard-chip.mdc-evolution-chip--disabled{background-color:var(--mat-chip-elevated-disabled-container-color)}.mat-mdc-standard-chip.mdc-evolution-chip--selected:not(.mdc-evolution-chip--disabled){background-color:var(--mat-chip-elevated-selected-container-color, var(--mat-sys-secondary-container))}.mat-mdc-standard-chip.mdc-evolution-chip--selected.mdc-evolution-chip--disabled{background-color:var(--mat-chip-flat-disabled-selected-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent))}@media(forced-colors: active){.mat-mdc-standard-chip{outline:solid 1px}}.mat-mdc-standard-chip .mdc-evolution-chip__icon--primary{border-radius:var(--mat-chip-with-avatar-avatar-shape-radius, 24px);width:var(--mat-chip-with-icon-icon-size, 18px);height:var(--mat-chip-with-icon-icon-size, 18px);font-size:var(--mat-chip-with-icon-icon-size, 18px)}.mdc-evolution-chip--selected .mdc-evolution-chip__icon--primary{opacity:0}.mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) .mdc-evolution-chip__icon--primary{color:var(--mat-chip-with-icon-icon-color, var(--mat-sys-on-surface-variant))}.mat-mdc-standard-chip.mdc-evolution-chip--disabled .mdc-evolution-chip__icon--primary{color:var(--mat-chip-with-icon-disabled-icon-color, var(--mat-sys-on-surface))}.mat-mdc-chip-highlighted{--mat-chip-with-icon-icon-color: var(--mat-chip-with-icon-selected-icon-color, var(--mat-sys-on-secondary-container));--mat-chip-elevated-container-color: var(--mat-chip-elevated-selected-container-color, var(--mat-sys-secondary-container));--mat-chip-label-text-color: var(--mat-chip-selected-label-text-color, var(--mat-sys-on-secondary-container));--mat-chip-outline-width: var(--mat-chip-flat-selected-outline-width, 0)}.mat-mdc-chip-focus-overlay{background:var(--mat-chip-focus-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-mdc-chip-selected .mat-mdc-chip-focus-overlay,.mat-mdc-chip-highlighted .mat-mdc-chip-focus-overlay{background:var(--mat-chip-selected-focus-state-layer-color, var(--mat-sys-on-secondary-container))}.mat-mdc-chip:hover .mat-mdc-chip-focus-overlay{background:var(--mat-chip-hover-state-layer-color, var(--mat-sys-on-surface-variant));opacity:var(--mat-chip-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-chip-focus-overlay .mat-mdc-chip-selected:hover,.mat-mdc-chip-highlighted:hover .mat-mdc-chip-focus-overlay{background:var(--mat-chip-selected-hover-state-layer-color, var(--mat-sys-on-secondary-container));opacity:var(--mat-chip-selected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-chip.cdk-focused .mat-mdc-chip-focus-overlay{background:var(--mat-chip-focus-state-layer-color, var(--mat-sys-on-surface-variant));opacity:var(--mat-chip-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-chip-selected.cdk-focused .mat-mdc-chip-focus-overlay,.mat-mdc-chip-highlighted.cdk-focused .mat-mdc-chip-focus-overlay{background:var(--mat-chip-selected-focus-state-layer-color, var(--mat-sys-on-secondary-container));opacity:var(--mat-chip-selected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mdc-evolution-chip--disabled:not(.mdc-evolution-chip--selected) .mat-mdc-chip-avatar{opacity:var(--mat-chip-with-avatar-disabled-avatar-opacity, 0.38)}.mdc-evolution-chip--disabled .mdc-evolution-chip__icon--trailing{opacity:var(--mat-chip-with-trailing-icon-disabled-trailing-icon-opacity, 0.38)}.mdc-evolution-chip--disabled.mdc-evolution-chip--selected .mdc-evolution-chip__checkmark{opacity:var(--mat-chip-with-icon-disabled-icon-opacity, 0.38)}.mat-mdc-standard-chip.mdc-evolution-chip--disabled{opacity:var(--mat-chip-disabled-container-opacity, 1)}.mat-mdc-standard-chip.mdc-evolution-chip--selected .mdc-evolution-chip__icon--trailing,.mat-mdc-standard-chip.mat-mdc-chip-highlighted .mdc-evolution-chip__icon--trailing{color:var(--mat-chip-selected-trailing-icon-color, var(--mat-sys-on-secondary-container))}.mat-mdc-standard-chip.mdc-evolution-chip--selected.mdc-evolution-chip--disabled .mdc-evolution-chip__icon--trailing,.mat-mdc-standard-chip.mat-mdc-chip-highlighted.mdc-evolution-chip--disabled .mdc-evolution-chip__icon--trailing{color:var(--mat-chip-selected-disabled-trailing-icon-color, var(--mat-sys-on-surface))}.mat-mdc-chip-edit,.mat-mdc-chip-remove{opacity:var(--mat-chip-trailing-action-opacity, 1)}.mat-mdc-chip-edit:focus,.mat-mdc-chip-remove:focus{opacity:var(--mat-chip-trailing-action-focus-opacity, 1)}.mat-mdc-chip-edit::after,.mat-mdc-chip-remove::after{background-color:var(--mat-chip-trailing-action-state-layer-color, var(--mat-sys-on-surface-variant))}.mat-mdc-chip-edit:hover::after,.mat-mdc-chip-remove:hover::after{opacity:var(--mat-chip-trailing-action-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity))}.mat-mdc-chip-edit:focus::after,.mat-mdc-chip-remove:focus::after{opacity:var(--mat-chip-trailing-action-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity))}.mat-mdc-chip-selected .mat-mdc-chip-remove::after,.mat-mdc-chip-highlighted .mat-mdc-chip-remove::after{background-color:var(--mat-chip-selected-trailing-action-state-layer-color, var(--mat-sys-on-secondary-container))}.mat-mdc-standard-chip{-webkit-tap-highlight-color:rgba(0,0,0,0)}.mat-mdc-standard-chip .mdc-evolution-chip__cell--primary,.mat-mdc-standard-chip .mdc-evolution-chip__action--primary,.mat-mdc-standard-chip .mat-mdc-chip-action-label{overflow:visible}.mat-mdc-standard-chip .mat-mdc-chip-graphic,.mat-mdc-standard-chip .mat-mdc-chip-trailing-icon{box-sizing:content-box}.mat-mdc-standard-chip._mat-animation-noopable,.mat-mdc-standard-chip._mat-animation-noopable .mdc-evolution-chip__graphic,.mat-mdc-standard-chip._mat-animation-noopable .mdc-evolution-chip__checkmark,.mat-mdc-standard-chip._mat-animation-noopable .mdc-evolution-chip__checkmark-path{transition-duration:1ms;animation-duration:1ms}.mat-mdc-chip-focus-overlay{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none;opacity:0;border-radius:inherit;transition:opacity 150ms linear}._mat-animation-noopable .mat-mdc-chip-focus-overlay{transition:none}.mat-mdc-basic-chip .mat-mdc-chip-focus-overlay{display:none}.mat-mdc-chip .mat-ripple.mat-mdc-chip-ripple{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none;border-radius:inherit}.mat-mdc-chip-avatar{text-align:center;line-height:1;color:var(--mat-chip-with-icon-icon-color, currentColor)}.mat-mdc-chip{position:relative;z-index:0}.mat-mdc-chip-action-label{text-align:left;z-index:1}[dir=rtl] .mat-mdc-chip-action-label{text-align:right}.mat-mdc-chip.mdc-evolution-chip--with-trailing-action .mat-mdc-chip-action-label{position:relative}.mat-mdc-chip-action-label .mat-mdc-chip-primary-focus-indicator{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none}.mat-mdc-chip-action-label .mat-focus-indicator::before{margin:calc(calc(var(--mat-focus-indicator-border-width, 3px) + 2px)*-1)}.mat-mdc-chip-edit::before,.mat-mdc-chip-remove::before{margin:calc(var(--mat-focus-indicator-border-width, 3px)*-1);left:8px;right:8px}.mat-mdc-chip-edit::after,.mat-mdc-chip-remove::after{content:"";display:block;opacity:0;position:absolute;top:-3px;bottom:-3px;left:5px;right:5px;border-radius:50%;box-sizing:border-box;padding:12px;margin:-12px;background-clip:content-box}.mat-mdc-chip-edit .mat-icon,.mat-mdc-chip-remove .mat-icon{width:18px;height:18px;font-size:18px;box-sizing:content-box}.mat-chip-edit-input{cursor:text;display:inline-block;color:inherit;outline:0}@media(forced-colors: active){.mat-mdc-chip-selected:not(.mat-mdc-chip-multiple){outline-width:3px}}.mat-mdc-chip-action:focus .mat-focus-indicator::before{content:""}.mdc-evolution-chip__icon,.mat-mdc-chip-edit .mat-icon,.mat-mdc-chip-remove .mat-icon{min-height:fit-content}img.mdc-evolution-chip__icon{min-height:0} +`],encapsulation:2,changeDetection:0})}return t})();var fU=(()=>{class t{_elementRef=u(Y);_changeDetectorRef=u(ye);_dir=u(Yt,{optional:!0});_lastDestroyedFocusedChipIndex=null;_keyManager;_destroyed=new z;_defaultRole="presentation";get chipFocusChanges(){return this._getChipStream(e=>e._onFocus)}get chipDestroyedChanges(){return this._getChipStream(e=>e.destroyed)}get chipRemovedChanges(){return this._getChipStream(e=>e.removed)}get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._syncChipsState()}_disabled=!1;get empty(){return!this._chips||this._chips.length===0}get role(){return this._explicitRole?this._explicitRole:this.empty?null:this._defaultRole}tabIndex=0;set role(e){this._explicitRole=e}_explicitRole=null;get focused(){return this._hasFocusedChip()}_chips;_chipActions=new Dr;constructor(){}ngAfterViewInit(){this._setUpFocusManagement(),this._trackChipSetChanges(),this._trackDestroyedFocusedChip()}ngOnDestroy(){this._keyManager?.destroy(),this._chipActions.destroy(),this._destroyed.next(),this._destroyed.complete()}_hasFocusedChip(){return this._chips&&this._chips.some(e=>e._hasFocus())}_syncChipsState(){this._chips?.forEach(e=>{e._chipListDisabled=this._disabled,e._changeDetectorRef.markForCheck()})}focus(){}_handleKeydown(e){this._originatesFromChip(e)&&this._keyManager.onKeydown(e)}_isValidIndex(e){return e>=0&&ethis._elementRef.nativeElement.tabIndex=e))}_getChipStream(e){return this._chips.changes.pipe(Ue(null),je(()=>it(...this._chips.map(e))))}_originatesFromChip(e){let i=e.target;for(;i&&i!==this._elementRef.nativeElement;){if(i.classList.contains("mat-mdc-chip"))return!0;i=i.parentElement}return!1}_setUpFocusManagement(){this._chips.changes.pipe(Ue(this._chips)).subscribe(e=>{let i=[];e.forEach(r=>r._getActions().forEach(o=>i.push(o))),this._chipActions.reset(i),this._chipActions.notifyOnChanges()}),this._keyManager=new Fs(this._chipActions).withVerticalOrientation().withHorizontalOrientation(this._dir?this._dir.value:"ltr").withHomeAndEnd().skipPredicate(e=>this._skipPredicate(e)),this.chipFocusChanges.pipe(xe(this._destroyed)).subscribe(({chip:e})=>{let i=e._getSourceAction(document.activeElement);i&&this._keyManager.updateActiveItem(i)}),this._dir?.change.pipe(xe(this._destroyed)).subscribe(e=>this._keyManager.withHorizontalOrientation(e))}_skipPredicate(e){return!e.isInteractive||e.disabled}_trackChipSetChanges(){this._chips.changes.pipe(Ue(null),xe(this._destroyed)).subscribe(()=>{this.disabled&&Promise.resolve().then(()=>this._syncChipsState()),this._redirectDestroyedChipFocus()})}_trackDestroyedFocusedChip(){this.chipDestroyedChanges.pipe(xe(this._destroyed)).subscribe(e=>{let r=this._chips.toArray().indexOf(e.chip),o=e.chip._hasFocus(),a=e.chip._hadFocusOnRemove&&this._keyManager.activeItem&&e.chip._getActions().includes(this._keyManager.activeItem),s=o||a;this._isValidIndex(r)&&s&&(this._lastDestroyedFocusedChipIndex=r)})}_redirectDestroyedChipFocus(){if(this._lastDestroyedFocusedChipIndex!=null){if(this._chips.length){let e=Math.min(this._lastDestroyedFocusedChipIndex,this._chips.length-1),i=this._chips.toArray()[e];i.disabled?this._chips.length===1?this.focus():this._keyManager.setPreviousItemActive():i.focus()}else this.focus();this._lastDestroyedFocusedChipIndex=null}}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-chip-set"]],contentQueries:function(i,r,o){if(i&1&&Ce(o,gA,5),i&2){let a;j(a=H())&&(r._chips=a)}},hostAttrs:[1,"mat-mdc-chip-set","mdc-evolution-chip-set"],hostVars:1,hostBindings:function(i,r){i&1&&S("keydown",function(a){return r._handleKeydown(a)}),i&2&&X("role",r.role)},inputs:{disabled:[2,"disabled","disabled",B],role:"role",tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:ht(e)]},ngContentSelectors:Lye,decls:2,vars:0,consts:[["role","presentation",1,"mdc-evolution-chip-set__chips"]],template:function(i,r){i&1&&(Ee(),gt(0,"div",0),ne(1),yt())},styles:[`.mat-mdc-chip-set{display:flex}.mat-mdc-chip-set:focus{outline:none}.mat-mdc-chip-set .mdc-evolution-chip-set__chips{min-width:100%;margin-left:-8px;margin-right:0}.mat-mdc-chip-set .mdc-evolution-chip{margin:4px 0 4px 8px}[dir=rtl] .mat-mdc-chip-set .mdc-evolution-chip-set__chips{margin-left:0;margin-right:-8px}[dir=rtl] .mat-mdc-chip-set .mdc-evolution-chip{margin-left:0;margin-right:8px}.mdc-evolution-chip-set__chips{display:flex;flex-flow:wrap;min-width:0}.mat-mdc-chip-set-stacked{flex-direction:column;align-items:flex-start}.mat-mdc-chip-set-stacked .mat-mdc-chip{width:100%}.mat-mdc-chip-set-stacked .mdc-evolution-chip__graphic{flex-grow:0}.mat-mdc-chip-set-stacked .mdc-evolution-chip__action--primary{flex-basis:100%;justify-content:start}input.mat-mdc-chip-input{flex:1 0 150px;margin-left:8px}[dir=rtl] input.mat-mdc-chip-input{margin-left:0;margin-right:8px}.mat-mdc-form-field:not(.mat-form-field-hide-placeholder) input.mat-mdc-chip-input::placeholder{opacity:1}.mat-mdc-form-field:not(.mat-form-field-hide-placeholder) input.mat-mdc-chip-input::-moz-placeholder{opacity:1}.mat-mdc-form-field:not(.mat-form-field-hide-placeholder) input.mat-mdc-chip-input::-webkit-input-placeholder{opacity:1}.mat-mdc-form-field:not(.mat-form-field-hide-placeholder) input.mat-mdc-chip-input:-ms-input-placeholder{opacity:1}.mat-mdc-chip-set+input.mat-mdc-chip-input{margin-left:0;margin-right:0} +`],encapsulation:2,changeDetection:0})}return t})();var Ow=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({providers:[kl,{provide:Vye,useValue:{separatorKeyCodes:[13]}}],imports:[De,Io,De]})}return t})();var gU=new O("CdkAccordion");var _U=(()=>{class t{accordion=u(gU,{optional:!0,skipSelf:!0});_changeDetectorRef=u(ye);_expansionDispatcher=u(hu);_openCloseAllSubscription=ke.EMPTY;closed=new U;opened=new U;destroyed=new U;expandedChange=new U;id=u(et).getId("cdk-accordion-child-");get expanded(){return this._expanded}set expanded(e){if(this._expanded!==e){if(this._expanded=e,this.expandedChange.emit(e),e){this.opened.emit();let i=this.accordion?this.accordion.id:this.id;this._expansionDispatcher.notify(this.id,i)}else this.closed.emit();this._changeDetectorRef.markForCheck()}}_expanded=!1;get disabled(){return this._disabled()}set disabled(e){this._disabled.set(e)}_disabled=he(!1);_removeUniqueSelectionListener=()=>{};constructor(){}ngOnInit(){this._removeUniqueSelectionListener=this._expansionDispatcher.listen((e,i)=>{this.accordion&&!this.accordion.multi&&this.accordion.id===i&&this.id!==e&&(this.expanded=!1)}),this.accordion&&(this._openCloseAllSubscription=this._subscribeToOpenCloseAllActions())}ngOnDestroy(){this.opened.complete(),this.closed.complete(),this.destroyed.emit(),this.destroyed.complete(),this._removeUniqueSelectionListener(),this._openCloseAllSubscription.unsubscribe()}toggle(){this.disabled||(this.expanded=!this.expanded)}close(){this.disabled||(this.expanded=!1)}open(){this.disabled||(this.expanded=!0)}_subscribeToOpenCloseAllActions(){return this.accordion._openCloseAllActions.subscribe(e=>{this.disabled||(this.expanded=e)})}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["cdk-accordion-item"],["","cdkAccordionItem",""]],inputs:{expanded:[2,"expanded","expanded",B],disabled:[2,"disabled","disabled",B]},outputs:{closed:"closed",opened:"opened",destroyed:"destroyed",expandedChange:"expandedChange"},exportAs:["cdkAccordionItem"],features:[we([{provide:gU,useValue:void 0}])]})}return t})(),bU=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({})}return t})();var jye=["body"],Hye=["bodyWrapper"],zye=[[["mat-expansion-panel-header"]],"*",[["mat-action-row"]]],Uye=["mat-expansion-panel-header","*","mat-action-row"];function $ye(t,n){}var Wye=[[["mat-panel-title"]],[["mat-panel-description"]],"*"],Gye=["mat-panel-title","mat-panel-description","*"];function qye(t,n){t&1&&(gt(0,"span",1),ii(),gt(1,"svg",2),ni(2,"path",3),yt()())}var vU=new O("MAT_ACCORDION"),yU=new O("MAT_EXPANSION_PANEL"),Yye=(()=>{class t{_template=u(te);_expansionPanel=u(yU,{optional:!0});constructor(){}static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["ng-template","matExpansionPanelContent",""]]})}return t})(),xU=new O("MAT_EXPANSION_PANEL_DEFAULT_OPTIONS"),_A=(()=>{class t extends _U{_viewContainerRef=u(st);_animationsDisabled=Qe();_document=u(_e);_ngZone=u(ae);_elementRef=u(Y);_renderer=u(ze);_cleanupTransitionEnd;get hideToggle(){return this._hideToggle||this.accordion&&this.accordion.hideToggle}set hideToggle(e){this._hideToggle=e}_hideToggle=!1;get togglePosition(){return this._togglePosition||this.accordion&&this.accordion.togglePosition}set togglePosition(e){this._togglePosition=e}_togglePosition;afterExpand=new U;afterCollapse=new U;_inputChanges=new z;accordion=u(vU,{optional:!0,skipSelf:!0});_lazyContent;_body;_bodyWrapper;_portal;_headerId=u(et).getId("mat-expansion-panel-header-");constructor(){super();let e=u(xU,{optional:!0});this._expansionDispatcher=u(hu),e&&(this.hideToggle=e.hideToggle)}_hasSpacing(){return this.accordion?this.expanded&&this.accordion.displayMode==="default":!1}_getExpandedState(){return this.expanded?"expanded":"collapsed"}toggle(){this.expanded=!this.expanded}close(){this.expanded=!1}open(){this.expanded=!0}ngAfterContentInit(){this._lazyContent&&this._lazyContent._expansionPanel===this&&this.opened.pipe(Ue(null),ce(()=>this.expanded&&!this._portal),mt(1)).subscribe(()=>{this._portal=new kn(this._lazyContent._template,this._viewContainerRef)}),this._setupAnimationEvents()}ngOnChanges(e){this._inputChanges.next(e)}ngOnDestroy(){super.ngOnDestroy(),this._cleanupTransitionEnd?.(),this._inputChanges.complete()}_containsFocus(){if(this._body){let e=this._document.activeElement,i=this._body.nativeElement;return e===i||i.contains(e)}return!1}_transitionEndListener=({target:e,propertyName:i})=>{e===this._bodyWrapper?.nativeElement&&i==="grid-template-rows"&&this._ngZone.run(()=>{this.expanded?this.afterExpand.emit():this.afterCollapse.emit()})};_setupAnimationEvents(){this._ngZone.runOutsideAngular(()=>{this._animationsDisabled?(this.opened.subscribe(()=>this._ngZone.run(()=>this.afterExpand.emit())),this.closed.subscribe(()=>this._ngZone.run(()=>this.afterCollapse.emit()))):setTimeout(()=>{let e=this._elementRef.nativeElement;this._cleanupTransitionEnd=this._renderer.listen(e,"transitionend",this._transitionEndListener),e.classList.add("mat-expansion-panel-animations-enabled")},200)})}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-expansion-panel"]],contentQueries:function(i,r,o){if(i&1&&Ce(o,Yye,5),i&2){let a;j(a=H())&&(r._lazyContent=a.first)}},viewQuery:function(i,r){if(i&1&&(ie(jye,5),ie(Hye,5)),i&2){let o;j(o=H())&&(r._body=o.first),j(o=H())&&(r._bodyWrapper=o.first)}},hostAttrs:[1,"mat-expansion-panel"],hostVars:4,hostBindings:function(i,r){i&2&&G("mat-expanded",r.expanded)("mat-expansion-panel-spacing",r._hasSpacing())},inputs:{hideToggle:[2,"hideToggle","hideToggle",B],togglePosition:"togglePosition"},outputs:{afterExpand:"afterExpand",afterCollapse:"afterCollapse"},exportAs:["matExpansionPanel"],features:[we([{provide:vU,useValue:void 0},{provide:yU,useExisting:t}]),le,Oe],ngContentSelectors:Uye,decls:9,vars:4,consts:[["bodyWrapper",""],["body",""],[1,"mat-expansion-panel-content-wrapper"],["role","region",1,"mat-expansion-panel-content",3,"id"],[1,"mat-expansion-panel-body"],[3,"cdkPortalOutlet"]],template:function(i,r){i&1&&(Ee(zye),ne(0),m(1,"div",2,0)(3,"div",3,1)(5,"div",4),ne(6,1),A(7,$ye,0,0,"ng-template",5),h(),ne(8,2),h()()),i&2&&(g(),X("inert",r.expanded?null:""),g(2),v("id",r.id),X("aria-labelledby",r._headerId),g(4),v("cdkPortalOutlet",r._portal))},dependencies:[Ir],styles:[`.mat-expansion-panel{box-sizing:content-box;display:block;margin:0;overflow:hidden;position:relative;background:var(--mat-expansion-container-background-color, var(--mat-sys-surface));color:var(--mat-expansion-container-text-color, var(--mat-sys-on-surface));border-radius:var(--mat-expansion-container-shape, 12px)}.mat-expansion-panel.mat-expansion-panel-animations-enabled{transition:margin 225ms cubic-bezier(0.4, 0, 0.2, 1),box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-expansion-panel:not([class*=mat-elevation-z]){box-shadow:var(--mat-expansion-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12))}.mat-accordion .mat-expansion-panel:not(.mat-expanded),.mat-accordion .mat-expansion-panel:not(.mat-expansion-panel-spacing){border-radius:0}.mat-accordion .mat-expansion-panel:first-of-type{border-top-right-radius:var(--mat-expansion-container-shape, 12px);border-top-left-radius:var(--mat-expansion-container-shape, 12px)}.mat-accordion .mat-expansion-panel:last-of-type{border-bottom-right-radius:var(--mat-expansion-container-shape, 12px);border-bottom-left-radius:var(--mat-expansion-container-shape, 12px)}@media(forced-colors: active){.mat-expansion-panel{outline:solid 1px}}.mat-expansion-panel-content-wrapper{display:grid;grid-template-rows:0fr;grid-template-columns:100%}.mat-expansion-panel-animations-enabled .mat-expansion-panel-content-wrapper{transition:grid-template-rows 225ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-expansion-panel.mat-expanded>.mat-expansion-panel-content-wrapper{grid-template-rows:1fr}@supports not (grid-template-rows: 0fr){.mat-expansion-panel-content-wrapper{height:0}.mat-expansion-panel.mat-expanded>.mat-expansion-panel-content-wrapper{height:auto}}.mat-expansion-panel-content{display:flex;flex-direction:column;overflow:visible;min-height:0;visibility:hidden;font-family:var(--mat-expansion-container-text-font, var(--mat-sys-body-large-font));font-size:var(--mat-expansion-container-text-size, var(--mat-sys-body-large-size));font-weight:var(--mat-expansion-container-text-weight, var(--mat-sys-body-large-weight));line-height:var(--mat-expansion-container-text-line-height, var(--mat-sys-body-large-line-height));letter-spacing:var(--mat-expansion-container-text-tracking, var(--mat-sys-body-large-tracking))}.mat-expansion-panel-animations-enabled .mat-expansion-panel-content{transition:visibility 190ms linear}.mat-expansion-panel.mat-expanded>.mat-expansion-panel-content-wrapper>.mat-expansion-panel-content{visibility:visible}.mat-expansion-panel-body{padding:0 24px 16px}.mat-expansion-panel-spacing{margin:16px 0}.mat-accordion>.mat-expansion-panel-spacing:first-child,.mat-accordion>*:first-child:not(.mat-expansion-panel) .mat-expansion-panel-spacing{margin-top:0}.mat-accordion>.mat-expansion-panel-spacing:last-child,.mat-accordion>*:last-child:not(.mat-expansion-panel) .mat-expansion-panel-spacing{margin-bottom:0}.mat-action-row{border-top-style:solid;border-top-width:1px;display:flex;flex-direction:row;justify-content:flex-end;padding:16px 8px 16px 24px;border-top-color:var(--mat-expansion-actions-divider-color, var(--mat-sys-outline))}.mat-action-row .mat-button-base,.mat-action-row .mat-mdc-button-base{margin-left:8px}[dir=rtl] .mat-action-row .mat-button-base,[dir=rtl] .mat-action-row .mat-mdc-button-base{margin-left:0;margin-right:8px} +`],encapsulation:2,changeDetection:0})}return t})();var CU=(()=>{class t{panel=u(_A,{host:!0});_element=u(Y);_focusMonitor=u(oi);_changeDetectorRef=u(ye);_parentChangeSubscription=ke.EMPTY;constructor(){u(ft).load(Oi);let e=this.panel,i=u(xU,{optional:!0}),r=u(new Li("tabindex"),{optional:!0}),o=e.accordion?e.accordion._stateChanges.pipe(ce(a=>!!(a.hideToggle||a.togglePosition))):zi;this.tabIndex=parseInt(r||"")||0,this._parentChangeSubscription=it(e.opened,e.closed,o,e._inputChanges.pipe(ce(a=>!!(a.hideToggle||a.disabled||a.togglePosition)))).subscribe(()=>this._changeDetectorRef.markForCheck()),e.closed.pipe(ce(()=>e._containsFocus())).subscribe(()=>this._focusMonitor.focusVia(this._element,"program")),i&&(this.expandedHeight=i.expandedHeight,this.collapsedHeight=i.collapsedHeight)}expandedHeight;collapsedHeight;tabIndex=0;get disabled(){return this.panel.disabled}_toggle(){this.disabled||this.panel.toggle()}_isExpanded(){return this.panel.expanded}_getExpandedState(){return this.panel._getExpandedState()}_getPanelId(){return this.panel.id}_getTogglePosition(){return this.panel.togglePosition}_showToggle(){return!this.panel.hideToggle&&!this.panel.disabled}_getHeaderHeight(){let e=this._isExpanded();return e&&this.expandedHeight?this.expandedHeight:!e&&this.collapsedHeight?this.collapsedHeight:null}_keydown(e){switch(e.keyCode){case 32:case 13:Gt(e)||(e.preventDefault(),this._toggle());break;default:this.panel.accordion&&this.panel.accordion._handleHeaderKeydown(e);return}}focus(e,i){e?this._focusMonitor.focusVia(this._element,e,i):this._element.nativeElement.focus(i)}ngAfterViewInit(){this._focusMonitor.monitor(this._element).subscribe(e=>{e&&this.panel.accordion&&this.panel.accordion._handleHeaderFocus(this)})}ngOnDestroy(){this._parentChangeSubscription.unsubscribe(),this._focusMonitor.stopMonitoring(this._element)}static \u0275fac=function(i){return new(i||t)};static \u0275cmp=E({type:t,selectors:[["mat-expansion-panel-header"]],hostAttrs:["role","button",1,"mat-expansion-panel-header","mat-focus-indicator"],hostVars:13,hostBindings:function(i,r){i&1&&S("click",function(){return r._toggle()})("keydown",function(a){return r._keydown(a)}),i&2&&(X("id",r.panel._headerId)("tabindex",r.disabled?-1:r.tabIndex)("aria-controls",r._getPanelId())("aria-expanded",r._isExpanded())("aria-disabled",r.panel.disabled),At("height",r._getHeaderHeight()),G("mat-expanded",r._isExpanded())("mat-expansion-toggle-indicator-after",r._getTogglePosition()==="after")("mat-expansion-toggle-indicator-before",r._getTogglePosition()==="before"))},inputs:{expandedHeight:"expandedHeight",collapsedHeight:"collapsedHeight",tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:ht(e)]},ngContentSelectors:Gye,decls:5,vars:3,consts:[[1,"mat-content"],[1,"mat-expansion-indicator"],["xmlns","http://www.w3.org/2000/svg","viewBox","0 -960 960 960","aria-hidden","true","focusable","false"],["d","M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z"]],template:function(i,r){i&1&&(Ee(Wye),gt(0,"span",0),ne(1),ne(2,1),ne(3,2),yt(),L(4,qye,3,0,"span",1)),i&2&&(G("mat-content-hide-toggle",!r._showToggle()),g(4),V(r._showToggle()?4:-1))},styles:[`.mat-expansion-panel-header{display:flex;flex-direction:row;align-items:center;padding:0 24px;border-radius:inherit;height:var(--mat-expansion-header-collapsed-state-height, 48px);font-family:var(--mat-expansion-header-text-font, var(--mat-sys-title-medium-font));font-size:var(--mat-expansion-header-text-size, var(--mat-sys-title-medium-size));font-weight:var(--mat-expansion-header-text-weight, var(--mat-sys-title-medium-weight));line-height:var(--mat-expansion-header-text-line-height, var(--mat-sys-title-medium-line-height));letter-spacing:var(--mat-expansion-header-text-tracking, var(--mat-sys-title-medium-tracking))}.mat-expansion-panel-animations-enabled .mat-expansion-panel-header{transition:height 225ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-expansion-panel-header::before{border-radius:inherit}.mat-expansion-panel-header.mat-expanded{height:var(--mat-expansion-header-expanded-state-height, 64px)}.mat-expansion-panel-header[aria-disabled=true]{color:var(--mat-expansion-header-disabled-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent))}.mat-expansion-panel-header:not([aria-disabled=true]){cursor:pointer}.mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover{background:var(--mat-expansion-header-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent))}@media(hover: none){.mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover{background:var(--mat-expansion-container-background-color, var(--mat-sys-surface))}}.mat-expansion-panel .mat-expansion-panel-header:not([aria-disabled=true]).cdk-keyboard-focused,.mat-expansion-panel .mat-expansion-panel-header:not([aria-disabled=true]).cdk-program-focused{background:var(--mat-expansion-header-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent))}.mat-expansion-panel-header._mat-animation-noopable{transition:none}.mat-expansion-panel-header:focus,.mat-expansion-panel-header:hover{outline:none}.mat-expansion-panel-header.mat-expanded:focus,.mat-expansion-panel-header.mat-expanded:hover{background:inherit}.mat-expansion-panel-header.mat-expansion-toggle-indicator-before{flex-direction:row-reverse}.mat-expansion-panel-header.mat-expansion-toggle-indicator-before .mat-expansion-indicator{margin:0 16px 0 0}[dir=rtl] .mat-expansion-panel-header.mat-expansion-toggle-indicator-before .mat-expansion-indicator{margin:0 0 0 16px}.mat-content{display:flex;flex:1;flex-direction:row;overflow:hidden}.mat-content.mat-content-hide-toggle{margin-right:8px}[dir=rtl] .mat-content.mat-content-hide-toggle{margin-right:0;margin-left:8px}.mat-expansion-toggle-indicator-before .mat-content.mat-content-hide-toggle{margin-left:24px;margin-right:0}[dir=rtl] .mat-expansion-toggle-indicator-before .mat-content.mat-content-hide-toggle{margin-right:24px;margin-left:0}.mat-expansion-panel-header-title{color:var(--mat-expansion-header-text-color, var(--mat-sys-on-surface))}.mat-expansion-panel-header-title,.mat-expansion-panel-header-description{display:flex;flex-grow:1;flex-basis:0;margin-right:16px;align-items:center}[dir=rtl] .mat-expansion-panel-header-title,[dir=rtl] .mat-expansion-panel-header-description{margin-right:0;margin-left:16px}.mat-expansion-panel-header[aria-disabled=true] .mat-expansion-panel-header-title,.mat-expansion-panel-header[aria-disabled=true] .mat-expansion-panel-header-description{color:inherit}.mat-expansion-panel-header-description{flex-grow:2;color:var(--mat-expansion-header-description-color, var(--mat-sys-on-surface-variant))}.mat-expansion-panel-animations-enabled .mat-expansion-indicator{transition:transform 225ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-expansion-panel-header.mat-expanded .mat-expansion-indicator{transform:rotate(180deg)}.mat-expansion-indicator::after{border-style:solid;border-width:0 2px 2px 0;content:"";padding:3px;transform:rotate(45deg);vertical-align:middle;color:var(--mat-expansion-header-indicator-color, var(--mat-sys-on-surface-variant));display:var(--mat-expansion-legacy-header-indicator-display, none)}.mat-expansion-indicator svg{width:24px;height:24px;margin:0 -8px;vertical-align:middle;fill:var(--mat-expansion-header-indicator-color, var(--mat-sys-on-surface-variant));display:var(--mat-expansion-header-indicator-display, inline-block)}@media(forced-colors: active){.mat-expansion-panel-content{border-top:1px solid;border-top-left-radius:0;border-top-right-radius:0}} +`],encapsulation:2,changeDetection:0})}return t})();var wU=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275dir=P({type:t,selectors:[["mat-panel-title"]],hostAttrs:[1,"mat-expansion-panel-header-title"]})}return t})();var DU=(()=>{class t{static \u0275fac=function(i){return new(i||t)};static \u0275mod=ee({type:t});static \u0275inj=J({imports:[De,bU,Oo]})}return t})();function Kye(t,n){if(t&1&&(lt(0),m(1,"mat-icon"),f(2,"account_circle"),h(),f(3),ot()),t&2){let e=n.ngIf;g(3),fe(" ",e.name||e.preferred_username||"User Profile"," ")}}function Zye(t,n){t&1&&(lt(0),m(1,"mat-icon"),f(2,"account_circle"),h(),f(3," User Profile "),ot())}function Xye(t,n){t&1&&(m(0,"div",3)(1,"mat-card")(2,"mat-card-content")(3,"div",4)(4,"mat-icon"),f(5,"lock"),h(),m(6,"h2"),f(7,"Not Authenticated"),h(),m(8,"p"),f(9,"Please log in to view your profile information."),h(),m(10,"button",5)(11,"mat-icon"),f(12,"login"),h(),f(13," Log In "),h()()()()())}function Jye(t,n){if(t&1&&(m(0,"mat-chip",23),f(1),h()),t&2){let e=n.$implicit;v("color","primary"),g(),fe(" ",e," ")}}function exe(t,n){if(t&1&&(m(0,"mat-chip",24),f(1),h()),t&2){let e=n.$implicit;v("color","accent"),g(),fe(" ",e," ")}}function txe(t,n){if(t&1&&(m(0,"div",17)(1,"div",18)(2,"label"),f(3,"User ID (sub)"),h(),m(4,"div",19),f(5),h()(),m(6,"div",18)(7,"label"),f(8,"Display Name"),h(),m(9,"div",19),f(10),h()(),m(11,"div",18)(12,"label"),f(13,"Email"),h(),m(14,"div",19),f(15),h()(),m(16,"div",18)(17,"label"),f(18,"Username"),h(),m(19,"div",19),f(20),h()(),m(21,"div",20)(22,"label"),f(23,"Roles"),h(),m(24,"div",19)(25,"mat-chip-set"),A(26,Jye,2,2,"mat-chip",21),h()()(),m(27,"div",20)(28,"label"),f(29,"Permissions"),h(),m(30,"div",19)(31,"mat-chip-set"),A(32,exe,2,2,"mat-chip",22),h()()()()),t&2){let e=n.ngIf,i=x(2);g(5),N(e.sub),g(5),N(e.name||"N/A"),g(5),N(e.email||"N/A"),g(5),N(e.preferred_username||"N/A"),g(6),v("ngForOf",i.profileData==null?null:i.profileData.roles),g(6),v("ngForOf",i.profileData==null?null:i.profileData.permissions)}}function ixe(t,n){t&1&&(m(0,"mat-chip",36)(1,"mat-icon"),f(2,"verified_user"),h(),f(3," Valid "),h())}function nxe(t,n){t&1&&(m(0,"mat-chip",37)(1,"mat-icon"),f(2,"error"),h(),f(3," Expired "),h())}function rxe(t,n){if(t&1&&(m(0,"div",30)(1,"label"),f(2,"Issuer"),h(),m(3,"div"),f(4),h()()),t&2){let e=x().ngIf;g(4),N(e.payload.iss)}}function oxe(t,n){if(t&1&&(m(0,"mat-expansion-panel",38)(1,"mat-expansion-panel-header")(2,"mat-panel-title"),f(3,"Raw JWT Token"),h()(),m(4,"pre",39),f(5),h()()),t&2){let e=x().ngIf;g(5),N(e.raw)}}function axe(t,n){if(t&1){let e=q();m(0,"div",25)(1,"div",26)(2,"mat-chip-set")(3,"mat-chip",23)(4,"mat-icon"),f(5,"schedule"),h(),f(6),h(),A(7,ixe,4,0,"mat-chip",27)(8,nxe,4,0,"mat-chip",28),h()(),m(9,"div",29)(10,"div",30)(11,"label"),f(12,"Issued At"),h(),m(13,"div"),f(14),h()(),m(15,"div",30)(16,"label"),f(17,"Expires At"),h(),m(18,"div"),f(19),h()(),A(20,rxe,5,1,"div",31),h(),m(21,"div",32)(22,"button",33),S("click",function(){k(e);let r=x(2);return T(r.toggleRawIdToken())}),m(23,"mat-icon"),f(24),h(),f(25),h(),m(26,"button",33),S("click",function(){let r=k(e).ngIf,o=x(2);return T(o.copyToClipboard(r.raw,"ID Token"))}),m(27,"mat-icon"),f(28,"content_copy"),h(),f(29," Copy Token "),h()(),A(30,oxe,6,1,"mat-expansion-panel",34),m(31,"mat-expansion-panel")(32,"mat-expansion-panel-header")(33,"mat-panel-title"),f(34,"Token Header"),h()(),m(35,"pre",35),f(36),h()(),m(37,"mat-expansion-panel")(38,"mat-expansion-panel-header")(39,"mat-panel-title"),f(40,"Token Claims (Payload)"),h()(),m(41,"pre",35),f(42),h()()()}if(t&2){let e=n.ngIf,i=x(2);g(3),v("color",i.getExpirationColor(e)),g(3),fe(" ",i.getExpirationStatus(e)," "),g(),v("ngIf",!e.isExpired),g(),v("ngIf",e.isExpired),g(6),N(i.formatDate(e.issuedAt)),g(5),N(i.formatDate(e.expiresAt)),g(),v("ngIf",e.payload.iss),g(4),N(i.showRawIdToken?"visibility_off":"visibility"),g(),fe(" ",i.showRawIdToken?"Hide":"Show"," Raw Token "),g(5),v("ngIf",i.showRawIdToken),g(6),N(i.formatJson(e.header)),g(6),N(i.formatJson(e.payload))}}function sxe(t,n){t&1&&(m(0,"div",40)(1,"mat-icon"),f(2,"info"),h(),m(3,"p"),f(4,"ID Token not available"),h()())}function lxe(t,n){t&1&&(m(0,"mat-chip",36)(1,"mat-icon"),f(2,"verified_user"),h(),f(3," Valid "),h())}function cxe(t,n){t&1&&(m(0,"mat-chip",37)(1,"mat-icon"),f(2,"error"),h(),f(3," Expired "),h())}function dxe(t,n){if(t&1&&(m(0,"div",30)(1,"label"),f(2,"Audience"),h(),m(3,"div"),f(4),h()()),t&2){let e=x().ngIf;g(4),N(e.payload.aud)}}function uxe(t,n){if(t&1&&(m(0,"mat-expansion-panel",38)(1,"mat-expansion-panel-header")(2,"mat-panel-title"),f(3,"Raw JWT Token"),h()(),m(4,"pre",39),f(5),h()()),t&2){let e=x().ngIf;g(5),N(e.raw)}}function mxe(t,n){if(t&1&&(m(0,"mat-chip"),f(1),h()),t&2){let e=n.$implicit;g(),fe(" ",e," ")}}function hxe(t,n){if(t&1&&(m(0,"mat-expansion-panel")(1,"mat-expansion-panel-header")(2,"mat-panel-title"),f(3,"Scopes"),h()(),m(4,"div",41)(5,"mat-chip-set"),A(6,mxe,2,1,"mat-chip",42),h()()()),t&2){let e=x().ngIf,i=x(2);g(6),v("ngForOf",i.getScopes(e.payload.scope))}}function pxe(t,n){if(t&1){let e=q();m(0,"div",25)(1,"div",26)(2,"mat-chip-set")(3,"mat-chip",23)(4,"mat-icon"),f(5,"schedule"),h(),f(6),h(),A(7,lxe,4,0,"mat-chip",27)(8,cxe,4,0,"mat-chip",28),h()(),m(9,"div",29)(10,"div",30)(11,"label"),f(12,"Issued At"),h(),m(13,"div"),f(14),h()(),m(15,"div",30)(16,"label"),f(17,"Expires At"),h(),m(18,"div"),f(19),h()(),A(20,dxe,5,1,"div",31),h(),m(21,"div",32)(22,"button",33),S("click",function(){k(e);let r=x(2);return T(r.toggleRawAccessToken())}),m(23,"mat-icon"),f(24),h(),f(25),h(),m(26,"button",33),S("click",function(){let r=k(e).ngIf,o=x(2);return T(o.copyToClipboard(r.raw,"Access Token"))}),m(27,"mat-icon"),f(28,"content_copy"),h(),f(29," Copy Token "),h()(),A(30,uxe,6,1,"mat-expansion-panel",34),m(31,"mat-expansion-panel")(32,"mat-expansion-panel-header")(33,"mat-panel-title"),f(34,"Token Header"),h()(),m(35,"pre",35),f(36),h()(),m(37,"mat-expansion-panel")(38,"mat-expansion-panel-header")(39,"mat-panel-title"),f(40,"Token Claims (Payload)"),h()(),m(41,"pre",35),f(42),h()(),A(43,hxe,7,1,"mat-expansion-panel",0),h()}if(t&2){let e=n.ngIf,i=x(2);g(3),v("color",i.getExpirationColor(e)),g(3),fe(" ",i.getExpirationStatus(e)," "),g(),v("ngIf",!e.isExpired),g(),v("ngIf",e.isExpired),g(6),N(i.formatDate(e.issuedAt)),g(5),N(i.formatDate(e.expiresAt)),g(),v("ngIf",e.payload.aud),g(4),N(i.showRawAccessToken?"visibility_off":"visibility"),g(),fe(" ",i.showRawAccessToken?"Hide":"Show"," Raw Token "),g(5),v("ngIf",i.showRawAccessToken),g(6),N(i.formatJson(e.header)),g(6),N(i.formatJson(e.payload)),g(),v("ngIf",e.payload.scope)}}function fxe(t,n){t&1&&(m(0,"div",40)(1,"mat-icon"),f(2,"info"),h(),m(3,"p"),f(4,"Access Token not available"),h()())}function gxe(t,n){if(t&1&&(m(0,"div",6)(1,"mat-card",7)(2,"mat-card-content")(3,"div",8)(4,"mat-icon"),f(5,"warning"),h(),m(6,"div")(7,"strong"),f(8,"Security Notice:"),h(),f(9," Tokens displayed on this page contain sensitive information. Do not share these tokens with anyone or paste them in untrusted applications. "),h()()()(),m(10,"mat-card",9)(11,"mat-card-header")(12,"mat-icon",10),f(13,"person"),h(),m(14,"mat-card-title"),f(15,"User Information"),h(),m(16,"mat-card-subtitle"),f(17,"Your profile details from ID token"),h()(),m(18,"mat-card-content"),A(19,txe,33,6,"div",11),h()(),m(20,"mat-card",12)(21,"mat-card-header")(22,"mat-icon",10),f(23,"vpn_key"),h(),m(24,"mat-card-title"),f(25,"Token Details"),h(),m(26,"mat-card-subtitle"),f(27,"Detailed JWT token information and claims"),h()(),m(28,"mat-card-content")(29,"mat-tab-group")(30,"mat-tab",13),A(31,axe,43,12,"div",14)(32,sxe,5,0,"div",15),h(),m(33,"mat-tab",16),A(34,pxe,44,13,"div",14)(35,fxe,5,0,"div",15),h()()()()()),t&2){let e=x();g(19),v("ngIf",e.profileData==null?null:e.profileData.userInfo),g(12),v("ngIf",e.profileData==null?null:e.profileData.idToken),g(),v("ngIf",!(e.profileData!=null&&e.profileData.idToken)),g(2),v("ngIf",e.profileData==null?null:e.profileData.accessToken),g(),v("ngIf",!(e.profileData!=null&&e.profileData.accessToken))}}var MU=(()=>{let n=class n{constructor(){this.authService=u(jt),this.oauthService=u(Km),this.tokenDecoder=u(p6),this.snackBar=u(_i),this.router=u(Ae),this.profileData=null,this.showRawIdToken=!1,this.showRawAccessToken=!1}ngOnInit(){this.loadProfileData(),this.authSubscription=this.authService.isAuthenticated$.subscribe(()=>{this.loadProfileData()})}ngOnDestroy(){this.authSubscription?.unsubscribe()}loadProfileData(){let i=this.authService.isAuthenticated(),r=this.oauthService.hasValidAccessToken();if(console.log("Authentication Status:",{isAuthenticated:i,hasValidAccessToken:r}),!i&&!r){this.profileData=null;return}let o=this.authService.getUserInfo(),a=this.authService.getAccessToken(),s=this.oauthService.getIdToken(),l=this.authService.getUserRoles();console.log("Profile Data Debug:",{isAuthenticated:i,hasUserInfo:!!o,hasAccessToken:!!a,accessTokenLength:a?.length,hasIdToken:!!s,idTokenLength:s?.length,roles:l}),this.profileData={isAuthenticated:i,userInfo:o,idToken:s?this.tokenDecoder.decodeToken(s):null,accessToken:a?this.tokenDecoder.decodeToken(a):null,roles:l,permissions:this.getUserPermissions()},console.log("Decoded tokens:",{idToken:this.profileData.idToken,accessToken:this.profileData.accessToken})}getUserPermissions(){let i=[];return this.authService.hasRole("HRAdmin")||this.authService.hasRole("Manager")?i.push("canAdd","canEdit","canDelete","canRead"):this.authService.hasRole("Employee")&&i.push("canRead"),i}toggleRawIdToken(){this.showRawIdToken=!this.showRawIdToken}toggleRawAccessToken(){this.showRawAccessToken=!this.showRawAccessToken}copyToClipboard(i,r){navigator.clipboard.writeText(i).then(()=>{this.showMessage(`${r} copied to clipboard`)}).catch(o=>{console.error("Failed to copy:",o),this.showMessage("Failed to copy to clipboard")})}formatJson(i){return JSON.stringify(i,null,2)}getExpirationStatus(i){return this.tokenDecoder.getExpirationStatus(i)}getExpirationColor(i){if(!i||!i.expiresIn)return"";let r=i.expiresIn/3600;return i.isExpired?"warn":r<1?"accent":"primary"}formatDate(i){return i?new Intl.DateTimeFormat("en-US",{dateStyle:"medium",timeStyle:"medium"}).format(i):"N/A"}showMessage(i){this.snackBar.open(i,"Close",{duration:3e3,horizontalPosition:"end",verticalPosition:"top"})}getScopes(i){return i?Array.isArray(i)?i:typeof i=="string"?i.split(" ").filter(r=>r.length>0):[]:[]}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-profile-overview"]],decls:5,vars:4,consts:[[4,"ngIf"],["class","not-authenticated",4,"ngIf"],["class","profile-container",4,"ngIf"],[1,"not-authenticated"],[1,"empty-state"],["mat-raised-button","","color","primary","routerLink","/login"],[1,"profile-container"],[1,"security-warning"],[1,"warning-content"],[1,"profile-card"],["mat-card-avatar",""],["class","info-grid",4,"ngIf"],[1,"tokens-card"],["label","ID Token"],["class","token-content",4,"ngIf"],["class","no-token",4,"ngIf"],["label","Access Token"],[1,"info-grid"],[1,"info-item"],[1,"value"],[1,"info-item","full-width"],["highlighted","",3,"color",4,"ngFor","ngForOf"],[3,"color",4,"ngFor","ngForOf"],["highlighted","",3,"color"],[3,"color"],[1,"token-content"],[1,"token-status"],["color","primary",4,"ngIf"],["color","warn",4,"ngIf"],[1,"token-metadata"],[1,"metadata-item"],["class","metadata-item",4,"ngIf"],[1,"token-actions"],["mat-button","",3,"click"],["expanded","",4,"ngIf"],[1,"json-display"],["color","primary"],["color","warn"],["expanded",""],[1,"token-display"],[1,"no-token"],[1,"scopes-list"],[4,"ngFor","ngForOf"]],template:function(r,o){r&1&&(m(0,"page-header"),A(1,Kye,4,1,"ng-container",0)(2,Zye,4,0,"ng-container",0),h(),A(3,Xye,14,0,"div",1)(4,gxe,36,5,"div",2)),r&2&&(g(),v("ngIf",o.profileData==null?null:o.profileData.userInfo),g(),v("ngIf",!(o.profileData!=null&&o.profileData.isAuthenticated)),g(),v("ngIf",!(o.profileData!=null&&o.profileData.isAuthenticated)),g(),v("ngIf",o.profileData==null?null:o.profileData.isAuthenticated))},dependencies:[Je,Un,Wt,It,kt,Hs,Tt,Rt,js,Ot,Ge,Ze,Fe,_t,Ow,gA,fU,DU,_A,CU,wU,An,wi,cp,C_,cw,fS,Wn,Lt],styles:[".profile-container[_ngcontent-%COMP%]{padding:16px;max-width:1200px;margin:0 auto;display:flex;flex-direction:column;gap:24px}.not-authenticated[_ngcontent-%COMP%]{padding:16px;max-width:600px;margin:0 auto}.not-authenticated[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;text-align:center}.not-authenticated[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:64px;width:64px;height:64px;color:#00000061;margin-bottom:16px}.not-authenticated[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] h2[_ngcontent-%COMP%]{margin:0 0 8px;font-size:24px;font-weight:500}.not-authenticated[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0 0 24px;color:#0009}.not-authenticated[_ngcontent-%COMP%] .empty-state[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:20px;width:20px;height:20px;margin-right:8px;color:inherit}.security-warning[_ngcontent-%COMP%]{background-color:#fff3cd;border-left:4px solid #ff9800}.security-warning[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:16px}.security-warning[_ngcontent-%COMP%] .warning-content[_ngcontent-%COMP%]{display:flex;align-items:center;gap:12px}.security-warning[_ngcontent-%COMP%] .warning-content[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{color:#ff9800;flex-shrink:0}.security-warning[_ngcontent-%COMP%] .warning-content[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font-weight:600}.profile-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%], .tokens-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%]{padding:16px;border-bottom:1px solid rgba(0,0,0,.12)}.profile-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%], .tokens-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] mat-card-title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0}.profile-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] mat-card-subtitle[_ngcontent-%COMP%], .tokens-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] mat-card-subtitle[_ngcontent-%COMP%]{margin-top:4px;color:#0009}.profile-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%], .tokens-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:24px}.info-grid[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(2,1fr);gap:24px}@media (max-width: 768px){.info-grid[_ngcontent-%COMP%]{grid-template-columns:1fr}}.info-grid[_ngcontent-%COMP%] .info-item[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:8px}.info-grid[_ngcontent-%COMP%] .info-item.full-width[_ngcontent-%COMP%]{grid-column:1/-1}.info-grid[_ngcontent-%COMP%] .info-item[_ngcontent-%COMP%] label[_ngcontent-%COMP%]{font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:.5px;color:#0009}.info-grid[_ngcontent-%COMP%] .info-item[_ngcontent-%COMP%] .value[_ngcontent-%COMP%]{font-size:16px;color:#000000de;word-break:break-word}.token-content[_ngcontent-%COMP%]{padding:24px 0;display:flex;flex-direction:column;gap:16px}.token-content[_ngcontent-%COMP%] .token-status[_ngcontent-%COMP%] mat-chip-set[_ngcontent-%COMP%] mat-chip[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px;font-size:18px;width:18px;height:18px}.token-content[_ngcontent-%COMP%] .token-metadata[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;padding:16px;background-color:#00000005;border-radius:4px}.token-content[_ngcontent-%COMP%] .token-metadata[_ngcontent-%COMP%] .metadata-item[_ngcontent-%COMP%] label[_ngcontent-%COMP%]{display:block;font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:.5px;color:#0009;margin-bottom:4px}.token-content[_ngcontent-%COMP%] .token-metadata[_ngcontent-%COMP%] .metadata-item[_ngcontent-%COMP%] div[_ngcontent-%COMP%]{font-size:14px;color:#000000de;word-break:break-word}.token-content[_ngcontent-%COMP%] .token-actions[_ngcontent-%COMP%]{display:flex;gap:8px;flex-wrap:wrap}.token-content[_ngcontent-%COMP%] .token-actions[_ngcontent-%COMP%] button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin-right:4px}.token-content[_ngcontent-%COMP%] mat-expansion-panel[_ngcontent-%COMP%]{margin-top:8px}.token-content[_ngcontent-%COMP%] .token-display[_ngcontent-%COMP%], .token-content[_ngcontent-%COMP%] .json-display[_ngcontent-%COMP%]{background-color:#f5f5f5;padding:16px;border-radius:4px;overflow-x:auto;font-family:Courier New,monospace;font-size:12px;line-height:1.5;margin:0;white-space:pre-wrap;word-break:break-all}.token-content[_ngcontent-%COMP%] .scopes-list[_ngcontent-%COMP%]{padding:16px}.token-content[_ngcontent-%COMP%] .scopes-list[_ngcontent-%COMP%] mat-chip-set[_ngcontent-%COMP%] mat-chip[_ngcontent-%COMP%]{margin:4px}.no-token[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;text-align:center;color:#00000061}.no-token[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:12px}.no-token[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:14px}mat-chip-set[_ngcontent-%COMP%] mat-chip[_ngcontent-%COMP%]{margin:4px}@media (max-width: 768px){.profile-container[_ngcontent-%COMP%]{padding:12px}.profile-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%], .tokens-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:16px}.token-content[_ngcontent-%COMP%]{padding:16px 0}.token-content[_ngcontent-%COMP%] .token-metadata[_ngcontent-%COMP%]{grid-template-columns:1fr}.security-warning[_ngcontent-%COMP%] .warning-content[_ngcontent-%COMP%]{flex-direction:column;align-items:flex-start}}"]});let t=n;return t})();var EU=(()=>{let n=class n{};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-profile-settings"]],decls:14,vars:0,consts:[[1,"settings-container"],[1,"coming-soon"]],template:function(r,o){r&1&&(m(0,"page-header")(1,"mat-icon"),f(2,"settings"),h(),f(3,` Profile Settings +`),h(),m(4,"div",0)(5,"mat-card")(6,"mat-card-content")(7,"div",1)(8,"mat-icon"),f(9,"construction"),h(),m(10,"h2"),f(11,"Coming Soon"),h(),m(12,"p"),f(13,"User settings functionality will be available in a future update."),h()()()()())},dependencies:[Je,It,kt,Tt,Ge,Ze,Lt],styles:[".settings-container[_ngcontent-%COMP%]{padding:16px;max-width:800px;margin:0 auto}.settings-container[_ngcontent-%COMP%] .coming-soon[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 16px;text-align:center}.settings-container[_ngcontent-%COMP%] .coming-soon[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:64px;width:64px;height:64px;color:#00000061;margin-bottom:16px}.settings-container[_ngcontent-%COMP%] .coming-soon[_ngcontent-%COMP%] h2[_ngcontent-%COMP%]{margin:0 0 8px;font-size:24px;font-weight:500}.settings-container[_ngcontent-%COMP%] .coming-soon[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;color:#0009}"]});let t=n;return t})();function _xe(t,n){t&1&&(m(0,"div",2)(1,"mat-card",3)(2,"mat-card-content")(3,"div",4)(4,"mat-icon"),f(5,"info"),h(),m(6,"div")(7,"strong"),f(8,"AI features are disabled."),h(),m(9,"p"),f(10," To enable AI, set "),m(11,"code"),f(12,"aiEnabled: true"),h(),f(13," in "),m(14,"code"),f(15,"src/environments/environment.ts"),h(),f(16," and "),m(17,"code"),f(18,'"AiEnabled": true'),h(),f(19," in the API's "),m(20,"code"),f(21,"appsettings.json"),h(),f(22,". "),h()()()()()())}function bxe(t,n){if(t&1&&(m(0,"div",20)(1,"mat-icon",21),f(2),h(),m(3,"div",22),f(4),h()()),t&2){let e=n.$implicit;G("user-message",e.role==="user")("assistant-message",e.role==="assistant"),g(2),N(e.role==="user"?"person":"smart_toy"),g(2),N(e.content)}}function vxe(t,n){if(t&1&&(m(0,"div",18),A(1,bxe,5,6,"div",19),h()),t&2){let e=x(2);g(),v("ngForOf",e.messages)}}function yxe(t,n){t&1&&(m(0,"div",23)(1,"mat-icon"),f(2,"chat_bubble_outline"),h(),m(3,"p"),f(4,"Start a conversation"),h()())}function xxe(t,n){t&1&&(m(0,"div",24),M(1,"mat-spinner",25),m(2,"span"),f(3,"Thinking\u2026"),h()())}function Cxe(t,n){if(t&1&&(m(0,"div",26)(1,"mat-icon"),f(2,"error_outline"),h(),m(3,"span"),f(4),h()()),t&2){let e=x(2);g(4),N(e.error)}}function wxe(t,n){if(t&1){let e=q();m(0,"div",5)(1,"mat-card",6)(2,"mat-card-header")(3,"mat-icon",7),f(4,"smart_toy"),h(),m(5,"mat-card-title"),f(6,"AI Assistant"),h(),m(7,"mat-card-subtitle"),f(8,"Ask anything \u2014 general knowledge, writing help, code questions"),h(),m(9,"div",8)(10,"button",9),S("click",function(){k(e);let r=x();return T(r.clear())}),m(11,"mat-icon"),f(12,"delete_sweep"),h()()()(),m(13,"mat-card-content"),A(14,vxe,2,1,"div",10)(15,yxe,5,0,"div",11)(16,xxe,4,0,"div",12)(17,Cxe,5,1,"div",13),h(),M(18,"mat-divider"),m(19,"mat-card-actions",14)(20,"mat-form-field",15)(21,"mat-label"),f(22,"Message"),h(),m(23,"input",16),fn("ngModelChange",function(r){k(e);let o=x();return Mn(o.input,r)||(o.input=r),T(r)}),S("keydown",function(r){k(e);let o=x();return T(o.onKeydown(r))}),h()(),m(24,"button",17),S("click",function(){k(e);let r=x();return T(r.send())}),m(25,"mat-icon"),f(26,"send"),h(),f(27," Send "),h()()()()}if(t&2){let e=x();g(14),v("ngIf",e.messages.length>0),g(),v("ngIf",e.messages.length===0&&!e.loading),g(),v("ngIf",e.loading),g(),v("ngIf",e.error),g(6),pn("ngModel",e.input),v("disabled",e.loading),g(),v("disabled",!e.input.trim()||e.loading)}}var SU=(()=>{let n=class n{constructor(){this.aiService=u(nd),this.destroy$=new z,this.aiEnabled=Ji.aiEnabled,this.messages=[],this.input="",this.loading=!1,this.error=""}send(){let i=this.input.trim();!i||this.loading||(this.messages.push({role:"user",content:i}),this.input="",this.loading=!0,this.error="",this.aiService.chat(i).pipe(xe(this.destroy$)).subscribe({next:r=>{this.messages.push({role:"assistant",content:r.reply}),this.loading=!1},error:r=>{this.error=r?.error?.detail??"Failed to get a response. Is the API running with AiEnabled: true?",this.loading=!1}}))}onKeydown(i){i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),this.send())}clear(){this.messages=[],this.error=""}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-ai-assistant"]],decls:3,vars:2,consts:[["class","ai-disabled-banner",4,"ngIf"],["class","chat-container",4,"ngIf"],[1,"ai-disabled-banner"],[1,"disabled-card"],[1,"disabled-content"],[1,"chat-container"],[1,"chat-card"],["mat-card-avatar",""],[1,"header-actions"],["mat-icon-button","","matTooltip","Clear conversation",3,"click"],["class","message-list",4,"ngIf"],["class","empty-state",4,"ngIf"],["class","loading-row",4,"ngIf"],["class","error-row",4,"ngIf"],[1,"input-area"],["appearance","outline","subscriptSizing","dynamic",1,"message-input"],["matInput","","placeholder","Ask the AI assistant anything\u2026",3,"ngModelChange","keydown","ngModel","disabled"],["mat-fab","","extended","","color","primary",3,"click","disabled"],[1,"message-list"],["class","message",3,"user-message","assistant-message",4,"ngFor","ngForOf"],[1,"message"],[1,"avatar-icon"],[1,"bubble"],[1,"empty-state"],[1,"loading-row"],["diameter","24"],[1,"error-row"]],template:function(r,o){r&1&&(M(0,"page-header"),A(1,_xe,23,0,"div",0)(2,wxe,28,7,"div",1)),r&2&&(g(),v("ngIf",!o.aiEnabled),g(),v("ngIf",o.aiEnabled))},dependencies:[Je,Un,Wt,Qr,di,Pt,Ro,It,kt,Xy,Hs,Tt,Rt,js,Ot,Ge,Ze,Fe,Ft,Dx,Bi,Ci,Xt,gi,ai,Zt,Kt,Nr,Kr,Lt],styles:[".ai-disabled-banner[_ngcontent-%COMP%]{padding:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%]{max-width:720px;margin:0 auto;border-left:4px solid #2196f3}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:20px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:28px;width:28px;height:28px;color:#2196f3;flex-shrink:0;margin-top:2px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font-size:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:8px 0 0;color:#0009;font-size:14px;line-height:1.5}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] code[_ngcontent-%COMP%]{background:#0000000f;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:13px}.chat-container[_ngcontent-%COMP%]{padding:16px;max-width:900px;margin:0 auto}.chat-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:flex-start;padding:16px 16px 0}.chat-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] .mat-mdc-card-header-text[_ngcontent-%COMP%]{flex:1}.chat-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] .header-actions[_ngcontent-%COMP%]{margin-left:auto}.chat-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:16px;min-height:320px;max-height:480px;overflow-y:auto}.message-list[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:12px}.message[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:10px}.message[_ngcontent-%COMP%] .avatar-icon[_ngcontent-%COMP%]{font-size:22px;width:22px;height:22px;flex-shrink:0;margin-top:4px}.message[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%]{padding:10px 14px;border-radius:12px;font-size:14px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-width:100%}.message.user-message[_ngcontent-%COMP%]{flex-direction:row-reverse}.message.user-message[_ngcontent-%COMP%] .avatar-icon[_ngcontent-%COMP%]{color:#3f51b5}.message.user-message[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%]{background:#e8eaf6;color:#000000de}.message.assistant-message[_ngcontent-%COMP%] .avatar-icon[_ngcontent-%COMP%]{color:#4caf50}.message.assistant-message[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%]{background:#f5f5f5;color:#000000de}.empty-state[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:240px;color:#00000061}.empty-state[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:12px}.empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:14px}.loading-row[_ngcontent-%COMP%]{display:flex;align-items:center;gap:12px;padding:16px 0;color:#0000008a;font-size:14px}.error-row[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:8px;padding:12px;border-radius:6px;background:#fff3e0;color:#e65100;font-size:14px;margin-top:8px}.error-row[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:20px;width:20px;height:20px;flex-shrink:0}.input-area[_ngcontent-%COMP%]{padding:12px 16px;display:flex;gap:12px;align-items:flex-end}.input-area[_ngcontent-%COMP%] .message-input[_ngcontent-%COMP%]{flex:1;margin-bottom:0}@media (max-width: 600px){.chat-container[_ngcontent-%COMP%]{padding:8px}.input-area[_ngcontent-%COMP%]{flex-direction:column;align-items:stretch}.input-area[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{width:100%}}"]});let t=n;return t})();function Dxe(t,n){t&1&&(m(0,"div",2)(1,"mat-card",3)(2,"mat-card-content")(3,"div",4)(4,"mat-icon"),f(5,"info"),h(),m(6,"div")(7,"strong"),f(8,"AI features are disabled."),h(),m(9,"p"),f(10," To enable AI, set "),m(11,"code"),f(12,"aiEnabled: true"),h(),f(13," in "),m(14,"code"),f(15,"src/environments/environment.ts"),h(),f(16," and "),m(17,"code"),f(18,'"AiEnabled": true'),h(),f(19," in the API's "),m(20,"code"),f(21,"appsettings.json"),h(),f(22,". "),h()()()()()())}function Mxe(t,n){if(t&1){let e=q();m(0,"div",18)(1,"p",19),f(2,"Try asking:"),h(),m(3,"div",20)(4,"button",21),S("click",function(){k(e);let r=x(2);return T(r.input="Which department has the most employees?")}),f(5,"Which department has the most employees?"),h(),m(6,"button",21),S("click",function(){k(e);let r=x(2);return T(r.input="How many new hires joined this month?")}),f(7,"How many new hires joined this month?"),h(),m(8,"button",21),S("click",function(){k(e);let r=x(2);return T(r.input="What is the gender distribution?")}),f(9,"What is the gender distribution?"),h(),m(10,"button",21),S("click",function(){k(e);let r=x(2);return T(r.input="Who are the most recent hires?")}),f(11,"Who are the most recent hires?"),h()()()}}function Exe(t,n){if(t&1&&(m(0,"span",29),f(1),h()),t&2){let e=x().$implicit;g(),fe("",e.executionTimeMs,"ms")}}function Sxe(t,n){if(t&1&&(m(0,"div",24)(1,"mat-icon",25),f(2),h(),m(3,"div",26)(4,"div",27),f(5),h(),A(6,Exe,2,1,"span",28),h()()),t&2){let e=n.$implicit;G("user-message",e.role==="user")("assistant-message",e.role==="assistant"),g(2),N(e.role==="user"?"person":"analytics"),g(3),N(e.content),g(),v("ngIf",e.executionTimeMs)}}function kxe(t,n){if(t&1&&(m(0,"div",22),A(1,Sxe,7,7,"div",23),h()),t&2){let e=x(2);g(),v("ngForOf",e.messages)}}function Txe(t,n){t&1&&(m(0,"div",30),M(1,"mat-spinner",31),m(2,"span"),f(3,"Fetching live data and reasoning\u2026"),h()())}function Ixe(t,n){if(t&1&&(m(0,"div",32)(1,"mat-icon"),f(2,"error_outline"),h(),m(3,"span"),f(4),h()()),t&2){let e=x(2);g(4),N(e.error)}}function Axe(t,n){if(t&1){let e=q();m(0,"div",5)(1,"mat-card",6)(2,"mat-card-header")(3,"mat-icon",7),f(4,"analytics"),h(),m(5,"mat-card-title"),f(6,"HR AI Assistant"),h(),m(7,"mat-card-subtitle"),f(8,"Ask about your live workforce data \u2014 headcount, departments, recent hires"),h(),m(9,"div",8)(10,"button",9),S("click",function(){k(e);let r=x();return T(r.clear())}),m(11,"mat-icon"),f(12,"delete_sweep"),h()()()(),m(13,"mat-card-content"),A(14,Mxe,12,0,"div",10)(15,kxe,2,1,"div",11)(16,Txe,4,0,"div",12)(17,Ixe,5,1,"div",13),h(),M(18,"mat-divider"),m(19,"mat-card-actions",14)(20,"mat-form-field",15)(21,"mat-label"),f(22,"Question"),h(),m(23,"input",16),fn("ngModelChange",function(r){k(e);let o=x();return Mn(o.input,r)||(o.input=r),T(r)}),S("keydown",function(r){k(e);let o=x();return T(o.onKeydown(r))}),h()(),m(24,"button",17),S("click",function(){k(e);let r=x();return T(r.send())}),m(25,"mat-icon"),f(26,"send"),h(),f(27," Ask "),h()()()()}if(t&2){let e=x();g(14),v("ngIf",e.messages.length===0&&!e.loading),g(),v("ngIf",e.messages.length>0),g(),v("ngIf",e.loading),g(),v("ngIf",e.error),g(6),pn("ngModel",e.input),v("disabled",e.loading),g(),v("disabled",!e.input.trim()||e.loading)}}var kU=(()=>{let n=class n{constructor(){this.aiService=u(nd),this.destroy$=new z,this.aiEnabled=Ji.aiEnabled,this.messages=[],this.input="",this.loading=!1,this.error=""}send(){let i=this.input.trim();!i||this.loading||(this.messages.push({role:"user",content:i}),this.input="",this.loading=!0,this.error="",this.aiService.hrInsight(i).pipe(xe(this.destroy$)).subscribe({next:r=>{this.messages.push({role:"assistant",content:r.answer,executionTimeMs:r.executionTimeMs}),this.loading=!1},error:r=>{this.error=r?.error?.detail??"Failed to get HR insights. Is the API running with AiEnabled: true?",this.loading=!1}}))}onKeydown(i){i.key==="Enter"&&!i.shiftKey&&(i.preventDefault(),this.send())}clear(){this.messages=[],this.error=""}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-ai-hr-insight"]],decls:3,vars:2,consts:[["class","ai-disabled-banner",4,"ngIf"],["class","chat-container",4,"ngIf"],[1,"ai-disabled-banner"],[1,"disabled-card"],[1,"disabled-content"],[1,"chat-container"],[1,"chat-card"],["mat-card-avatar",""],[1,"header-actions"],["mat-icon-button","","matTooltip","Clear conversation",3,"click"],["class","suggestions",4,"ngIf"],["class","message-list",4,"ngIf"],["class","loading-row",4,"ngIf"],["class","error-row",4,"ngIf"],[1,"input-area"],["appearance","outline","subscriptSizing","dynamic",1,"message-input"],["matInput","","placeholder","Ask about your workforce data\u2026",3,"ngModelChange","keydown","ngModel","disabled"],["mat-fab","","extended","","color","primary",3,"click","disabled"],[1,"suggestions"],[1,"suggestions-label"],[1,"suggestion-list"],["mat-stroked-button","",3,"click"],[1,"message-list"],["class","message",3,"user-message","assistant-message",4,"ngFor","ngForOf"],[1,"message"],[1,"avatar-icon"],[1,"bubble-wrapper"],[1,"bubble"],["class","exec-time",4,"ngIf"],[1,"exec-time"],[1,"loading-row"],["diameter","24"],[1,"error-row"]],template:function(r,o){r&1&&(M(0,"page-header"),A(1,Dxe,23,0,"div",0)(2,Axe,28,7,"div",1)),r&2&&(g(),v("ngIf",!o.aiEnabled),g(),v("ngIf",o.aiEnabled))},dependencies:[Je,Un,Wt,Qr,di,Pt,Ro,It,kt,Xy,Hs,Tt,Rt,js,Ot,Ge,Ze,Fe,_t,Ft,Dx,Bi,Ci,Xt,gi,ai,Zt,Kt,Nr,Kr,Lt],styles:[".ai-disabled-banner[_ngcontent-%COMP%]{padding:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%]{max-width:720px;margin:0 auto;border-left:4px solid #2196f3}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:20px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:28px;width:28px;height:28px;color:#2196f3;flex-shrink:0;margin-top:2px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font-size:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:8px 0 0;color:#0009;font-size:14px;line-height:1.5}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] code[_ngcontent-%COMP%]{background:#0000000f;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:13px}.chat-container[_ngcontent-%COMP%]{padding:16px;max-width:900px;margin:0 auto}.chat-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%]{display:flex;align-items:flex-start;padding:16px 16px 0}.chat-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] .mat-mdc-card-header-text[_ngcontent-%COMP%]{flex:1}.chat-card[_ngcontent-%COMP%] mat-card-header[_ngcontent-%COMP%] .header-actions[_ngcontent-%COMP%]{margin-left:auto}.chat-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:16px;min-height:320px;max-height:480px;overflow-y:auto}.suggestions[_ngcontent-%COMP%]{padding-bottom:16px}.suggestions[_ngcontent-%COMP%] .suggestions-label[_ngcontent-%COMP%]{font-size:13px;color:#0000008a;margin:0 0 10px}.suggestions[_ngcontent-%COMP%] .suggestion-list[_ngcontent-%COMP%]{display:flex;flex-wrap:wrap;gap:8px}.suggestions[_ngcontent-%COMP%] .suggestion-list[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{font-size:13px;height:32px}.message-list[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:12px}.message[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:10px}.message[_ngcontent-%COMP%] .avatar-icon[_ngcontent-%COMP%]{font-size:22px;width:22px;height:22px;flex-shrink:0;margin-top:4px}.message[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%]{padding:10px 14px;border-radius:12px;font-size:14px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-width:100%}.message[_ngcontent-%COMP%] .bubble-wrapper[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:4px}.message[_ngcontent-%COMP%] .bubble-wrapper[_ngcontent-%COMP%] .exec-time[_ngcontent-%COMP%]{font-size:11px;color:#00000061;padding-left:4px}.message.user-message[_ngcontent-%COMP%]{flex-direction:row-reverse}.message.user-message[_ngcontent-%COMP%] .avatar-icon[_ngcontent-%COMP%]{color:#3f51b5}.message.user-message[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%]{background:#e8eaf6;color:#000000de}.message.assistant-message[_ngcontent-%COMP%] .avatar-icon[_ngcontent-%COMP%]{color:#4caf50}.message.assistant-message[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%]{background:#f5f5f5;color:#000000de}.loading-row[_ngcontent-%COMP%]{display:flex;align-items:center;gap:12px;padding:16px 0;color:#0000008a;font-size:14px}.error-row[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:8px;padding:12px;border-radius:6px;background:#fff3e0;color:#e65100;font-size:14px;margin-top:8px}.error-row[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:20px;width:20px;height:20px;flex-shrink:0}.input-area[_ngcontent-%COMP%]{padding:12px 16px;display:flex;gap:12px;align-items:flex-end}.input-area[_ngcontent-%COMP%] .message-input[_ngcontent-%COMP%]{flex:1;margin-bottom:0}@media (max-width: 600px){.chat-container[_ngcontent-%COMP%]{padding:8px}.input-area[_ngcontent-%COMP%]{flex-direction:column;align-items:stretch}.input-area[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{width:100%}.suggestion-list[_ngcontent-%COMP%]{flex-direction:column}}"]});let t=n;return t})();function Oxe(t,n){t&1&&(m(0,"mat-card",0)(1,"mat-card-content")(2,"div",2)(3,"mat-icon"),f(4,"info"),h(),m(5,"div")(6,"strong"),f(7,"AI features are disabled."),h(),m(8,"p"),f(9," To enable AI, set "),m(10,"code"),f(11,"aiEnabled: true"),h(),f(12," in "),m(13,"code"),f(14,"src/environments/environment.ts"),h(),f(15," and "),m(16,"code"),f(17,'"AiEnabled": true'),h(),f(18," in the API's "),m(19,"code"),f(20,"appsettings.json"),h(),f(21,". "),h()()()()())}function Rxe(t,n){if(t&1&&(m(0,"div",9)(1,"mat-icon"),f(2,"info_outline"),h(),m(3,"span"),f(4,"Parsed filter: "),m(5,"em"),f(6),h()()()),t&2){let e=x(2);g(6),N(e.parsedExpression)}}function Pxe(t,n){t&1&&(m(0,"div",10),M(1,"mat-spinner",14),m(2,"span"),f(3,"Parsing query and searching\u2026"),h()())}function Fxe(t,n){if(t&1&&(m(0,"div",11)(1,"mat-icon"),f(2,"error_outline"),h(),m(3,"span"),f(4),h()()),t&2){let e=x(2);g(4),N(e.error)}}function Nxe(t,n){t&1&&(m(0,"th",26),f(1,"Employee #"),h())}function Lxe(t,n){if(t&1&&(m(0,"td",27),f(1),h()),t&2){let e=n.$implicit;g(),N(e.employeeNumber)}}function Vxe(t,n){t&1&&(m(0,"th",26),f(1,"Name"),h())}function Bxe(t,n){if(t&1&&(m(0,"td",27),f(1),h()),t&2){let e=n.$implicit;g(),_l("",e.firstName," ",e.lastName)}}function jxe(t,n){t&1&&(m(0,"th",26),f(1,"Position"),h())}function Hxe(t,n){if(t&1&&(m(0,"td",27),f(1),h()),t&2){let e=n.$implicit;g(),N(e.positionTitle||(e.position==null?null:e.position.positionTitle)||"-")}}function zxe(t,n){t&1&&(m(0,"th",26),f(1,"Department"),h())}function Uxe(t,n){if(t&1&&(m(0,"td",27),f(1),h()),t&2){let e=n.$implicit,i=x(3);g(),N(i.getDepartmentName(e))}}function $xe(t,n){t&1&&(m(0,"th",26),f(1,"Actions"),h())}function Wxe(t,n){if(t&1){let e=q();m(0,"td",27)(1,"button",28),S("click",function(){let r=k(e).$implicit,o=x(3);return T(o.viewEmployee(r.id))}),m(2,"mat-icon"),f(3,"visibility"),h()()()}}function Gxe(t,n){t&1&&M(0,"tr",29)}function qxe(t,n){t&1&&M(0,"tr",30)}function Yxe(t,n){if(t&1&&(m(0,"div",12)(1,"table",15),lt(2,16),A(3,Nxe,2,0,"th",17)(4,Lxe,2,1,"td",18),ot(),lt(5,19),A(6,Vxe,2,0,"th",17)(7,Bxe,2,2,"td",18),ot(),lt(8,20),A(9,jxe,2,0,"th",17)(10,Hxe,2,1,"td",18),ot(),lt(11,21),A(12,zxe,2,0,"th",17)(13,Uxe,2,1,"td",18),ot(),lt(14,22),A(15,$xe,2,0,"th",17)(16,Wxe,4,0,"td",18),ot(),A(17,Gxe,1,0,"tr",23)(18,qxe,1,0,"tr",24),h(),m(19,"p",25),f(20),h()()),t&2){let e=x(2);g(),v("dataSource",e.results),g(16),v("matHeaderRowDef",e.displayedColumns),g(),v("matRowDefColumns",e.displayedColumns),g(2),fe("",e.results.length," result(s) found")}}function Qxe(t,n){t&1&&(m(0,"div",13)(1,"mat-icon"),f(2,"person_search"),h(),m(3,"p"),f(4,"No employees matched your query"),h()())}function Kxe(t,n){t&1&&(m(0,"div",13)(1,"mat-icon"),f(2,"manage_search"),h(),m(3,"p"),f(4,"Type a natural language query to search employees"),h()())}function Zxe(t,n){if(t&1){let e=q();m(0,"mat-card",1)(1,"mat-card-header")(2,"mat-icon",3),f(3,"manage_search"),h(),m(4,"mat-card-title"),f(5,"Natural Language Employee Search"),h(),m(6,"mat-card-subtitle"),f(7,"Describe the employee you're looking for in plain English"),h()(),m(8,"mat-card-content")(9,"div",4)(10,"mat-form-field",5)(11,"mat-label"),f(12,"Search employees"),h(),m(13,"input",6),fn("ngModelChange",function(r){k(e);let o=x();return Mn(o.query,r)||(o.query=r),T(r)}),S("ngModelChange",function(){k(e);let r=x();return T(r.onQueryChange())}),h(),m(14,"mat-icon",7),f(15,"search"),h()(),m(16,"button",8),S("click",function(){k(e);let r=x();return T(r.clear())}),m(17,"mat-icon"),f(18,"clear"),h(),f(19," Clear "),h()(),L(20,Rxe,7,1,"div",9),L(21,Pxe,4,0,"div",10),L(22,Fxe,5,1,"div",11),L(23,Yxe,21,4,"div",12),L(24,Qxe,5,0,"div",13),L(25,Kxe,5,0,"div",13),h()()}if(t&2){let e=x();g(13),pn("ngModel",e.query),v("disabled",e.loading),g(3),v("disabled",!e.query&&e.results.length===0),g(4),V(e.parsedExpression?20:-1),g(),V(e.loading?21:-1),g(),V(e.error?22:-1),g(),V(!e.loading&&e.results.length>0?23:-1),g(),V(!e.loading&&!e.error&&e.query&&e.results.length===0?24:-1),g(),V(!e.loading&&!e.error&&!e.query?25:-1)}}var TU=(()=>{let n=class n{constructor(){this.aiService=u(nd),this.employeeService=u(rd),this.departmentService=u(_a),this.router=u(Ae),this.destroy$=new z,this.searchSubject=new z,this.departmentMap=new Map,this.aiEnabled=Ji.aiEnabled,this.query="",this.loading=!1,this.error="",this.parsedExpression="",this.results=[],this.displayedColumns=["employeeNumber","fullName","positionTitle","departmentName","actions"],this.departmentService.getAll().pipe(xe(this.destroy$)).subscribe(i=>{i.forEach(r=>this.departmentMap.set(r.id,r.name))}),this.searchSubject.pipe(Dt(600),je(i=>i.trim()?(this.loading=!0,this.error="",this.aiService.nlEmployeeSearch(i).pipe(ei(r=>(this.error=r?.error?.detail??"Failed to parse query. Is the API running with AiEnabled: true?",this.loading=!1,Q(null))))):(this.results=[],this.parsedExpression="",Q(null))),xe(this.destroy$)).subscribe(i=>{if(!i){this.loading=!1;return}this.parsedExpression=i.parsedExpression,this.applyFilter(i)})}onQueryChange(){this.searchSubject.next(this.query)}applyFilter(i){let r={};i.firstName&&(r.FirstName=i.firstName),i.lastName&&(r.LastName=i.lastName),i.email&&(r.Email=i.email),i.employeeNumber&&(r.EmployeeNumber=i.employeeNumber),i.positionTitle&&(r.PositionTitle=i.positionTitle),this.employeeService.getAllPaged(I({pageNumber:1,pageSize:50},r)).subscribe({next:o=>{this.results=o.value,this.loading=!1},error:o=>{console.error("Error loading employees:",o),this.error="Failed to load employee results.",this.loading=!1}})}clear(){this.query="",this.results=[],this.parsedExpression="",this.error=""}getDepartmentName(i){return i.departmentName||i.department?.name||this.departmentMap.get(i.departmentId)||"-"}viewEmployee(i){this.router.navigate(["/employees",i])}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-ai-nl-search"]],decls:3,vars:2,consts:[[1,"disabled-card"],[1,"search-card"],[1,"disabled-content"],["mat-card-avatar",""],[1,"search-row"],["appearance","outline","subscriptSizing","dynamic",1,"search-input"],["matInput","","placeholder","e.g. software engineers in IT department hired last year",3,"ngModelChange","ngModel","disabled"],["matSuffix",""],["mat-stroked-button","",3,"click","disabled"],[1,"parsed-expression"],[1,"loading-row"],[1,"error-row"],[1,"table-container"],[1,"empty-state"],["diameter","24"],["mat-table","",1,"results-table",3,"dataSource"],["matColumnDef","employeeNumber"],["mat-header-cell","",4,"matHeaderCellDef"],["mat-cell","",4,"matCellDef"],["matColumnDef","fullName"],["matColumnDef","positionTitle"],["matColumnDef","departmentName"],["matColumnDef","actions"],["mat-header-row","",4,"matHeaderRowDef"],["mat-row","",4,"matRowDef","matRowDefColumns"],[1,"result-count"],["mat-header-cell",""],["mat-cell",""],["mat-icon-button","","color","primary","matTooltip","View Employee",3,"click"],["mat-header-row",""],["mat-row",""]],template:function(r,o){r&1&&(M(0,"page-header"),L(1,Oxe,22,0,"mat-card",0),L(2,Zxe,26,9,"mat-card",1)),r&2&&(g(),V(o.aiEnabled?-1:1),g(),V(o.aiEnabled?2:-1))},dependencies:[Je,Qr,di,Pt,Ro,It,kt,Hs,Tt,Rt,js,Ot,Ge,Ze,Fe,_t,Ft,Bi,Ci,Xt,gi,Ka,ai,Zt,Kt,ka,ba,ya,Da,xa,va,Ma,Ca,wa,Ea,Sa,An,ur,Lt],styles:[".ai-disabled-banner[_ngcontent-%COMP%]{padding:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%]{max-width:720px;margin:0 auto;border-left:4px solid #2196f3}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:20px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:28px;width:28px;height:28px;color:#2196f3;flex-shrink:0;margin-top:2px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font-size:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:8px 0 0;color:#0009;font-size:14px;line-height:1.5}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] code[_ngcontent-%COMP%]{background:#0000000f;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:13px}.search-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:16px}.search-row[_ngcontent-%COMP%]{display:flex;gap:12px;align-items:flex-start;margin-bottom:8px}.search-row[_ngcontent-%COMP%] .search-input[_ngcontent-%COMP%]{flex:1}.search-row[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{height:56px;flex-shrink:0}.parsed-expression[_ngcontent-%COMP%]{display:flex;align-items:center;gap:6px;font-size:13px;color:#0000008a;margin-bottom:12px;padding:8px 12px;background:#0000000a;border-radius:4px}.parsed-expression[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:16px;width:16px;height:16px}.table-container[_ngcontent-%COMP%]{margin-top:16px;overflow-x:auto}.results-table[_ngcontent-%COMP%]{width:100%}.result-count[_ngcontent-%COMP%]{font-size:13px;color:#0000008a;margin:8px 0 0;text-align:right}.loading-row[_ngcontent-%COMP%]{display:flex;align-items:center;gap:12px;padding:16px 0;color:#0000008a;font-size:14px}.error-row[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:8px;padding:12px;border-radius:6px;background:#fff3e0;color:#e65100;font-size:14px;margin-top:8px}.error-row[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:20px;width:20px;height:20px;flex-shrink:0}.empty-state[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:200px;color:#00000061}.empty-state[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:12px}.empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:14px}"]});let t=n;return t})();function Xxe(t,n){t&1&&(m(0,"div",2)(1,"mat-card",3)(2,"mat-card-content")(3,"div",4)(4,"mat-icon"),f(5,"info"),h(),m(6,"div")(7,"strong"),f(8,"AI features are disabled."),h(),m(9,"p"),f(10," To enable AI, set "),m(11,"code"),f(12,"aiEnabled: true"),h(),f(13," in "),m(14,"code"),f(15,"src/environments/environment.ts"),h(),f(16," and "),m(17,"code"),f(18,'"VectorSearchEnabled": true'),h(),f(19," in the API's "),m(20,"code"),f(21,"appsettings.json"),h(),f(22,". "),h()()()()()())}function Jxe(t,n){t&1&&(m(0,"div",16),M(1,"mat-spinner",17),m(2,"span"),f(3,"Searching with vector similarity\u2026"),h()())}function eCe(t,n){if(t&1&&(m(0,"div",18)(1,"mat-icon"),f(2,"error_outline"),h(),m(3,"span"),f(4),h()()),t&2){let e=x(2);g(4),N(e.error)}}function tCe(t,n){t&1&&(m(0,"th",32),f(1,"Match"),h())}function iCe(t,n){if(t&1&&(m(0,"td",33)(1,"span",34),f(2),h()()),t&2){let e=n.$implicit,i=x(3);g(2),N(i.scorePercent(e.score))}}function nCe(t,n){t&1&&(m(0,"th",32),f(1,"Position #"),h())}function rCe(t,n){if(t&1&&(m(0,"td",33),f(1),h()),t&2){let e=n.$implicit;g(),N(e.positionNumber)}}function oCe(t,n){t&1&&(m(0,"th",32),f(1,"Title"),h())}function aCe(t,n){if(t&1&&(m(0,"td",33),f(1),h()),t&2){let e=n.$implicit;g(),N(e.positionTitle)}}function sCe(t,n){t&1&&(m(0,"th",32),f(1,"Department"),h())}function lCe(t,n){if(t&1&&(m(0,"td",33),f(1),h()),t&2){let e=n.$implicit;g(),N(e.departmentName)}}function cCe(t,n){t&1&&(m(0,"th",32),f(1,"Salary Range"),h())}function dCe(t,n){if(t&1&&(m(0,"td",33),f(1),h()),t&2){let e=n.$implicit;g(),N(e.salaryRangeName)}}function uCe(t,n){t&1&&(m(0,"th",32),f(1,"Actions"),h())}function mCe(t,n){if(t&1){let e=q();m(0,"td",33)(1,"button",35),S("click",function(){let r=k(e).$implicit,o=x(3);return T(o.viewPosition(r.id))}),m(2,"mat-icon"),f(3,"visibility"),h()()()}}function hCe(t,n){t&1&&M(0,"tr",36)}function pCe(t,n){t&1&&M(0,"tr",37)}function fCe(t,n){if(t&1&&(m(0,"div",19)(1,"table",20),lt(2,21),A(3,tCe,2,0,"th",22)(4,iCe,3,1,"td",23),ot(),lt(5,24),A(6,nCe,2,0,"th",22)(7,rCe,2,1,"td",23),ot(),lt(8,25),A(9,oCe,2,0,"th",22)(10,aCe,2,1,"td",23),ot(),lt(11,26),A(12,sCe,2,0,"th",22)(13,lCe,2,1,"td",23),ot(),lt(14,27),A(15,cCe,2,0,"th",22)(16,dCe,2,1,"td",23),ot(),lt(17,28),A(18,uCe,2,0,"th",22)(19,mCe,4,0,"td",23),ot(),A(20,hCe,1,0,"tr",29)(21,pCe,1,0,"tr",30),h(),m(22,"p",31),f(23),h()()),t&2){let e=x(2);g(),v("dataSource",e.results),g(19),v("matHeaderRowDef",e.displayedColumns),g(),v("matRowDefColumns",e.displayedColumns),g(2),fe("",e.results.length," result(s) found")}}function gCe(t,n){t&1&&(m(0,"div",38)(1,"mat-icon"),f(2,"work_off"),h(),m(3,"p"),f(4,"No positions matched your query"),h()())}function _Ce(t,n){t&1&&(m(0,"div",38)(1,"mat-icon"),f(2,"travel_explore"),h(),m(3,"p"),f(4,"Describe a position to find semantic matches"),h()())}function bCe(t,n){if(t&1){let e=q();m(0,"div")(1,"mat-card",5)(2,"mat-card-header")(3,"mat-icon",6),f(4,"travel_explore"),h(),m(5,"mat-card-title"),f(6,"Vector Search \u2014 Positions"),h(),m(7,"mat-card-subtitle"),f(8,"Find positions using semantic similarity \u2014 describe what you're looking for"),h()(),m(9,"mat-card-content")(10,"div",7)(11,"mat-form-field",8)(12,"mat-label"),f(13,"Describe the position"),h(),m(14,"input",9),fn("ngModelChange",function(r){k(e);let o=x();return Mn(o.query,r)||(o.query=r),T(r)}),S("ngModelChange",function(){k(e);let r=x();return T(r.onQueryChange())}),h(),m(15,"mat-icon",10),f(16,"travel_explore"),h()(),m(17,"button",11),S("click",function(){k(e);let r=x();return T(r.clear())}),m(18,"mat-icon"),f(19,"clear"),h(),f(20," Clear "),h()(),A(21,Jxe,4,0,"div",12)(22,eCe,5,1,"div",13)(23,fCe,24,4,"div",14)(24,gCe,5,0,"div",15)(25,_Ce,5,0,"div",15),h()()()}if(t&2){let e=x();g(14),pn("ngModel",e.query),v("disabled",e.loading),g(3),v("disabled",!e.query&&e.results.length===0),g(4),v("ngIf",e.loading),g(),v("ngIf",e.error),g(),v("ngIf",!e.loading&&e.results.length>0),g(),v("ngIf",!e.loading&&!e.error&&e.query&&e.results.length===0),g(),v("ngIf",!e.loading&&!e.error&&!e.query)}}var IU=(()=>{let n=class n{constructor(){this.aiService=u(nd),this.router=u(Ae),this.destroy$=new z,this.searchSubject=new z,this.aiEnabled=Ji.aiEnabled,this.query="",this.loading=!1,this.error="",this.results=[],this.displayedColumns=["score","positionNumber","positionTitle","departmentName","salaryRangeName","actions"],this.searchSubject.pipe(Dt(600),je(i=>i.trim()?(this.loading=!0,this.error="",this.aiService.semanticPositionSearch(i).pipe(ei(r=>(this.error=r?.error?.detail??"Failed to search. Is the API running with VectorSearchEnabled: true?",this.loading=!1,Q(null))))):(this.results=[],Q(null))),xe(this.destroy$)).subscribe(i=>{if(i===null){this.loading=!1;return}this.results=i,this.loading=!1})}onQueryChange(){this.searchSubject.next(this.query)}clear(){this.query="",this.results=[],this.error=""}viewPosition(i){this.router.navigate(["/positions",i])}scorePercent(i){return`${Math.round(i*100)}%`}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-ai-vector-search"]],decls:3,vars:2,consts:[["class","ai-disabled-banner",4,"ngIf"],[4,"ngIf"],[1,"ai-disabled-banner"],[1,"disabled-card"],[1,"disabled-content"],[1,"search-card"],["mat-card-avatar",""],[1,"search-row"],["appearance","outline",1,"search-input"],["matInput","","placeholder","e.g. senior software engineer with cloud experience",3,"ngModelChange","ngModel","disabled"],["matSuffix",""],["mat-stroked-button","",3,"click","disabled"],["class","loading-row",4,"ngIf"],["class","error-row",4,"ngIf"],["class","table-container",4,"ngIf"],["class","empty-state",4,"ngIf"],[1,"loading-row"],["diameter","24"],[1,"error-row"],[1,"table-container"],["mat-table","",1,"results-table",3,"dataSource"],["matColumnDef","score"],["mat-header-cell","",4,"matHeaderCellDef"],["mat-cell","",4,"matCellDef"],["matColumnDef","positionNumber"],["matColumnDef","positionTitle"],["matColumnDef","departmentName"],["matColumnDef","salaryRangeName"],["matColumnDef","actions"],["mat-header-row","",4,"matHeaderRowDef"],["mat-row","",4,"matRowDef","matRowDefColumns"],[1,"result-count"],["mat-header-cell",""],["mat-cell",""],[1,"score-badge"],["mat-icon-button","","color","primary","matTooltip","View Position",3,"click"],["mat-header-row",""],["mat-row",""],[1,"empty-state"]],template:function(r,o){r&1&&(M(0,"page-header"),A(1,Xxe,23,0,"div",0)(2,bCe,26,8,"div",1)),r&2&&(g(),v("ngIf",!o.aiEnabled),g(),v("ngIf",o.aiEnabled))},dependencies:[Je,Wt,Qr,di,Pt,Ro,It,kt,Hs,Tt,Rt,js,Ot,Ge,Ze,Fe,_t,Ft,Bi,Ci,Xt,gi,Ka,ai,Zt,Kt,ka,ba,ya,Da,xa,va,Ma,Ca,wa,Ea,Sa,An,ur,Ow,Lt],styles:[".ai-disabled-banner[_ngcontent-%COMP%]{padding:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%]{max-width:720px;margin:0 auto;border-left:4px solid #2196f3}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:20px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:28px;width:28px;height:28px;color:#2196f3;flex-shrink:0;margin-top:2px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font-size:16px}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:8px 0 0;color:#0009;font-size:14px;line-height:1.5}.ai-disabled-banner[_ngcontent-%COMP%] .disabled-card[_ngcontent-%COMP%] .disabled-content[_ngcontent-%COMP%] code[_ngcontent-%COMP%]{background:#0000000f;padding:2px 6px;border-radius:4px;font-family:monospace;font-size:13px}.search-card[_ngcontent-%COMP%] mat-card-content[_ngcontent-%COMP%]{padding:16px}.search-row[_ngcontent-%COMP%]{display:flex;gap:12px;align-items:flex-start;margin-bottom:8px}.search-row[_ngcontent-%COMP%] .search-input[_ngcontent-%COMP%]{flex:1}.search-row[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{height:56px;flex-shrink:0}.table-container[_ngcontent-%COMP%]{margin-top:16px;overflow-x:auto}.results-table[_ngcontent-%COMP%]{width:100%}.score-badge[_ngcontent-%COMP%]{display:inline-block;padding:2px 8px;border-radius:12px;background:#e8f5e9;color:#2e7d32;font-size:13px;font-weight:500}.result-count[_ngcontent-%COMP%]{font-size:13px;color:#0000008a;margin:8px 0 0;text-align:right}.loading-row[_ngcontent-%COMP%]{display:flex;align-items:center;gap:12px;padding:16px 0;color:#0000008a;font-size:14px}.error-row[_ngcontent-%COMP%]{display:flex;align-items:flex-start;gap:8px;padding:12px;border-radius:6px;background:#fff3e0;color:#e65100;font-size:14px;margin-top:8px}.error-row[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:20px;width:20px;height:20px;flex-shrink:0}.empty-state[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:200px;color:#00000061}.empty-state[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;margin-bottom:12px}.empty-state[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;font-size:14px}"]});let t=n;return t})();var AU=[{path:"",component:l6,canActivate:[pI],canActivateChild:[pI],children:[{path:"",redirectTo:"dashboard",pathMatch:"full"},{path:"dashboard",component:g6},{path:"employees",component:A6},{path:"employees/create",component:mA,canActivate:[u_]},{path:"employees/edit/:id",component:mA,canActivate:[u_]},{path:"employees/:id",component:O6},{path:"departments",component:K6},{path:"departments/create",component:hA,canActivate:[u_]},{path:"departments/edit/:id",component:hA,canActivate:[u_]},{path:"departments/:id",component:Z6},{path:"positions",component:iU},{path:"positions/create",component:pA,canActivate:[m_]},{path:"positions/edit/:id",component:pA,canActivate:[m_]},{path:"positions/:id",component:rU},{path:"salary-ranges",component:aU},{path:"salary-ranges/create",component:fA,canActivate:[m_]},{path:"salary-ranges/edit/:id",component:fA,canActivate:[m_]},{path:"salary-ranges/:id",component:lU},{path:"profile",children:[{path:"overview",component:MU},{path:"settings",component:EU},{path:"",redirectTo:"overview",pathMatch:"full"}]},{path:"ai",children:[{path:"assistant",component:SU},{path:"hr-insight",component:kU},{path:"nl-search",component:TU},{path:"vector-search",component:IU},{path:"",redirectTo:"assistant",pathMatch:"full"}]},{path:"403",component:_6},{path:"404",component:b6},{path:"500",component:v6}]},{path:"auth",component:c6,children:[{path:"register",component:y6}]},{path:"callback",component:x6},{path:"**",redirectTo:"dashboard"}];var OU=(t,n)=>{let e=u(jt);if(!e.isAuthenticated())return n(t);let i=e.getAccessToken();if(!i)return n(t);let r=t.clone({setHeaders:{Authorization:`Bearer ${i}`}});return n(r)};var RU={providers:[r1(),{provide:TI,useValue:Ji.baseUrl},CL(),hc(()=>u(jt).initAuth()),hc(()=>u(i8).load()),hc(()=>u(e8).load()),FE(NE([OU,...c8])),mS(AU,hS({scrollPositionRestoration:"enabled",anchorScrolling:"enabled"}),Rv()),lj(),J5(X5()),J3({loader:ej({prefix:"i18n/",suffix:".json"})}),Eb(Bc.forRoot()),w3([...H3()]),{provide:Mg,useFactory:jz,deps:[ca],multi:!0},{provide:Pc,deps:[Cz],useFactory:t=>t.getPaginatorIntl()},{provide:GS,useValue:{appearance:"outlined"}},GV({parse:{dateInput:"yyyy-MM-dd"},display:{dateInput:"yyyy-MM-dd",monthYearLabel:"yyyy MMM",dateA11yLabel:"LL",monthYearA11yLabel:"yyyy MMM"}}),_3({parse:{dateInput:"yyyy-MM-dd",yearInput:"yyyy",monthInput:"MMMM",datetimeInput:"yyyy-MM-dd HH:mm",timeInput:"HH:mm"},display:{dateInput:"yyyy-MM-dd",yearInput:"yyyy",monthInput:"MMMM",datetimeInput:"yyyy-MM-dd HH:mm",timeInput:"HH:mm",monthYearLabel:"yyyy MMMM",dateA11yLabel:"LL",monthYearA11yLabel:"MMMM yyyy",popupHeaderDateLabel:"MMM dd, E"}})]};var PU=(()=>{let n=class n{constructor(){this.preloader=u(t8),this.settings=u(ha)}ngOnInit(){this.settings.setDirection(),this.settings.setTheme()}ngAfterViewInit(){this.preloader.hide()}};n.\u0275fac=function(r){return new(r||n)},n.\u0275cmp=E({type:n,selectors:[["app-root"]],decls:1,vars:0,template:function(r,o){r&1&&M(0,"router-outlet")},dependencies:[wl],encapsulation:2});let t=n;return t})();TE(PU,RU).catch(t=>console.error(t)); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 2149b97..0000000 --- a/package-lock.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "angularnettutotial", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "angularnettutotial", - "version": "1.0.0", - "license": "ISC" - }, - "Tests/AngularNetTutorial-Playwright": { - "name": "angularnettutorial-playwright", - "version": "1.0.0", - "extraneous": true, - "license": "ISC", - "devDependencies": { - "@playwright/test": "1.59.1", - "@types/node": "^25.2.2", - "cross-env": "^7.0.3", - "dotenv": "^16.4.5", - "ts-node": "^10.9.2" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 0875d84..0000000 --- a/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "angularnettutotial", - "version": "1.0.0", - "description": "This repository demonstrates the **Client, API Resource, and Token Service (CAT)** pattern using Angular and .NET technologies.", - "main": "index.js", - "directories": { - "doc": "docs" - }, - "scripts": {}, - "repository": { - "type": "git", - "url": "git+https://github.com/workcontrolgit/AngularNetTutorial.git" - }, - "keywords": [], - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/workcontrolgit/AngularNetTutorial/issues" - }, - "homepage": "https://github.com/workcontrolgit/AngularNetTutorial#readme" -} diff --git a/polyfills-5CFQRCPP.js b/polyfills-5CFQRCPP.js new file mode 100644 index 0000000..b237b5e --- /dev/null +++ b/polyfills-5CFQRCPP.js @@ -0,0 +1,2 @@ +var ce=globalThis;function te(t){return(ce.__Zone_symbol_prefix||"__zone_symbol__")+t}function ht(){let t=ce.performance;function n(I){t&&t.mark&&t.mark(I)}function a(I,s){t&&t.measure&&t.measure(I,s)}n("Zone");class e{static __symbol__=te;static assertZonePatched(){if(ce.Promise!==S.ZoneAwarePromise)throw new Error("Zone.js has detected that ZoneAwarePromise `(window|global).Promise` has been overwritten.\nMost likely cause is that a Promise polyfill has been loaded after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. If you must load one, do so before loading zone.js.)")}static get root(){let s=e.current;for(;s.parent;)s=s.parent;return s}static get current(){return b.zone}static get currentTask(){return D}static __load_patch(s,i,r=!1){if(S.hasOwnProperty(s)){let E=ce[te("forceDuplicateZoneCheck")]===!0;if(!r&&E)throw Error("Already loaded patch: "+s)}else if(!ce["__Zone_disable_"+s]){let E="Zone:"+s;n(E),S[s]=i(ce,e,R),a(E,E)}}get parent(){return this._parent}get name(){return this._name}_parent;_name;_properties;_zoneDelegate;constructor(s,i){this._parent=s,this._name=i?i.name||"unnamed":"",this._properties=i&&i.properties||{},this._zoneDelegate=new f(this,this._parent&&this._parent._zoneDelegate,i)}get(s){let i=this.getZoneWith(s);if(i)return i._properties[s]}getZoneWith(s){let i=this;for(;i;){if(i._properties.hasOwnProperty(s))return i;i=i._parent}return null}fork(s){if(!s)throw new Error("ZoneSpec required!");return this._zoneDelegate.fork(this,s)}wrap(s,i){if(typeof s!="function")throw new Error("Expecting function got: "+s);let r=this._zoneDelegate.intercept(this,s,i),E=this;return function(){return E.runGuarded(r,this,arguments,i)}}run(s,i,r,E){b={parent:b,zone:this};try{return this._zoneDelegate.invoke(this,s,i,r,E)}finally{b=b.parent}}runGuarded(s,i=null,r,E){b={parent:b,zone:this};try{try{return this._zoneDelegate.invoke(this,s,i,r,E)}catch(x){if(this._zoneDelegate.handleError(this,x))throw x}}finally{b=b.parent}}runTask(s,i,r){if(s.zone!=this)throw new Error("A task can only be run in the zone of creation! (Creation: "+(s.zone||J).name+"; Execution: "+this.name+")");let E=s,{type:x,data:{isPeriodic:ee=!1,isRefreshable:M=!1}={}}=s;if(s.state===q&&(x===U||x===k))return;let he=s.state!=A;he&&E._transitionTo(A,d);let _e=D;D=E,b={parent:b,zone:this};try{x==k&&s.data&&!ee&&!M&&(s.cancelFn=void 0);try{return this._zoneDelegate.invokeTask(this,E,i,r)}catch(Q){if(this._zoneDelegate.handleError(this,Q))throw Q}}finally{let Q=s.state;if(Q!==q&&Q!==X)if(x==U||ee||M&&Q===p)he&&E._transitionTo(d,A,p);else{let Te=E._zoneDelegates;this._updateTaskCount(E,-1),he&&E._transitionTo(q,A,q),M&&(E._zoneDelegates=Te)}b=b.parent,D=_e}}scheduleTask(s){if(s.zone&&s.zone!==this){let r=this;for(;r;){if(r===s.zone)throw Error(`can not reschedule task to ${this.name} which is descendants of the original zone ${s.zone.name}`);r=r.parent}}s._transitionTo(p,q);let i=[];s._zoneDelegates=i,s._zone=this;try{s=this._zoneDelegate.scheduleTask(this,s)}catch(r){throw s._transitionTo(X,p,q),this._zoneDelegate.handleError(this,r),r}return s._zoneDelegates===i&&this._updateTaskCount(s,1),s.state==p&&s._transitionTo(d,p),s}scheduleMicroTask(s,i,r,E){return this.scheduleTask(new g(F,s,i,r,E,void 0))}scheduleMacroTask(s,i,r,E,x){return this.scheduleTask(new g(k,s,i,r,E,x))}scheduleEventTask(s,i,r,E,x){return this.scheduleTask(new g(U,s,i,r,E,x))}cancelTask(s){if(s.zone!=this)throw new Error("A task can only be cancelled in the zone of creation! (Creation: "+(s.zone||J).name+"; Execution: "+this.name+")");if(!(s.state!==d&&s.state!==A)){s._transitionTo(V,d,A);try{this._zoneDelegate.cancelTask(this,s)}catch(i){throw s._transitionTo(X,V),this._zoneDelegate.handleError(this,i),i}return this._updateTaskCount(s,-1),s._transitionTo(q,V),s.runCount=-1,s}}_updateTaskCount(s,i){let r=s._zoneDelegates;i==-1&&(s._zoneDelegates=null);for(let E=0;EI.hasTask(i,r),onScheduleTask:(I,s,i,r)=>I.scheduleTask(i,r),onInvokeTask:(I,s,i,r,E,x)=>I.invokeTask(i,r,E,x),onCancelTask:(I,s,i,r)=>I.cancelTask(i,r)};class f{get zone(){return this._zone}_zone;_taskCounts={microTask:0,macroTask:0,eventTask:0};_parentDelegate;_forkDlgt;_forkZS;_forkCurrZone;_interceptDlgt;_interceptZS;_interceptCurrZone;_invokeDlgt;_invokeZS;_invokeCurrZone;_handleErrorDlgt;_handleErrorZS;_handleErrorCurrZone;_scheduleTaskDlgt;_scheduleTaskZS;_scheduleTaskCurrZone;_invokeTaskDlgt;_invokeTaskZS;_invokeTaskCurrZone;_cancelTaskDlgt;_cancelTaskZS;_cancelTaskCurrZone;_hasTaskDlgt;_hasTaskDlgtOwner;_hasTaskZS;_hasTaskCurrZone;constructor(s,i,r){this._zone=s,this._parentDelegate=i,this._forkZS=r&&(r&&r.onFork?r:i._forkZS),this._forkDlgt=r&&(r.onFork?i:i._forkDlgt),this._forkCurrZone=r&&(r.onFork?this._zone:i._forkCurrZone),this._interceptZS=r&&(r.onIntercept?r:i._interceptZS),this._interceptDlgt=r&&(r.onIntercept?i:i._interceptDlgt),this._interceptCurrZone=r&&(r.onIntercept?this._zone:i._interceptCurrZone),this._invokeZS=r&&(r.onInvoke?r:i._invokeZS),this._invokeDlgt=r&&(r.onInvoke?i:i._invokeDlgt),this._invokeCurrZone=r&&(r.onInvoke?this._zone:i._invokeCurrZone),this._handleErrorZS=r&&(r.onHandleError?r:i._handleErrorZS),this._handleErrorDlgt=r&&(r.onHandleError?i:i._handleErrorDlgt),this._handleErrorCurrZone=r&&(r.onHandleError?this._zone:i._handleErrorCurrZone),this._scheduleTaskZS=r&&(r.onScheduleTask?r:i._scheduleTaskZS),this._scheduleTaskDlgt=r&&(r.onScheduleTask?i:i._scheduleTaskDlgt),this._scheduleTaskCurrZone=r&&(r.onScheduleTask?this._zone:i._scheduleTaskCurrZone),this._invokeTaskZS=r&&(r.onInvokeTask?r:i._invokeTaskZS),this._invokeTaskDlgt=r&&(r.onInvokeTask?i:i._invokeTaskDlgt),this._invokeTaskCurrZone=r&&(r.onInvokeTask?this._zone:i._invokeTaskCurrZone),this._cancelTaskZS=r&&(r.onCancelTask?r:i._cancelTaskZS),this._cancelTaskDlgt=r&&(r.onCancelTask?i:i._cancelTaskDlgt),this._cancelTaskCurrZone=r&&(r.onCancelTask?this._zone:i._cancelTaskCurrZone),this._hasTaskZS=null,this._hasTaskDlgt=null,this._hasTaskDlgtOwner=null,this._hasTaskCurrZone=null;let E=r&&r.onHasTask,x=i&&i._hasTaskZS;(E||x)&&(this._hasTaskZS=E?r:c,this._hasTaskDlgt=i,this._hasTaskDlgtOwner=this,this._hasTaskCurrZone=this._zone,r.onScheduleTask||(this._scheduleTaskZS=c,this._scheduleTaskDlgt=i,this._scheduleTaskCurrZone=this._zone),r.onInvokeTask||(this._invokeTaskZS=c,this._invokeTaskDlgt=i,this._invokeTaskCurrZone=this._zone),r.onCancelTask||(this._cancelTaskZS=c,this._cancelTaskDlgt=i,this._cancelTaskCurrZone=this._zone))}fork(s,i){return this._forkZS?this._forkZS.onFork(this._forkDlgt,this.zone,s,i):new e(s,i)}intercept(s,i,r){return this._interceptZS?this._interceptZS.onIntercept(this._interceptDlgt,this._interceptCurrZone,s,i,r):i}invoke(s,i,r,E,x){return this._invokeZS?this._invokeZS.onInvoke(this._invokeDlgt,this._invokeCurrZone,s,i,r,E,x):i.apply(r,E)}handleError(s,i){return this._handleErrorZS?this._handleErrorZS.onHandleError(this._handleErrorDlgt,this._handleErrorCurrZone,s,i):!0}scheduleTask(s,i){let r=i;if(this._scheduleTaskZS)this._hasTaskZS&&r._zoneDelegates.push(this._hasTaskDlgtOwner),r=this._scheduleTaskZS.onScheduleTask(this._scheduleTaskDlgt,this._scheduleTaskCurrZone,s,i),r||(r=i);else if(i.scheduleFn)i.scheduleFn(i);else if(i.type==F)z(i);else throw new Error("Task is missing scheduleFn.");return r}invokeTask(s,i,r,E){return this._invokeTaskZS?this._invokeTaskZS.onInvokeTask(this._invokeTaskDlgt,this._invokeTaskCurrZone,s,i,r,E):i.callback.apply(r,E)}cancelTask(s,i){let r;if(this._cancelTaskZS)r=this._cancelTaskZS.onCancelTask(this._cancelTaskDlgt,this._cancelTaskCurrZone,s,i);else{if(!i.cancelFn)throw Error("Task is not cancelable");r=i.cancelFn(i)}return r}hasTask(s,i){try{this._hasTaskZS&&this._hasTaskZS.onHasTask(this._hasTaskDlgt,this._hasTaskCurrZone,s,i)}catch(r){this.handleError(s,r)}}_updateTaskCount(s,i){let r=this._taskCounts,E=r[s],x=r[s]=E+i;if(x<0)throw new Error("More tasks executed then were scheduled.");if(E==0||x==0){let ee={microTask:r.microTask>0,macroTask:r.macroTask>0,eventTask:r.eventTask>0,change:s};this.hasTask(this._zone,ee)}}}class g{type;source;invoke;callback;data;scheduleFn;cancelFn;_zone=null;runCount=0;_zoneDelegates=null;_state="notScheduled";constructor(s,i,r,E,x,ee){if(this.type=s,this.source=i,this.data=E,this.scheduleFn=x,this.cancelFn=ee,!r)throw new Error("callback is not defined");this.callback=r;let M=this;s===U&&E&&E.useG?this.invoke=g.invokeTask:this.invoke=function(){return g.invokeTask.call(ce,M,this,arguments)}}static invokeTask(s,i,r){s||(s=this),K++;try{return s.runCount++,s.zone.runTask(s,i,r)}finally{K==1&&$(),K--}}get zone(){return this._zone}get state(){return this._state}cancelScheduleRequest(){this._transitionTo(q,p)}_transitionTo(s,i,r){if(this._state===i||this._state===r)this._state=s,s==q&&(this._zoneDelegates=null);else throw new Error(`${this.type} '${this.source}': can not transition to '${s}', expecting state '${i}'${r?" or '"+r+"'":""}, was '${this._state}'.`)}toString(){return this.data&&typeof this.data.handleId<"u"?this.data.handleId.toString():Object.prototype.toString.call(this)}toJSON(){return{type:this.type,state:this.state,source:this.source,zone:this.zone.name,runCount:this.runCount}}}let T=te("setTimeout"),y=te("Promise"),w=te("then"),_=[],P=!1,L;function H(I){if(L||ce[y]&&(L=ce[y].resolve(0)),L){let s=L[w];s||(s=L.then),s.call(L,I)}else ce[T](I,0)}function z(I){K===0&&_.length===0&&H($),I&&_.push(I)}function $(){if(!P){for(P=!0;_.length;){let I=_;_=[];for(let s=0;sb,onUnhandledError:W,microtaskDrainDone:W,scheduleMicroTask:z,showUncaughtError:()=>!e[te("ignoreConsoleErrorUncaughtError")],patchEventTarget:()=>[],patchOnProperties:W,patchMethod:()=>W,bindArguments:()=>[],patchThen:()=>W,patchMacroTask:()=>W,patchEventPrototype:()=>W,isIEOrEdge:()=>!1,getGlobalObjects:()=>{},ObjectDefineProperty:()=>W,ObjectGetOwnPropertyDescriptor:()=>{},ObjectCreate:()=>{},ArraySlice:()=>[],patchClass:()=>W,wrapWithCurrentZone:()=>W,filterProperties:()=>[],attachOriginToPatched:()=>W,_redefineProperty:()=>W,patchCallbacks:()=>W,nativeScheduleMicroTask:H},b={parent:null,zone:new e(null,null)},D=null,K=0;function W(){}return a("Zone","Zone"),e}function dt(){let t=globalThis,n=t[te("forceDuplicateZoneCheck")]===!0;if(t.Zone&&(n||typeof t.Zone.__symbol__!="function"))throw new Error("Zone already loaded.");return t.Zone??=ht(),t.Zone}var pe=Object.getOwnPropertyDescriptor,Me=Object.defineProperty,Ae=Object.getPrototypeOf,_t=Object.create,Tt=Array.prototype.slice,je="addEventListener",He="removeEventListener",Ne=te(je),Ze=te(He),ae="true",le="false",ve=te("");function Ve(t,n){return Zone.current.wrap(t,n)}function xe(t,n,a,e,c){return Zone.current.scheduleMacroTask(t,n,a,e,c)}var j=te,we=typeof window<"u",be=we?window:void 0,Y=we&&be||globalThis,Et="removeAttribute";function Fe(t,n){for(let a=t.length-1;a>=0;a--)typeof t[a]=="function"&&(t[a]=Ve(t[a],n+"_"+a));return t}function gt(t,n){let a=t.constructor.name;for(let e=0;e{let y=function(){return T.apply(this,Fe(arguments,a+"."+c))};return fe(y,T),y})(f)}}}function et(t){return t?t.writable===!1?!1:!(typeof t.get=="function"&&typeof t.set>"u"):!0}var tt=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,De=!("nw"in Y)&&typeof Y.process<"u"&&Y.process.toString()==="[object process]",Ge=!De&&!tt&&!!(we&&be.HTMLElement),nt=typeof Y.process<"u"&&Y.process.toString()==="[object process]"&&!tt&&!!(we&&be.HTMLElement),Ce={},kt=j("enable_beforeunload"),Xe=function(t){if(t=t||Y.event,!t)return;let n=Ce[t.type];n||(n=Ce[t.type]=j("ON_PROPERTY"+t.type));let a=this||t.target||Y,e=a[n],c;if(Ge&&a===be&&t.type==="error"){let f=t;c=e&&e.call(this,f.message,f.filename,f.lineno,f.colno,f.error),c===!0&&t.preventDefault()}else c=e&&e.apply(this,arguments),t.type==="beforeunload"&&Y[kt]&&typeof c=="string"?t.returnValue=c:c!=null&&!c&&t.preventDefault();return c};function Ye(t,n,a){let e=pe(t,n);if(!e&&a&&pe(a,n)&&(e={enumerable:!0,configurable:!0}),!e||!e.configurable)return;let c=j("on"+n+"patched");if(t.hasOwnProperty(c)&&t[c])return;delete e.writable,delete e.value;let f=e.get,g=e.set,T=n.slice(2),y=Ce[T];y||(y=Ce[T]=j("ON_PROPERTY"+T)),e.set=function(w){let _=this;if(!_&&t===Y&&(_=Y),!_)return;typeof _[y]=="function"&&_.removeEventListener(T,Xe),g?.call(_,null),_[y]=w,typeof w=="function"&&_.addEventListener(T,Xe,!1)},e.get=function(){let w=this;if(!w&&t===Y&&(w=Y),!w)return null;let _=w[y];if(_)return _;if(f){let P=f.call(this);if(P)return e.set.call(this,P),typeof w[Et]=="function"&&w.removeAttribute(n),P}return null},Me(t,n,e),t[c]=!0}function rt(t,n,a){if(n)for(let e=0;efunction(g,T){let y=a(g,T);return y.cbIdx>=0&&typeof T[y.cbIdx]=="function"?xe(y.name,T[y.cbIdx],y,c):f.apply(g,T)})}function fe(t,n){t[j("OriginalDelegate")]=n}var $e=!1,Le=!1;function yt(){if($e)return Le;$e=!0;try{let t=be.navigator.userAgent;(t.indexOf("MSIE ")!==-1||t.indexOf("Trident/")!==-1||t.indexOf("Edge/")!==-1)&&(Le=!0)}catch{}return Le}function Je(t){return typeof t=="function"}function Ke(t){return typeof t=="number"}var pt={useG:!0},ne={},ot={},st=new RegExp("^"+ve+"(\\w+)(true|false)$"),it=j("propagationStopped");function ct(t,n){let a=(n?n(t):t)+le,e=(n?n(t):t)+ae,c=ve+a,f=ve+e;ne[t]={},ne[t][le]=c,ne[t][ae]=f}function vt(t,n,a,e){let c=e&&e.add||je,f=e&&e.rm||He,g=e&&e.listeners||"eventListeners",T=e&&e.rmAll||"removeAllListeners",y=j(c),w="."+c+":",_="prependListener",P="."+_+":",L=function(p,d,A){if(p.isRemoved)return;let V=p.callback;typeof V=="object"&&V.handleEvent&&(p.callback=k=>V.handleEvent(k),p.originalDelegate=V);let X;try{p.invoke(p,d,[A])}catch(k){X=k}let F=p.options;if(F&&typeof F=="object"&&F.once){let k=p.originalDelegate?p.originalDelegate:p.callback;d[f].call(d,A.type,k,F)}return X};function H(p,d,A){if(d=d||t.event,!d)return;let V=p||d.target||t,X=V[ne[d.type][A?ae:le]];if(X){let F=[];if(X.length===1){let k=L(X[0],V,d);k&&F.push(k)}else{let k=X.slice();for(let U=0;U{throw U})}}}let z=function(p){return H(this,p,!1)},$=function(p){return H(this,p,!0)};function J(p,d){if(!p)return!1;let A=!0;d&&d.useG!==void 0&&(A=d.useG);let V=d&&d.vh,X=!0;d&&d.chkDup!==void 0&&(X=d.chkDup);let F=!1;d&&d.rt!==void 0&&(F=d.rt);let k=p;for(;k&&!k.hasOwnProperty(c);)k=Ae(k);if(!k&&p[c]&&(k=p),!k||k[y])return!1;let U=d&&d.eventNameToString,S={},R=k[y]=k[c],b=k[j(f)]=k[f],D=k[j(g)]=k[g],K=k[j(T)]=k[T],W;d&&d.prepend&&(W=k[j(d.prepend)]=k[d.prepend]);function I(o,u){return u?typeof o=="boolean"?{capture:o,passive:!0}:o?typeof o=="object"&&o.passive!==!1?{...o,passive:!0}:o:{passive:!0}:o}let s=function(o){if(!S.isExisting)return R.call(S.target,S.eventName,S.capture?$:z,S.options)},i=function(o){if(!o.isRemoved){let u=ne[o.eventName],v;u&&(v=u[o.capture?ae:le]);let C=v&&o.target[v];if(C){for(let m=0;mre.zone.cancelTask(re);o.call(Ee,"abort",ie,{once:!0}),re.removeAbortListener=()=>Ee.removeEventListener("abort",ie)}if(S.target=null,me&&(me.taskData=null),Be&&(S.options.once=!0),typeof re.options!="boolean"&&(re.options=se),re.target=N,re.capture=Se,re.eventName=Z,B&&(re.originalDelegate=G),O?ge.unshift(re):ge.push(re),m)return N}};return k[c]=l(R,w,ee,M,F),W&&(k[_]=l(W,P,E,M,F,!0)),k[f]=function(){let o=this||t,u=arguments[0];d&&d.transferEventName&&(u=d.transferEventName(u));let v=arguments[2],C=v?typeof v=="boolean"?!0:v.capture:!1,m=arguments[1];if(!m)return b.apply(this,arguments);if(V&&!V(b,m,o,arguments))return;let O=ne[u],N;O&&(N=O[C?ae:le]);let Z=N&&o[N];if(Z)for(let G=0;Gfunction(c,f){c[it]=!0,e&&e.apply(c,f)})}function Pt(t,n){n.patchMethod(t,"queueMicrotask",a=>function(e,c){Zone.current.scheduleMicroTask("queueMicrotask",c[0])})}var Re=j("zoneTask");function ke(t,n,a,e){let c=null,f=null;n+=e,a+=e;let g={};function T(w){let _=w.data;_.args[0]=function(){return w.invoke.apply(this,arguments)};let P=c.apply(t,_.args);return Ke(P)?_.handleId=P:(_.handle=P,_.isRefreshable=Je(P.refresh)),w}function y(w){let{handle:_,handleId:P}=w.data;return f.call(t,_??P)}c=ue(t,n,w=>function(_,P){if(Je(P[0])){let L={isRefreshable:!1,isPeriodic:e==="Interval",delay:e==="Timeout"||e==="Interval"?P[1]||0:void 0,args:P},H=P[0];P[0]=function(){try{return H.apply(this,arguments)}finally{let{handle:A,handleId:V,isPeriodic:X,isRefreshable:F}=L;!X&&!F&&(V?delete g[V]:A&&(A[Re]=null))}};let z=xe(n,P[0],L,T,y);if(!z)return z;let{handleId:$,handle:J,isRefreshable:q,isPeriodic:p}=z.data;if($)g[$]=z;else if(J&&(J[Re]=z,q&&!p)){let d=J.refresh;J.refresh=function(){let{zone:A,state:V}=z;return V==="notScheduled"?(z._state="scheduled",A._updateTaskCount(z,1)):V==="running"&&(z._state="scheduling"),d.call(this)}}return J??$??z}else return w.apply(t,P)}),f=ue(t,a,w=>function(_,P){let L=P[0],H;Ke(L)?(H=g[L],delete g[L]):(H=L?.[Re],H?L[Re]=null:H=L),H?.type?H.cancelFn&&H.zone.cancelTask(H):w.apply(t,P)})}function Rt(t,n){let{isBrowser:a,isMix:e}=n.getGlobalObjects();if(!a&&!e||!t.customElements||!("customElements"in t))return;let c=["connectedCallback","disconnectedCallback","adoptedCallback","attributeChangedCallback","formAssociatedCallback","formDisabledCallback","formResetCallback","formStateRestoreCallback"];n.patchCallbacks(n,t.customElements,"customElements","define",c)}function Ct(t,n){if(Zone[n.symbol("patchEventTarget")])return;let{eventNames:a,zoneSymbolEventNames:e,TRUE_STR:c,FALSE_STR:f,ZONE_SYMBOL_PREFIX:g}=n.getGlobalObjects();for(let y=0;yf.target===t);if(e.length===0)return n;let c=e[0].ignoreProperties;return n.filter(f=>c.indexOf(f)===-1)}function Qe(t,n,a,e){if(!t)return;let c=lt(t,n,a);rt(t,c,e)}function Ie(t){return Object.getOwnPropertyNames(t).filter(n=>n.startsWith("on")&&n.length>2).map(n=>n.substring(2))}function Dt(t,n){if(De&&!nt||Zone[t.symbol("patchEvents")])return;let a=n.__Zone_ignore_on_properties,e=[];if(Ge){let c=window;e=e.concat(["Document","SVGElement","Element","HTMLElement","HTMLBodyElement","HTMLMediaElement","HTMLFrameSetElement","HTMLFrameElement","HTMLIFrameElement","HTMLMarqueeElement","Worker"]);let f=[];Qe(c,Ie(c),a&&a.concat(f),Ae(c))}e=e.concat(["XMLHttpRequest","XMLHttpRequestEventTarget","IDBIndex","IDBRequest","IDBOpenDBRequest","IDBDatabase","IDBTransaction","IDBCursor","WebSocket"]);for(let c=0;c{let a=n[t.__symbol__("legacyPatch")];a&&a()}),t.__load_patch("timers",n=>{let e="clear";ke(n,"set",e,"Timeout"),ke(n,"set",e,"Interval"),ke(n,"set",e,"Immediate")}),t.__load_patch("requestAnimationFrame",n=>{ke(n,"request","cancel","AnimationFrame"),ke(n,"mozRequest","mozCancel","AnimationFrame"),ke(n,"webkitRequest","webkitCancel","AnimationFrame")}),t.__load_patch("blocking",(n,a)=>{let e=["alert","prompt","confirm"];for(let c=0;cfunction(w,_){return a.current.run(g,n,_,y)})}}),t.__load_patch("EventTarget",(n,a,e)=>{wt(n,e),Ct(n,e);let c=n.XMLHttpRequestEventTarget;c&&c.prototype&&e.patchEventTarget(n,e,[c.prototype])}),t.__load_patch("MutationObserver",(n,a,e)=>{ye("MutationObserver"),ye("WebKitMutationObserver")}),t.__load_patch("IntersectionObserver",(n,a,e)=>{ye("IntersectionObserver")}),t.__load_patch("FileReader",(n,a,e)=>{ye("FileReader")}),t.__load_patch("on_property",(n,a,e)=>{Dt(e,n)}),t.__load_patch("customElements",(n,a,e)=>{Rt(n,e)}),t.__load_patch("XHR",(n,a)=>{w(n);let e=j("xhrTask"),c=j("xhrSync"),f=j("xhrListener"),g=j("xhrScheduled"),T=j("xhrURL"),y=j("xhrErrorBeforeScheduled");function w(_){let P=_.XMLHttpRequest;if(!P)return;let L=P.prototype;function H(R){return R[e]}let z=L[Ne],$=L[Ze];if(!z){let R=_.XMLHttpRequestEventTarget;if(R){let b=R.prototype;z=b[Ne],$=b[Ze]}}let J="readystatechange",q="scheduled";function p(R){let b=R.data,D=b.target;D[g]=!1,D[y]=!1;let K=D[f];z||(z=D[Ne],$=D[Ze]),K&&$.call(D,J,K);let W=D[f]=()=>{if(D.readyState===D.DONE)if(!b.aborted&&D[g]&&R.state===q){let s=D[a.__symbol__("loadfalse")];if(D.status!==0&&s&&s.length>0){let i=R.invoke;R.invoke=function(){let r=D[a.__symbol__("loadfalse")];for(let E=0;Efunction(R,b){return R[c]=b[2]==!1,R[T]=b[1],V.apply(R,b)}),X="XMLHttpRequest.send",F=j("fetchTaskAborting"),k=j("fetchTaskScheduling"),U=ue(L,"send",()=>function(R,b){if(a.current[k]===!0||R[c])return U.apply(R,b);{let D={target:R,url:R[T],isPeriodic:!1,args:b,aborted:!1},K=xe(X,d,D,p,A);R&&R[y]===!0&&!D.aborted&&K.state===q&&K.invoke()}}),S=ue(L,"abort",()=>function(R,b){let D=H(R);if(D&&typeof D.type=="string"){if(D.cancelFn==null||D.data&&D.data.aborted)return;D.zone.cancelTask(D)}else if(a.current[F]===!0)return S.apply(R,b)})}}),t.__load_patch("geolocation",n=>{n.navigator&&n.navigator.geolocation&>(n.navigator.geolocation,["getCurrentPosition","watchPosition"])}),t.__load_patch("PromiseRejectionEvent",(n,a)=>{function e(c){return function(f){at(n,c).forEach(T=>{let y=n.PromiseRejectionEvent;if(y){let w=new y(c,{promise:f.promise,reason:f.rejection});T.invoke(w)}})}}n.PromiseRejectionEvent&&(a[j("unhandledPromiseRejectionHandler")]=e("unhandledrejection"),a[j("rejectionHandledHandler")]=e("rejectionhandled"))}),t.__load_patch("queueMicrotask",(n,a,e)=>{Pt(n,e)})}function Ot(t){t.__load_patch("ZoneAwarePromise",(n,a,e)=>{let c=Object.getOwnPropertyDescriptor,f=Object.defineProperty;function g(h){if(h&&h.toString===Object.prototype.toString){let l=h.constructor&&h.constructor.name;return(l||"")+": "+JSON.stringify(h)}return h?h.toString():Object.prototype.toString.call(h)}let T=e.symbol,y=[],w=n[T("DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION")]!==!1,_=T("Promise"),P=T("then"),L="__creationTrace__";e.onUnhandledError=h=>{if(e.showUncaughtError()){let l=h&&h.rejection;l?console.error("Unhandled Promise rejection:",l instanceof Error?l.message:l,"; Zone:",h.zone.name,"; Task:",h.task&&h.task.source,"; Value:",l,l instanceof Error?l.stack:void 0):console.error(h)}},e.microtaskDrainDone=()=>{for(;y.length;){let h=y.shift();try{h.zone.runGuarded(()=>{throw h.throwOriginal?h.rejection:h})}catch(l){z(l)}}};let H=T("unhandledPromiseRejectionHandler");function z(h){e.onUnhandledError(h);try{let l=a[H];typeof l=="function"&&l.call(this,h)}catch{}}function $(h){return h&&typeof h.then=="function"}function J(h){return h}function q(h){return M.reject(h)}let p=T("state"),d=T("value"),A=T("finally"),V=T("parentPromiseValue"),X=T("parentPromiseState"),F="Promise.then",k=null,U=!0,S=!1,R=0;function b(h,l){return o=>{try{I(h,l,o)}catch(u){I(h,!1,u)}}}let D=function(){let h=!1;return function(o){return function(){h||(h=!0,o.apply(null,arguments))}}},K="Promise resolved with itself",W=T("currentTaskTrace");function I(h,l,o){let u=D();if(h===o)throw new TypeError(K);if(h[p]===k){let v=null;try{(typeof o=="object"||typeof o=="function")&&(v=o&&o.then)}catch(C){return u(()=>{I(h,!1,C)})(),h}if(l!==S&&o instanceof M&&o.hasOwnProperty(p)&&o.hasOwnProperty(d)&&o[p]!==k)i(o),I(h,o[p],o[d]);else if(l!==S&&typeof v=="function")try{v.call(o,u(b(h,l)),u(b(h,!1)))}catch(C){u(()=>{I(h,!1,C)})()}else{h[p]=l;let C=h[d];if(h[d]=o,h[A]===A&&l===U&&(h[p]=h[X],h[d]=h[V]),l===S&&o instanceof Error){let m=a.currentTask&&a.currentTask.data&&a.currentTask.data[L];m&&f(o,W,{configurable:!0,enumerable:!1,writable:!0,value:m})}for(let m=0;m{try{let O=h[d],N=!!o&&A===o[A];N&&(o[V]=O,o[X]=C);let Z=l.run(m,void 0,N&&m!==q&&m!==J?[]:[O]);I(o,!0,Z)}catch(O){I(o,!1,O)}},o)}let E="function ZoneAwarePromise() { [native code] }",x=function(){},ee=n.AggregateError;class M{static toString(){return E}static resolve(l){return l instanceof M?l:I(new this(null),U,l)}static reject(l){return I(new this(null),S,l)}static withResolvers(){let l={};return l.promise=new M((o,u)=>{l.resolve=o,l.reject=u}),l}static any(l){if(!l||typeof l[Symbol.iterator]!="function")return Promise.reject(new ee([],"All promises were rejected"));let o=[],u=0;try{for(let m of l)u++,o.push(M.resolve(m))}catch{return Promise.reject(new ee([],"All promises were rejected"))}if(u===0)return Promise.reject(new ee([],"All promises were rejected"));let v=!1,C=[];return new M((m,O)=>{for(let N=0;N{v||(v=!0,m(Z))},Z=>{C.push(Z),u--,u===0&&(v=!0,O(new ee(C,"All promises were rejected")))})})}static race(l){let o,u,v=new this((O,N)=>{o=O,u=N});function C(O){o(O)}function m(O){u(O)}for(let O of l)$(O)||(O=this.resolve(O)),O.then(C,m);return v}static all(l){return M.allWithCallback(l)}static allSettled(l){return(this&&this.prototype instanceof M?this:M).allWithCallback(l,{thenCallback:u=>({status:"fulfilled",value:u}),errorCallback:u=>({status:"rejected",reason:u})})}static allWithCallback(l,o){let u,v,C=new this((Z,G)=>{u=Z,v=G}),m=2,O=0,N=[];for(let Z of l){$(Z)||(Z=this.resolve(Z));let G=O;try{Z.then(B=>{N[G]=o?o.thenCallback(B):B,m--,m===0&&u(N)},B=>{o?(N[G]=o.errorCallback(B),m--,m===0&&u(N)):v(B)})}catch(B){v(B)}m++,O++}return m-=2,m===0&&u(N),C}constructor(l){let o=this;if(!(o instanceof M))throw new Error("Must be an instanceof Promise.");o[p]=k,o[d]=[];try{let u=D();l&&l(u(b(o,U)),u(b(o,S)))}catch(u){I(o,!1,u)}}get[Symbol.toStringTag](){return"Promise"}get[Symbol.species](){return M}then(l,o){let u=this.constructor?.[Symbol.species];(!u||typeof u!="function")&&(u=this.constructor||M);let v=new u(x),C=a.current;return this[p]==k?this[d].push(C,v,l,o):r(this,C,v,l,o),v}catch(l){return this.then(null,l)}finally(l){let o=this.constructor?.[Symbol.species];(!o||typeof o!="function")&&(o=M);let u=new o(x);u[A]=A;let v=a.current;return this[p]==k?this[d].push(v,u,l,l):r(this,v,u,l,l),u}}M.resolve=M.resolve,M.reject=M.reject,M.race=M.race,M.all=M.all;let he=n[_]=n.Promise;n.Promise=M;let _e=T("thenPatched");function Q(h){let l=h.prototype,o=c(l,"then");if(o&&(o.writable===!1||!o.configurable))return;let u=l.then;l[P]=u,h.prototype.then=function(v,C){return new M((O,N)=>{u.call(this,O,N)}).then(v,C)},h[_e]=!0}e.patchThen=Q;function Te(h){return function(l,o){let u=h.apply(l,o);if(u instanceof M)return u;let v=u.constructor;return v[_e]||Q(v),u}}return he&&(Q(he),ue(n,"fetch",h=>Te(h))),Promise[a.__symbol__("uncaughtPromiseErrors")]=y,M})}function Nt(t){t.__load_patch("toString",n=>{let a=Function.prototype.toString,e=j("OriginalDelegate"),c=j("Promise"),f=j("Error"),g=function(){if(typeof this=="function"){let _=this[e];if(_)return typeof _=="function"?a.call(_):Object.prototype.toString.call(_);if(this===Promise){let P=n[c];if(P)return a.call(P)}if(this===Error){let P=n[f];if(P)return a.call(P)}}return a.call(this)};g[e]=a,Function.prototype.toString=g;let T=Object.prototype.toString,y="[object Promise]";Object.prototype.toString=function(){return typeof Promise=="function"&&this instanceof Promise?y:T.call(this)}})}function Zt(t,n,a,e,c){let f=Zone.__symbol__(e);if(n[f])return;let g=n[f]=n[e];n[e]=function(T,y,w){return y&&y.prototype&&c.forEach(function(_){let P=`${a}.${e}::`+_,L=y.prototype;try{if(L.hasOwnProperty(_)){let H=t.ObjectGetOwnPropertyDescriptor(L,_);H&&H.value?(H.value=t.wrapWithCurrentZone(H.value,P),t._redefineProperty(y.prototype,_,H)):L[_]&&(L[_]=t.wrapWithCurrentZone(L[_],P))}else L[_]&&(L[_]=t.wrapWithCurrentZone(L[_],P))}catch{}}),g.call(n,T,y,w)},t.attachOriginToPatched(n[e],g)}function Lt(t){t.__load_patch("util",(n,a,e)=>{let c=Ie(n);e.patchOnProperties=rt,e.patchMethod=ue,e.bindArguments=Fe,e.patchMacroTask=mt;let f=a.__symbol__("BLACK_LISTED_EVENTS"),g=a.__symbol__("UNPATCHED_EVENTS");n[g]&&(n[f]=n[g]),n[f]&&(a[f]=a[g]=n[f]),e.patchEventPrototype=bt,e.patchEventTarget=vt,e.isIEOrEdge=yt,e.ObjectDefineProperty=Me,e.ObjectGetOwnPropertyDescriptor=pe,e.ObjectCreate=_t,e.ArraySlice=Tt,e.patchClass=ye,e.wrapWithCurrentZone=Ve,e.filterProperties=lt,e.attachOriginToPatched=fe,e._redefineProperty=Object.defineProperty,e.patchCallbacks=Zt,e.getGlobalObjects=()=>({globalSources:ot,zoneSymbolEventNames:ne,eventNames:c,isBrowser:Ge,isMix:nt,isNode:De,TRUE_STR:ae,FALSE_STR:le,ZONE_SYMBOL_PREFIX:ve,ADD_EVENT_LISTENER_STR:je,REMOVE_EVENT_LISTENER_STR:He})})}function It(t){Ot(t),Nt(t),Lt(t)}var ut=dt();It(ut);St(ut); diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md deleted file mode 100644 index cb29d52..0000000 --- a/scripts/AGENTS.md +++ /dev/null @@ -1,53 +0,0 @@ -# Repository Guidelines - -## Project Structure & Module Organization - -This `scripts/` folder holds Playwright automation used to capture tutorial screenshots. Key files live in the repository root relative to this folder: - -- `capture-angular.js`, `capture-webapi.js`, `capture-identityserver.js`: component-specific screenshot scripts. -- `package.json`: Playwright dependency and npm scripts. -- `docs/images/{angular|webapi|identityserver}/`: screenshot output directories. -- `README.md`: full runbook, credentials, and troubleshooting. - -## Build, Test, and Development Commands - -Run from `scripts/`: - -- `npm install`: installs Playwright and dependencies. -- `npm run screenshots:angular`: captures Angular app screenshots. -- `npm run screenshots:webapi`: captures Swagger UI screenshot. -- `npm run screenshots:identityserver`: captures IdentityServer admin screenshots. -- `npm run screenshots:all`: runs all three scripts sequentially. - -These scripts require the three services to be running locally: - -- IdentityServer: `https://localhost:44310` -- API: `https://localhost:44378` -- Angular: `http://localhost:4200` - -## Coding Style & Naming Conventions - -- Language: Node.js (CommonJS) with Playwright. -- Indentation: 2 spaces (match existing scripts). -- Filenames: `capture-.js`. -- Screenshot names are lowercase kebab-case (for example `employee-list-page.png`). -- Prefer explicit selectors and `waitForTimeout`/`waitForSelector` for stability. - -## Testing Guidelines - -No automated tests are configured in this folder. Use the scripts above as smoke tests, and validate output in `docs/images//`. - -## Commit & Pull Request Guidelines - -Recent commit messages are short, sentence-style summaries (for example `add screenshots`, `Update readme`). Use simple, imperative phrasing and keep messages concise. - -PRs should include: - -- A short description of what changed and why. -- Links to any related issue or tutorial update. -- Before/after screenshots when you modify capture logic or UI flows. - -## Security & Configuration Notes - -- Credentials are stored in `README.md` for local usage. Do not reuse them outside this tutorial environment. -- Scripts run with `headless: false` by default to aid debugging; switch to `true` for unattended runs. diff --git a/scripts/ANGULAR-LOGIN-ISSUE.md b/scripts/ANGULAR-LOGIN-ISSUE.md deleted file mode 100644 index 2769376..0000000 --- a/scripts/ANGULAR-LOGIN-ISSUE.md +++ /dev/null @@ -1,213 +0,0 @@ -# Angular Login Page Screenshot Issue - -## Issue Discovery - -The Angular login page at `http://localhost:4200/login` was difficult to capture in screenshots because it **automatically redirects** to IdentityServer immediately when the component loads. - -## Root Cause - -**File:** `Clients/TalentManagement-Angular-Material/talent-management/src/app/routes/sessions/login/login.ts` - -**Line 47-50:** -```typescript -ngOnInit() { - // Automatically redirect to IdentityServer login when this page loads - this.login(); -} -``` - -**Line 64-67:** -```typescript -login() { - // Redirect to Duende IdentityServer for OIDC login - this.oidcAuth.login(); -} -``` - -The `login()` method calls `this.oidcAuth.login()` which triggers the OAuth 2.0 / OIDC authorization code flow, immediately redirecting the browser to: - -``` -https://localhost:44310/connect/authorize?client_id=TalentManagement&... -``` - -## What the Login Page Should Show - -The Angular login page (`/login`) displays a Material Design card with: - -**Title:** "Welcome to Talent Management" - -**Content:** -- Description: "Please sign in to manage employee data and access all features." -- **Primary Button:** "Sign In with Identity Server" (mat-raised-button, color="primary") -- **Divider:** "OR" -- **Secondary Button:** "Continue as Guest (View Only)" (mat-stroked-button) -- **Role Information:** - - Employee: View own profile - - Manager: View team members - - HRAdmin: Full CRUD operations - -## Authentication Flow - -The intended user flow is: - -1. **User visits:** `http://localhost:4200` -2. **Auth Guard redirects to:** `http://localhost:4200/login` -3. **Login component loads** and immediately calls `ngOnInit()` -4. **ngOnInit() calls:** `this.oidcAuth.login()` -5. **Browser redirects to:** `https://localhost:44310/connect/authorize?...` -6. **User authenticates** on IdentityServer -7. **IdentityServer redirects to:** `http://localhost:4200/callback?code=...` -8. **Callback component** processes the authorization code and exchanges for tokens -9. **User redirected to:** `http://localhost:4200/dashboard` - -## Screenshot Challenge - -The auto-redirect happens in **ngOnInit()**, which runs immediately after the component is initialized. This gives only a **very brief window** (typically <100ms) to capture the rendered login page before the OAuth redirect occurs. - -Traditional screenshot approaches fail because: -- `waitUntil: 'networkidle'` waits too long, redirect already happened -- `waitUntil: 'domcontentloaded'` still may be too slow -- Angular's component initialization and rendering takes time - -## Solutions Implemented - -### Solution 1: Quick Capture (take-all-screenshots.js) -```javascript -await page.goto('http://localhost:4200/login', { - waitUntil: 'domcontentloaded', - timeout: 10000 -}); - -await page.waitForSelector('mat-card-title', { timeout: 2000 }); - -await page.screenshot({ - path: path.join(imagesDir, 'angular-login-page.png'), - fullPage: false, -}); -``` - -Navigates directly to `/login` and captures as quickly as possible after the Material card title renders. - -### Solution 2: Block OAuth Redirect (recapture-angular-login.js) -```javascript -// Block navigation to IdentityServer -await page.route('**/connect/authorize**', route => { - route.abort(); -}); - -await page.goto('http://localhost:4200/login', { - waitUntil: 'domcontentloaded', - timeout: 10000 -}); - -await page.waitForTimeout(2000); - -await page.screenshot({ - path: path.join(imagesDir, 'angular-login-page-blocked.png'), - fullPage: false, -}); -``` - -Uses Playwright's route interception to block the OAuth redirect, allowing the page to remain on the login screen indefinitely. - -### Solution 3: Alternative - Continue as Guest Flow - -The login page also has a "Continue as Guest" button that navigates to `/dashboard` without authentication. This button does NOT trigger the auto-redirect, but it's not the primary login flow we want to showcase. - -## Recommended Approach - -**Use Solution 2 (recapture-angular-login.js)** for the cleanest screenshot: - -```bash -cd scripts -npm run screenshots:login -``` - -This script: -1. Clears all cookies/storage for clean state -2. Blocks OAuth redirects -3. Navigates to `/login` route -4. Waits for page to fully render -5. Captures screenshot showing the full login card - -The resulting screenshot will show: -- ✅ "Welcome to Talent Management" title -- ✅ "Sign In with Identity Server" button -- ✅ "Continue as Guest" button -- ✅ Role-based access information -- ✅ Clean, professional Material Design UI - -## Alternative Design Consideration - -If the auto-redirect UX is problematic for users (they never see the login page), consider: - -**Option A:** Remove auto-redirect from `ngOnInit()` -```typescript -ngOnInit() { - // Don't auto-redirect - let user click the button - // this.login(); -} -``` - -**Option B:** Add a delay or user confirmation -```typescript -ngOnInit() { - // Show the page for 2 seconds before auto-redirecting - setTimeout(() => this.login(), 2000); -} -``` - -**Option C:** Only auto-redirect if user came from auth guard -```typescript -ngOnInit() { - const returnUrl = this.route.snapshot.queryParams['returnUrl']; - if (returnUrl) { - // User was blocked by auth guard, auto-redirect - this.login(); - } - // Otherwise, show the page and let them click -} -``` - -Currently the app uses **full auto-redirect** which provides the smoothest OAuth flow but makes the Angular login page essentially a "splash screen" that users rarely see. - -## Files Involved - -**Angular Routing:** -- `src/app/app.routes.ts:74` - Route definition: `{ path: 'login', component: Login }` -- `src/app/routes/sessions/login/login.ts` - Login component with auto-redirect -- `src/app/routes/sessions/login/login.html` - Login page template -- `src/app/core/authentication/oidc-auth.service.ts` - OAuth service - -**Screenshot Scripts:** -- `scripts/take-all-screenshots.js` - Main screenshot script (updated) -- `scripts/recapture-angular-login.js` - Special script for login page -- `scripts/package.json` - npm scripts configuration -- `scripts/README.md` - Documentation - -**Routes:** -- `/` - Root (redirects to `/dashboard` if authenticated, else `/login`) -- `/login` - Angular login page (auto-redirects to IdentityServer) -- `/auth/login` - Alternative login route (uses AuthLayout) -- `/callback` - OAuth callback handler - -## Testing - -To verify the login page renders correctly: - -1. **Clear browser state:** - - Open DevTools (F12) - - Application → Clear storage → Clear site data - -2. **Navigate to login:** - ``` - http://localhost:4200/login - ``` - -3. **Expected behavior:** - - Page loads for a brief moment showing Material card - - Immediately redirects to `https://localhost:44310/connect/authorize?...` - -4. **To see the login page without redirect:** - - Use Playwright with route blocking (Solution 2) - - Or temporarily comment out `this.login()` in `ngOnInit()` diff --git a/scripts/CLEANUP-SUMMARY.md b/scripts/CLEANUP-SUMMARY.md deleted file mode 100644 index 39045bc..0000000 --- a/scripts/CLEANUP-SUMMARY.md +++ /dev/null @@ -1,220 +0,0 @@ -# Screenshot Scripts Cleanup Summary - -## Overview - -Removed all obsolete screenshot scripts and updated documentation to reflect the streamlined, organized structure. - -## Scripts Removed - -### 7 Obsolete Scripts Deleted - -1. **capture-angular-login.js** - Functionality merged into capture-angular.js -2. **capture-identityserver-admin.js** - Superseded by capture-identityserver.js -3. **capture-remaining-screenshots.js** - Functionality merged into capture-angular.js -4. **recapture-angular-login.js** - Superseded by capture-angular.js -5. **take-all-screenshots.js** - Legacy monolithic script, superseded by organized scripts -6. **take-screenshots.js** - Old version, superseded -7. **take-screenshots-simple.js** - Old version, superseded - -## Active Scripts (3 Total) - -### Production Scripts - -1. **capture-angular.js** - - Captures all Angular application screenshots (7 total) - - Includes login flow with ashtyn1 credentials - - Direct URL navigation for reliability - -2. **capture-webapi.js** - - Captures Web API/Swagger UI screenshots (1 total) - - No authentication required - -3. **capture-identityserver.js** - - Captures IdentityServer screenshots (3 total) - - Includes login with admin credentials - - Captures admin UI dashboard - -## Documentation Updated - -### Files Modified - -1. **scripts/package.json** - - Removed: `screenshots:legacy` script - - Removed: `screenshots:login` script - - Clean 4-script configuration - -2. **scripts/README.md** - - Removed "Legacy Scripts" section - - Updated package.json scripts reference - - Streamlined documentation - -3. **scripts/SCREENSHOT-ORGANIZATION.md** - - Removed "Files Preserved (Legacy)" section - - Removed "Rollback Plan" section - - Updated script counts (7 Angular, 3 IdentityServer) - - Removed references to obsolete scripts - -## Before and After - -### Before Cleanup - -``` -scripts/ -├── capture-angular.js ✅ -├── capture-angular-login.js ❌ -├── capture-identityserver.js ✅ -├── capture-identityserver-admin.js ❌ -├── capture-remaining-screenshots.js ❌ -├── capture-webapi.js ✅ -├── recapture-angular-login.js ❌ -├── take-all-screenshots.js ❌ -├── take-screenshots.js ❌ -└── take-screenshots-simple.js ❌ -``` - -**Total:** 10 scripts (3 active, 7 obsolete) - -### After Cleanup - -``` -scripts/ -├── capture-angular.js ✅ -├── capture-identityserver.js ✅ -├── capture-webapi.js ✅ -├── package.json -├── README.md -├── SCREENSHOT-ORGANIZATION.md -└── ANGULAR-LOGIN-ISSUE.md -``` - -**Total:** 3 scripts (all active) - -## NPM Scripts Configuration - -### Before - -```json -{ - "scripts": { - "screenshots:angular": "node capture-angular.js", - "screenshots:webapi": "node capture-webapi.js", - "screenshots:identityserver": "node capture-identityserver.js", - "screenshots:all": "npm run screenshots:angular && npm run screenshots:webapi && npm run screenshots:identityserver", - "screenshots:legacy": "node take-all-screenshots.js", - "screenshots:login": "node recapture-angular-login.js" - } -} -``` - -### After - -```json -{ - "scripts": { - "screenshots:angular": "node capture-angular.js", - "screenshots:webapi": "node capture-webapi.js", - "screenshots:identityserver": "node capture-identityserver.js", - "screenshots:all": "npm run screenshots:angular && npm run screenshots:webapi && npm run screenshots:identityserver" - } -} -``` - -## Benefits of Cleanup - -### 1. Simplified Structure -- Only 3 scripts to maintain -- Clear separation by component -- No confusion about which script to use - -### 2. Consistent Naming -- All scripts follow `capture-{component}.js` pattern -- Easy to understand purpose from filename - -### 3. Reduced Maintenance -- Fewer scripts to update when credentials change -- Single source of truth for each component -- No duplicate functionality - -### 4. Better Documentation -- Removed outdated legacy script references -- Clear usage instructions -- No rollback or migration paths needed - -### 5. Cleaner Repository -- Removed 7 obsolete files -- Reduced confusion for contributors -- Professional, production-ready structure - -## Usage - -All screenshot capture commands remain the same: - -```bash -# Individual components -npm run screenshots:angular # 7 screenshots -npm run screenshots:webapi # 1 screenshot -npm run screenshots:identityserver # 3 screenshots - -# All at once -npm run screenshots:all # 11 screenshots total -``` - -## Screenshots Organization - -All screenshots remain in organized folders: - -``` -docs/images/ -├── angular/ (7 files, 734KB) -├── webapi/ (1 file, 55KB) -└── identityserver/ (3 files, 152KB) - -Total: 11 screenshots, 941KB -``` - -## Testing Verification - -All active scripts have been tested and verified: - -✅ **capture-angular.js** -- 7 screenshots captured successfully -- All pages render correctly -- Login flow works with ashtyn1 credentials - -✅ **capture-webapi.js** -- 1 screenshot captured successfully -- Swagger UI loads properly -- No authentication issues - -✅ **capture-identityserver.js** -- 3 screenshots captured successfully -- Login pages (empty and with admin credentials) -- Admin UI dashboard loads correctly - -## Migration Notes - -### No Breaking Changes - -- All existing screenshots remain in place -- NPM script names unchanged (except removed legacy scripts) -- Documentation structure unchanged -- Image paths unchanged - -### For Contributors - -If you previously used: -- ❌ `npm run screenshots:legacy` - Use `npm run screenshots:all` instead -- ❌ `npm run screenshots:login` - Use `npm run screenshots:angular` instead - -## Conclusion - -The screenshot automation system is now streamlined, well-organized, and production-ready with: - -- ✅ 3 focused scripts (down from 10) -- ✅ Clear component-based organization -- ✅ Consistent naming and structure -- ✅ Updated documentation -- ✅ No duplicate functionality -- ✅ Easier maintenance and contribution - -All 7 obsolete scripts have been successfully removed with no loss of functionality. diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 6bb4573..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,264 +0,0 @@ -# Tutorial Screenshot Automation - -This folder contains Playwright scripts to automatically capture screenshots for the tutorial documentation. - -## Prerequisites - -1. **Install dependencies:** - ```bash - cd scripts - npm install - ``` - -2. **Start all services** (in separate terminals): - ```bash - # Terminal 1: IdentityServer - cd TokenService/Duende-IdentityServer/src/Duende.STS.Identity - dotnet run - - # Terminal 2: API - cd ApiResources/TalentManagement-API - dotnet run - - # Terminal 3: Angular - cd Clients/TalentManagement-Angular-Material/talent-management - npm start - ``` - -3. **Verify services are running:** - - IdentityServer: https://localhost:44310 - - API: https://localhost:44378 - - Angular: http://localhost:4200 - -## Usage - -### Capture Screenshots by Component - -The scripts are organized by application component for better control: - -#### 1. Capture Angular Screenshots -```bash -cd scripts -npm run screenshots:angular -``` - -**Credentials:** `ashtyn1` / `Pa$$word123` - -**Captures:** -- angular-login-page.png - Angular user menu dropdown showing Login link -- identityserver-login-ashtyn1.png - IdentityServer login with ashtyn1 credentials filled -- application-dashboard.png - Dashboard after login -- employee-list-page.png - Employee list view -- search-filtering-ui.png - Search and filtering UI -- employee-form.png - Create employee form (from /employees/create) -- crud-operations.png - CRUD operations demonstration - -**Total:** 7 screenshots - -**Saved to:** `docs/images/angular/` - -#### 2. Capture Web API Screenshots -```bash -cd scripts -npm run screenshots:webapi -``` - -**Credentials:** None (no login required) - -**Captures:** -- swagger-api-endpoints.png - Swagger UI with API endpoints - -**Saved to:** `docs/images/webapi/` - -#### 3. Capture IdentityServer Screenshots -```bash -cd scripts -npm run screenshots:identityserver -``` - -**Credentials:** `admin` / `Pa$$word123` - -**Captures:** -- identityserver-login.png - IdentityServer login page (empty) -- identityserver-login-admin.png - IdentityServer login with admin credentials filled -- identityserver-admin-ui.png - Admin UI dashboard (after login) - -**Total:** 3 screenshots - -**Saved to:** `docs/images/identityserver/` - -#### 4. Capture All Screenshots -```bash -cd scripts -npm run screenshots:all -``` - -Runs all three scripts sequentially. - -## Directory Structure - -Screenshots are organized by application component: - -``` -docs/images/ -├── angular/ # Angular application screenshots (7 files) -│ ├── angular-login-page.png -│ ├── identityserver-login-ashtyn1.png -│ ├── application-dashboard.png -│ ├── employee-list-page.png -│ ├── search-filtering-ui.png -│ ├── employee-form.png -│ └── crud-operations.png -├── webapi/ # Web API screenshots (1 file) -│ └── swagger-api-endpoints.png -└── identityserver/ # IdentityServer screenshots (3 files) - ├── identityserver-login.png - ├── identityserver-login-admin.png - └── identityserver-admin-ui.png -``` - -## Scripts - -### Main Scripts (Organized by Component) - -- **capture-angular.js** - Captures all Angular application screenshots - - Uses Angular user account: `ashtyn1` / `Pa$$word123` - - Navigates through login flow, dashboard, employee management - - Saves to `docs/images/angular/` - -- **capture-webapi.js** - Captures Web API screenshots - - No authentication required - - Captures Swagger UI - - Saves to `docs/images/webapi/` - -- **capture-identityserver.js** - Captures IdentityServer screenshots - - Uses admin account: `admin` / `Pa$$word123` - - Captures login page (empty), login with admin credentials, and admin UI - - Saves to `docs/images/identityserver/` - -## Authentication Credentials - -### Angular Application User -```json -{ - "Username": "ashtyn1", - "Password": "Pa$$word123" -} -``` - -Used by: `capture-angular.js` - -### IdentityServer Admin -```json -{ - "Username": "admin", - "Password": "Pa$$word123" -} -``` - -Used by: `capture-identityserver.js` - -### Web API -No authentication required for Swagger UI. - -## Important Notes - -### Angular Login Page Auto-Redirect - -The Angular login page (`http://localhost:4200/login`) **automatically redirects** to IdentityServer via `ngOnInit()`: - -**File:** `src/app/routes/sessions/login/login.ts:49` -```typescript -ngOnInit() { - // Automatically redirect to IdentityServer login when this page loads - this.login(); -} -``` - -This means: -- The page renders for only ~100ms before redirecting -- Users rarely see this page in normal flow -- Screenshot capture must be very fast or block the redirect - -The `capture-angular.js` script captures this page quickly before the redirect occurs. - -### Direct URL Navigation - -The scripts use direct URL navigation for reliability: -- **Employee List:** `http://localhost:4200/employees` -- **Create Employee:** `http://localhost:4200/employees/create` - -This avoids issues with clicking navigation elements and ensures consistent screenshots. - -## Troubleshooting - -### Services Not Running - -**Error:** `net::ERR_CONNECTION_REFUSED` - -**Solution:** Ensure all three services are running: -```bash -# Check if ports are in use -netstat -ano | findstr ":4200" # Angular -netstat -ano | findstr ":44378" # API -netstat -ano | findstr ":44310" # IdentityServer -``` - -### Authentication Failed - -**Error:** Login fails or wrong credentials - -**Solution:** Verify credentials match: -- Angular: `ashtyn1` / `Pa$$word123` -- IdentityServer Admin: `admin` / `Pa$$word123` - -### Screenshots Are Empty/Wrong - -**Problem:** Screenshots show blank pages or wrong content - -**Solution:** -1. Increase `waitForTimeout` values in scripts -2. Check `waitUntil: 'networkidle'` is working -3. Run scripts in non-headless mode (`headless: false`) to debug - -### Browser Doesn't Close - -**Problem:** Playwright browser windows remain open - -**Solution:** The scripts use `headless: false` for visibility. Change to `headless: true` for automated runs: -```javascript -const browser = await chromium.launch({ - headless: true, // Change to true - slowMo: 1000, -}); -``` - -## Package.json Scripts Reference - -```json -{ - "scripts": { - "screenshots:angular": "node capture-angular.js", - "screenshots:webapi": "node capture-webapi.js", - "screenshots:identityserver": "node capture-identityserver.js", - "screenshots:all": "npm run screenshots:angular && npm run screenshots:webapi && npm run screenshots:identityserver" - } -} -``` - -## Development - -To modify screenshot capture: - -1. Edit the appropriate script (`capture-angular.js`, `capture-webapi.js`, or `capture-identityserver.js`) -2. Adjust selectors, wait times, or navigation URLs -3. Run the specific script to test: `npm run screenshots:angular` -4. Verify screenshots in `docs/images/{component}/` - -## Contributing - -When adding new screenshots: -1. Add capture logic to the appropriate component script -2. Save to the correct `docs/images/{component}/` folder -3. Update this README with the new screenshot details -4. Update tutorial documentation to reference the new images diff --git a/scripts/SCREENSHOT-ORGANIZATION.md b/scripts/SCREENSHOT-ORGANIZATION.md deleted file mode 100644 index f33fcb6..0000000 --- a/scripts/SCREENSHOT-ORGANIZATION.md +++ /dev/null @@ -1,256 +0,0 @@ -# Screenshot Organization Summary - -## Overview - -Screenshot capture scripts have been reorganized from a single monolithic script into three component-specific scripts for better maintainability and control. - -## Changes Made - -### 1. Script Reorganization - -**Before:** -- Single monolithic script -- Captured all screenshots in one run -- Hard to maintain and debug -- All screenshots in flat `docs/images/` directory - -**After:** -- Three component-specific scripts: - - `capture-angular.js` - Angular application - - `capture-webapi.js` - Web API (Swagger) - - `capture-identityserver.js` - IdentityServer -- Can run independently or together -- Organized by logical application boundaries -- Screenshots organized into subdirectories - -### 2. Directory Structure - -``` -docs/images/ -├── angular/ # Angular application screenshots (7 files) -│ ├── angular-login-page.png -│ ├── identityserver-login-ashtyn1.png -│ ├── application-dashboard.png -│ ├── employee-list-page.png -│ ├── search-filtering-ui.png -│ ├── employee-form.png -│ └── crud-operations.png -├── webapi/ # Web API screenshots (1 file) -│ └── swagger-api-endpoints.png -└── identityserver/ # IdentityServer screenshots (3 files) - ├── identityserver-login.png - ├── identityserver-login-admin.png - └── identityserver-admin-ui.png -``` - -### 3. Authentication Credentials - -Scripts now use the correct credentials for each component: - -**Angular User (capture-angular.js):** -```json -{ - "Username": "ashtyn1", - "Password": "Pa$$word123" -} -``` - -**IdentityServer Admin (capture-identityserver.js):** -```json -{ - "Username": "admin", - "Password": "Pa$$word123" -} -``` - -**Web API:** -- No authentication required - -### 4. NPM Scripts - -**Updated package.json:** -```json -{ - "scripts": { - "screenshots:angular": "node capture-angular.js", - "screenshots:webapi": "node capture-webapi.js", - "screenshots:identityserver": "node capture-identityserver.js", - "screenshots:all": "npm run screenshots:angular && npm run screenshots:webapi && npm run screenshots:identityserver" - } -} -``` - -### 5. Script Features - -#### capture-angular.js -- **Purpose:** Capture all Angular UI screenshots -- **Authentication:** ashtyn1 / Pa$$word123 -- **Screenshots:** 7 total - - Angular user menu dropdown with Login link visible - - IdentityServer login with ashtyn1 credentials filled - - Dashboard - - Employee list - - Search/filtering UI - - Create employee form (navigates to /employees/create) - - CRUD operations -- **Output:** `docs/images/angular/` - -#### capture-webapi.js -- **Purpose:** Capture Web API documentation -- **Authentication:** None -- **Screenshots:** 1 total - - Swagger UI with API endpoints -- **Output:** `docs/images/webapi/` - -#### capture-identityserver.js -- **Purpose:** Capture IdentityServer UI -- **Authentication:** admin / Pa$$word123 -- **Screenshots:** 3 total - - Login page (empty) - - Login page with admin credentials filled - - Admin UI dashboard (after login) -- **Output:** `docs/images/identityserver/` - -## Usage - -### Run Individual Components - -```bash -# Angular screenshots only -cd scripts -npm run screenshots:angular - -# Web API screenshots only -cd scripts -npm run screenshots:webapi - -# IdentityServer screenshots only -cd scripts -npm run screenshots:identityserver -``` - -### Run All Components - -```bash -cd scripts -npm run screenshots:all -``` - -This runs all three scripts sequentially. - -## Benefits - -### 1. Separation of Concerns -- Each script focuses on one application component -- Easier to understand and maintain -- Clear responsibility boundaries - -### 2. Independent Execution -- Run only what you need -- Faster iteration when updating specific components -- Don't need all services running to capture some screenshots - -### 3. Organized Output -- Screenshots grouped by component -- Easier to find specific images -- Better for documentation structure - -### 4. Correct Authentication -- Each script uses appropriate credentials -- No confusion about which account to use -- Matches actual user roles - -### 5. Better Error Handling -- Failures in one component don't affect others -- Easier to debug issues -- Clear error messages by component - -## Migration Guide - -### For Developers - -**Workflow:** -```bash -cd scripts -npm run screenshots:all # Runs all three scripts -# OR -npm run screenshots:angular # Just Angular -npm run screenshots:webapi # Just Web API -npm run screenshots:identityserver # Just IdentityServer -``` - -### For Documentation - -**Old image paths:** -```markdown -![Dashboard](../images/application-dashboard.png) -``` - -**New image paths:** -```markdown -![Dashboard](../images/angular/application-dashboard.png) -![Swagger](../images/webapi/swagger-api-endpoints.png) -![Admin UI](../images/identityserver/identityserver-admin-ui.png) -``` - -## Files Created - -1. **capture-angular.js** - Angular screenshot script -2. **capture-webapi.js** - Web API screenshot script -3. **capture-identityserver.js** - IdentityServer screenshot script -4. **SCREENSHOT-ORGANIZATION.md** - This documentation - -## Files Modified - -1. **package.json** - Updated npm scripts -2. **README.md** - Complete rewrite with new organization - -## Testing - -All scripts have been tested and verified: - -✅ **capture-angular.js** - Successfully captured 7 screenshots -- File sizes: 41KB - 149KB -- All pages rendered correctly -- Login flow worked with ashtyn1 credentials - -✅ **capture-webapi.js** - Successfully captured 1 screenshot -- File size: 55KB -- Swagger UI loaded properly -- No authentication issues - -✅ **capture-identityserver.js** - Successfully captured 3 screenshots -- File sizes: 40KB - 71KB -- Login page captured (empty and with admin credentials) -- Admin UI dashboard loaded after login with admin credentials - -## Next Steps - -### Optional Improvements - -1. **Add more screenshots:** - - Department management - - Position management - - Salary range management - - Profile pages - -2. **Enhance scripts:** - - Add command-line arguments for credentials - - Support different environments (dev, staging, prod) - - Add screenshot comparison for visual regression testing - -3. **Documentation:** - - Update tutorial markdown files with new image paths - - Create diagram explaining CAT pattern architecture - - Add JWT token structure diagram - -### Breaking Changes - -⚠️ **Image paths have changed!** - -All documentation referencing screenshots must be updated to use the new organized paths: -- `images/*.png` → `images/angular/*.png` -- `images/*.png` → `images/webapi/*.png` -- `images/*.png` → `images/identityserver/*.png` - -A find-and-replace operation will be needed across all markdown documentation files. diff --git a/scripts/capture-angular.js b/scripts/capture-angular.js deleted file mode 100644 index 2f8f09c..0000000 --- a/scripts/capture-angular.js +++ /dev/null @@ -1,322 +0,0 @@ -// Script to capture Angular application screenshots -const { chromium } = require('playwright'); -const path = require('path'); -const fs = require('fs'); - -const imagesDir = path.join(__dirname, '..', 'docs', 'images', 'angular'); -if (!fs.existsSync(imagesDir)) { - fs.mkdirSync(imagesDir, { recursive: true }); -} - -async function captureAngularScreenshots() { - const browser = await chromium.launch({ - headless: false, - slowMo: 800, - }); - - const context = await browser.newContext({ - viewport: { width: 1920, height: 1080 }, - ignoreHTTPSErrors: true, - }); - - const page = await context.newPage(); - - const userIconSelectors = [ - 'button:has(mat-icon:has-text("account_circle"))', - 'button[aria-label*="user" i]', - 'button[aria-label*="account" i]', - 'button mat-icon:has-text("account_circle")', - 'button mat-icon:has-text("person")', - 'mat-toolbar button:last-child', - ]; - - async function openUserMenu() { - for (const selector of userIconSelectors) { - const icon = page.locator(selector).first(); - if (await icon.count() > 0) { - await icon.click({ timeout: 5000 }); - await page.waitForTimeout(500); - return true; - } - } - return false; - } - - async function clickFirstAvailable(selectors) { - for (const selector of selectors) { - try { - const element = page.locator(selector).first(); - if (await element.count() > 0) { - await element.click({ timeout: 5000 }); - return true; - } - } catch (e) { - // continue - } - } - return false; - } - - async function captureEntityScreenshots({ routeBase, filePrefix, label, createSelectors }) { - console.log(`Capturing ${label} list page...`); - await page.goto(`http://localhost:4200/${routeBase}`, { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(1500); - await page.screenshot({ - path: path.join(imagesDir, `${filePrefix}-list-page.png`), - fullPage: true, - }); - console.log(` Saved: ${filePrefix}-list-page.png`); - - console.log(`Capturing ${label} search and filtering UI...`); - await page.screenshot({ - path: path.join(imagesDir, `${filePrefix}-search-filtering-ui.png`), - fullPage: true, - }); - console.log(` Saved: ${filePrefix}-search-filtering-ui.png`); - - console.log(`Capturing ${label} CRUD operations overview...`); - await page.waitForTimeout(1200); - await page.screenshot({ - path: path.join(imagesDir, `${filePrefix}-crud-operations.png`), - fullPage: true, - }); - console.log(` Saved: ${filePrefix}-crud-operations.png`); - - console.log(`Capturing create ${label} form...`); - const createClicked = await clickFirstAvailable(createSelectors); - if (!createClicked) { - await page.goto(`http://localhost:4200/${routeBase}/create`, { waitUntil: 'networkidle', timeout: 30000 }); - } else { - await page.waitForURL(`**/${routeBase}/create**`, { timeout: 15000 }).catch(async () => { - await page.goto(`http://localhost:4200/${routeBase}/create`, { waitUntil: 'networkidle', timeout: 30000 }); - }); - } - - await page.waitForTimeout(1200); - await page.screenshot({ - path: path.join(imagesDir, `${filePrefix}-form.png`), - fullPage: true, - }); - console.log(` Saved: ${filePrefix}-form.png\n`); - } - - try { - console.log('Capturing Angular application screenshots...\n'); - - console.log('1) Capturing anonymous dashboard before login...'); - await page.goto('http://localhost:4200/dashboard', { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(1500); - await page.screenshot({ - path: path.join(imagesDir, 'application-dashboard-anonymous.png'), - fullPage: false, - }); - console.log(' Saved: application-dashboard-anonymous.png\n'); - - console.log('2) Capturing user menu with login link...'); - const menuOpenedBeforeLogin = await openUserMenu(); - if (!menuOpenedBeforeLogin) { - throw new Error('Could not open user menu before login.'); - } - await page.screenshot({ - path: path.join(imagesDir, 'angular-login-page.png'), - fullPage: false, - }); - console.log(' Saved: angular-login-page.png\n'); - - console.log('3) Logging in via IdentityServer...'); - let loginClicked = await clickFirstAvailable([ - '[role="menuitem"]:has-text("Login")', - '[role="menuitem"]:has-text("Sign in")', - 'button:has-text("Login")', - 'a:has-text("Login")', - 'a[href*="/login"]', - ]); - - if (!loginClicked) { - const menuReopened = await openUserMenu(); - if (menuReopened) { - loginClicked = await clickFirstAvailable([ - '[role="menuitem"]:has-text("Login")', - '[role="menuitem"]:has-text("Sign in")', - 'button:has-text("Login")', - 'a:has-text("Login")', - 'a[href*="/login"]', - ]); - } - } - - if (!loginClicked) { - throw new Error('Could not click Login from Angular user menu.'); - } - - await page.waitForURL('**://localhost:44310/**', { timeout: 30000 }); - await page.waitForTimeout(1200); - - await page.fill('input[name="Username"], input[name="username"], input#Username', 'ashtyn1'); - await page.fill('input[name="Password"], input[name="password"], input#Password', 'Pa$$word123'); - await page.waitForTimeout(400); - - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-login-ashtyn1.png'), - fullPage: false, - }); - console.log(' Saved: identityserver-login-ashtyn1.png'); - - await clickFirstAvailable([ - 'button[type="submit"]', - 'button:has-text("Login")', - 'input[type="submit"]', - ]); - - await page.waitForURL('http://localhost:4200/**', { timeout: 30000 }).catch(async () => { - await page.goto('http://localhost:4200/dashboard', { waitUntil: 'networkidle', timeout: 30000 }); - }); - await page.waitForTimeout(1500); - console.log(' Logged in successfully\n'); - - console.log('4) Capturing profile page from user menu...'); - const profileMenuOpened = await openUserMenu(); - if (!profileMenuOpened) { - throw new Error('Could not open user menu to navigate to profile page.'); - } - - const profileClicked = await clickFirstAvailable([ - '[role="menuitem"]:has-text("Profile")', - '[role="menuitem"]:has-text("Overview")', - 'a:has-text("Profile")', - 'button:has-text("Profile")', - 'a[href*="/profile"]', - ]); - - if (!profileClicked) { - throw new Error('Could not click Profile from user menu.'); - } - - await page.waitForURL('**/profile/**', { timeout: 15000 }).catch(async () => { - await page.goto('http://localhost:4200/profile/overview', { waitUntil: 'networkidle', timeout: 30000 }); - }); - await page.waitForTimeout(1200); - await page.screenshot({ - path: path.join(imagesDir, 'profile-overview-page.png'), - fullPage: true, - }); - console.log(' Saved: profile-overview-page.png\n'); - - console.log('5) Capturing entity pages for Employee, Department, Position, and Salary Range...'); - await captureEntityScreenshots({ - routeBase: 'employees', - filePrefix: 'employee', - label: 'employee', - createSelectors: [ - 'a:has-text("Create")', - 'button:has-text("Create")', - 'a:has-text("Add Employee")', - 'button:has-text("Add Employee")', - 'a[href*="/employees/create"]', - 'button[routerlink*="/employees/create"]', - ], - }); - - await captureEntityScreenshots({ - routeBase: 'departments', - filePrefix: 'department', - label: 'department', - createSelectors: [ - 'a:has-text("Create")', - 'button:has-text("Create")', - 'a:has-text("Add Department")', - 'button:has-text("Add Department")', - 'a[href*="/departments/create"]', - 'button[routerlink*="/departments/create"]', - ], - }); - - await captureEntityScreenshots({ - routeBase: 'positions', - filePrefix: 'position', - label: 'position', - createSelectors: [ - 'a:has-text("Create")', - 'button:has-text("Create")', - 'a:has-text("Add Position")', - 'button:has-text("Add Position")', - 'a[href*="/positions/create"]', - 'button[routerlink*="/positions/create"]', - ], - }); - - await captureEntityScreenshots({ - routeBase: 'salary-ranges', - filePrefix: 'salary-range', - label: 'salary range', - createSelectors: [ - 'a:has-text("Create")', - 'button:has-text("Create")', - 'a:has-text("Add Salary Range")', - 'button:has-text("Add Salary Range")', - 'a[href*="/salary-ranges/create"]', - 'button[routerlink*="/salary-ranges/create"]', - ], - }); - - console.log('6) Capturing user menu with logout link...'); - await page.goto('http://localhost:4200/dashboard', { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(1000); - const menuOpenedAfterLogin = await openUserMenu(); - if (!menuOpenedAfterLogin) { - throw new Error('Could not open user menu after login.'); - } - - await page.screenshot({ - path: path.join(imagesDir, 'user-menu-logout-link.png'), - fullPage: false, - }); - console.log(' Saved: user-menu-logout-link.png'); - - const logoutClicked = await clickFirstAvailable([ - '[role="menuitem"]:has-text("Logout")', - '[role="menuitem"]:has-text("Log out")', - 'button:has-text("Logout")', - 'a:has-text("Logout")', - 'a[href*="logout" i]', - ]); - - if (!logoutClicked) { - throw new Error('Could not click Logout menu item.'); - } - - await page.waitForTimeout(1200); - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-logout-intermediate.png'), - fullPage: true, - }); - console.log(` Saved: identityserver-logout-intermediate.png (URL: ${page.url()})`); - - const hereClicked = await clickFirstAvailable([ - 'a:has-text("Click Here")', - 'a:has-text("here")', - 'a:has-text("click here")', - 'form button[type="submit"]', - 'form input[type="submit"]', - ]); - - if (hereClicked) { - await page.waitForURL('http://localhost:4200/**', { timeout: 30000 }).catch(async () => { - await page.goto('http://localhost:4200', { waitUntil: 'networkidle', timeout: 30000 }); - }); - console.log(' Clicked Here and redirected back to Angular.\n'); - } else { - console.log(' Could not find Click Here link/button on logout page.\n'); - } - - console.log('Angular screenshot flow completed successfully.'); - console.log(`Screenshots saved to: ${imagesDir}\n`); - } catch (error) { - console.error('Error taking screenshots:', error.message); - console.error('\nStack trace:', error.stack); - } finally { - await browser.close(); - } -} - -captureAngularScreenshots().catch(console.error); diff --git a/scripts/capture-identityserver.js b/scripts/capture-identityserver.js deleted file mode 100644 index 80eb311..0000000 --- a/scripts/capture-identityserver.js +++ /dev/null @@ -1,254 +0,0 @@ -// Script to capture IdentityServer screenshots -const { chromium } = require('playwright'); -const path = require('path'); -const fs = require('fs'); - -// Create identityserver images directory -const imagesDir = path.join(__dirname, '..', 'docs', 'images', 'identityserver'); -if (!fs.existsSync(imagesDir)) { - fs.mkdirSync(imagesDir, { recursive: true }); -} - -async function captureIdentityServerScreenshots() { - const browser = await chromium.launch({ - headless: false, - slowMo: 1000, - }); - - const context = await browser.newContext({ - viewport: { width: 1920, height: 1080 }, - ignoreHTTPSErrors: true, - }); - - const page = await context.newPage(); - - try { - console.log('Capturing IdentityServer screenshots...\n'); - - // 1. Open IdentityServer login page - console.log('1) Opening IdentityServer login page...'); - await page.goto('https://localhost:44310/Account/Login', { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(2000); - console.log(' Login page loaded\n'); - - // 2. IdentityServer Login Page with admin credentials - console.log('2) Capturing IdentityServer login with admin credentials...'); - await page.fill('input[name="Username"], input[name="username"], input#Username', 'admin'); - await page.fill('input[name="Password"], input[name="password"], input#Password', 'Pa$$word123'); - await page.waitForTimeout(500); - - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-login-admin.png'), - fullPage: false, - }); - console.log(' Saved: identityserver-login-admin.png\n'); - - // 3. IdentityServer Admin UI - console.log('3) Navigating to IdentityServer Admin UI...'); - await page.goto('https://localhost:44303', { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(2000); - - // Check if we need to login to Admin UI - const hasLoginForm = await page.locator('input[name="Username"], input[name="username"]').count() > 0; - - if (hasLoginForm) { - console.log(' Logging in with admin credentials...'); - - // Fill in admin credentials - await page.fill('input[name="Username"], input[name="username"]', 'admin'); - await page.fill('input[name="Password"], input[name="password"]', 'Pa$$word123'); - - // Click login button - const submitSelectors = [ - 'button[type="submit"]', - 'button:has-text("Login")', - 'button:has-text("Sign In")', - 'input[type="submit"]', - ]; - - let submitted = false; - for (const selector of submitSelectors) { - try { - const button = page.locator(selector).first(); - if (await button.count() > 0) { - await button.click({ timeout: 5000 }); - submitted = true; - break; - } - } catch (e) { - // continue trying submit selectors - } - } - - if (!submitted) { - await page.press('input[name="Password"], input[name="password"], input#Password', 'Enter'); - } - - // Wait for dashboard to load - await page.waitForTimeout(5000); - console.log(' Logged in successfully'); - } else { - console.log(' Already logged in or no login required'); - } - - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-admin-dashboard.png'), - fullPage: false, - }); - console.log(' Saved: identityserver-admin-dashboard.png\n'); - - // 4. Clients configuration list - console.log('4) Navigating to Clients configuration screen...'); - await page.goto('https://localhost:44303/Configuration/Clients', { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(2000); - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-clients-list.png'), - fullPage: true, - }); - console.log(' Saved: identityserver-clients-list.png\n'); - - // 5. Edit TalentManagement client and capture Urls tab - console.log('5) Navigating to TalentManagement client edit page...'); - await page.goto('https://localhost:44303/Configuration/Client/2', { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(2000); - - // Try to switch to Urls tab if tab navigation is available - const urlsTabSelectors = [ - 'a:has-text("Urls")', - 'button:has-text("Urls")', - '[role="tab"]:has-text("Urls")', - 'li:has-text("Urls")', - ]; - let urlsTabClicked = false; - for (const selector of urlsTabSelectors) { - try { - const tab = page.locator(selector).first(); - if (await tab.count() > 0) { - await tab.click({ timeout: 5000 }); - urlsTabClicked = true; - break; - } - } catch (e) { - // continue trying Urls tab selectors - } - } - - if (!urlsTabClicked) { - console.log(' Urls tab was not auto-detected; capturing current client edit view.'); - } else { - await page.waitForTimeout(1500); - } - - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-client-talentmanagement-urls-tab.png'), - fullPage: true, - }); - console.log(' Saved: identityserver-client-talentmanagement-urls-tab.png\n'); - - // 6. Advanced tab > Grant Types sub tab - console.log('6) Capturing Advanced tab - Grant Types...'); - const advancedTabSelectors = [ - 'a:has-text("Advanced")', - 'button:has-text("Advanced")', - '[role="tab"]:has-text("Advanced")', - 'li:has-text("Advanced")', - ]; - for (const selector of advancedTabSelectors) { - try { - const advancedTab = page.locator(selector).first(); - if (await advancedTab.count() > 0) { - await advancedTab.click({ timeout: 5000 }); - break; - } - } catch (e) { - // continue trying advanced tab selectors - } - } - await page.waitForTimeout(1500); - - const grantTypesSelectors = [ - 'a:has-text("Grant Types")', - 'button:has-text("Grant Types")', - '[role="tab"]:has-text("Grant Types")', - 'li:has-text("Grant Types")', - ]; - for (const selector of grantTypesSelectors) { - try { - const grantTab = page.locator(selector).first(); - if (await grantTab.count() > 0) { - await grantTab.click({ timeout: 5000 }); - break; - } - } catch (e) { - // continue trying grant types selectors - } - } - await page.waitForTimeout(1500); - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-client-talentmanagement-advanced-grant-types.png'), - fullPage: true, - }); - console.log(' Saved: identityserver-client-talentmanagement-advanced-grant-types.png\n'); - - // 7. Advanced tab > Token sub tab - console.log('7) Capturing Advanced tab - Token...'); - for (const selector of advancedTabSelectors) { - try { - const advancedTab = page.locator(selector).first(); - if (await advancedTab.count() > 0) { - await advancedTab.click({ timeout: 5000 }); - break; - } - } catch (e) { - // continue trying advanced tab selectors - } - } - await page.waitForTimeout(1200); - - const tokenSelectors = [ - 'a:has-text("Token")', - 'button:has-text("Token")', - '[role="tab"]:has-text("Token")', - 'li:has-text("Token")', - ]; - for (const selector of tokenSelectors) { - try { - const tokenTab = page.locator(selector).first(); - if (await tokenTab.count() > 0) { - await tokenTab.click({ timeout: 5000 }); - break; - } - } catch (e) { - // continue trying token selectors - } - } - await page.waitForTimeout(1500); - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-client-talentmanagement-advanced-token.png'), - fullPage: true, - }); - console.log(' Saved: identityserver-client-talentmanagement-advanced-token.png\n'); - - // 8. Swagger UI - console.log('8) Capturing Swagger UI...'); - await page.goto('https://localhost:44302/swagger/index.html', { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(2000); - await page.screenshot({ - path: path.join(imagesDir, 'identityserver-swagger-ui.png'), - fullPage: true, - }); - console.log(' Saved: identityserver-swagger-ui.png\n'); - - console.log('All IdentityServer screenshots captured successfully!'); - console.log(`Screenshots saved to: ${imagesDir}\n`); - - } catch (error) { - console.error('Error taking screenshots:', error.message); - console.error('\nStack trace:', error.stack); - } finally { - await browser.close(); - } -} - -captureIdentityServerScreenshots().catch(console.error); - diff --git a/scripts/capture-webapi.js b/scripts/capture-webapi.js deleted file mode 100644 index d5a02e6..0000000 --- a/scripts/capture-webapi.js +++ /dev/null @@ -1,109 +0,0 @@ -// Script to capture Web API screenshots (Swagger UI) -const { chromium } = require('playwright'); -const path = require('path'); -const fs = require('fs'); - -// Create webapi images directory -const imagesDir = path.join(__dirname, '..', 'docs', 'images', 'webapi'); -if (!fs.existsSync(imagesDir)) { - fs.mkdirSync(imagesDir, { recursive: true }); -} - -async function captureWebAPIScreenshots() { - const browser = await chromium.launch({ - headless: false, - slowMo: 1000, - }); - - const context = await browser.newContext({ - viewport: { width: 1920, height: 1080 }, - ignoreHTTPSErrors: true, - }); - - const page = await context.newPage(); - - async function toggleResource(tagName) { - const headerSelectors = [ - `.opblock-tag-section:has(.opblock-tag:has-text("${tagName}")) .opblock-tag`, - `.opblock-tag:has-text("${tagName}")`, - `h3.opblock-tag:has-text("${tagName}")`, - `[data-tag="${tagName}"] .opblock-tag`, - ]; - - for (const selector of headerSelectors) { - const target = page.locator(selector).first(); - if (await target.count() > 0) { - await target.click(); - return true; - } - } - return false; - } - - async function captureExpandedResource(tagName, outputFile) { - const clicked = await toggleResource(tagName); - - if (!clicked) { - throw new Error(`Could not find Swagger resource tag: ${tagName}`); - } - - await page.waitForTimeout(1500); - - await page.screenshot({ - path: path.join(imagesDir, outputFile), - fullPage: true, - }); - } - - function toKebabCase(value) { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); - } - - try { - console.log('Capturing Web API screenshots...\n'); - - // 1. Swagger UI landing - console.log('1) Opening Swagger UI...'); - await page.goto('https://localhost:44378/swagger/index.html', { waitUntil: 'networkidle', timeout: 30000 }); - await page.waitForTimeout(2000); - - const resourceTags = await page.locator('.opblock-tag').allTextContents(); - const tags = resourceTags - .map((tag) => tag.trim()) - .filter((tag) => tag.length > 0); - - if (tags.length === 0) { - throw new Error('No Swagger resources were found.'); - } - - let previousTag = null; - let step = 2; - for (const tagName of tags) { - if (previousTag) { - await toggleResource(previousTag); - await page.waitForTimeout(300); - } - - const outputFile = `swagger-${toKebabCase(tagName)}-resource-expanded.png`; - console.log(`${step}) Capturing expanded ${tagName} resource...`); - await captureExpandedResource(tagName, outputFile); - console.log(` Saved: ${outputFile}\n`); - - previousTag = tagName; - step += 1; - } - - console.log('All Web API screenshots captured successfully!'); - console.log(`Screenshots saved to: ${imagesDir}\n`); - } catch (error) { - console.error('Error taking screenshots:', error.message); - console.error('\nStack trace:', error.stack); - } finally { - await browser.close(); - } -} - -captureWebAPIScreenshots().catch(console.error); diff --git a/scripts/package-lock.json b/scripts/package-lock.json deleted file mode 100644 index cb74514..0000000 --- a/scripts/package-lock.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "tutorial-screenshots", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "tutorial-screenshots", - "version": "1.0.0", - "dependencies": { - "playwright": "^1.49.0" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/scripts/package.json b/scripts/package.json deleted file mode 100644 index 17c7ba6..0000000 --- a/scripts/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "tutorial-screenshots", - "version": "1.0.0", - "description": "Playwright scripts for capturing tutorial screenshots", - "scripts": { - "screenshots:angular": "node capture-angular.js", - "screenshots:webapi": "node capture-webapi.js", - "screenshots:identityserver": "node capture-identityserver.js", - "screenshots:all": "npm run screenshots:angular && npm run screenshots:webapi && npm run screenshots:identityserver" - }, - "dependencies": { - "playwright": "^1.49.0" - } -} diff --git a/setup-submodules.ps1 b/setup-submodules.ps1 deleted file mode 100644 index 7d34e5f..0000000 --- a/setup-submodules.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -# PowerShell script to set up Git submodules for CAT Pattern Tutorial - -Write-Host "Setting up Git submodules for CAT Pattern Tutorial..." -ForegroundColor Green - -# Remove existing folders -Write-Host "`nRemoving existing folders..." -ForegroundColor Yellow -Remove-Item -Path "Clients\TalentManagement-Angular-Material" -Recurse -Force -ErrorAction SilentlyContinue -Remove-Item -Path "ApiResources\TalentManagement-API" -Recurse -Force -ErrorAction SilentlyContinue -Remove-Item -Path "TokenService\Duende-IdentityServer" -Recurse -Force -ErrorAction SilentlyContinue - -# Add submodules -Write-Host "`nAdding Angular client submodule..." -ForegroundColor Cyan -git submodule add https://github.com/workcontrolgit/TalentManagement-Angular-Material.git Clients/TalentManagement-Angular-Material - -Write-Host "Adding API submodule..." -ForegroundColor Cyan -git submodule add https://github.com/workcontrolgit/TalentManagement-API.git ApiResources/TalentManagement-API - -Write-Host "Adding IdentityServer submodule..." -ForegroundColor Cyan -git submodule add https://github.com/workcontrolgit/Duende-IdentityServer.git TokenService/Duende-IdentityServer - -# Initialize -Write-Host "`nInitializing submodules..." -ForegroundColor Cyan -git submodule update --init --recursive - -Write-Host "`nDone!" -ForegroundColor Green -Write-Host "`nDon't forget to commit the changes:" -ForegroundColor Yellow -Write-Host " git add ." -ForegroundColor White -Write-Host " git commit -m 'Add submodules for Angular client, API, and IdentityServer'" -ForegroundColor White diff --git a/silent-refresh.html b/silent-refresh.html new file mode 100644 index 0000000..c4e2e74 --- /dev/null +++ b/silent-refresh.html @@ -0,0 +1,12 @@ + + + + + Silent Refresh + + + + + diff --git a/staticwebapp.config.json b/staticwebapp.config.json new file mode 100644 index 0000000..325d9f6 --- /dev/null +++ b/staticwebapp.config.json @@ -0,0 +1,14 @@ +{ + "navigationFallback": { + "rewrite": "/index.html", + "exclude": ["/api/*", "/*.{css,js,png,jpg,svg,ico,woff,woff2,ttf,eot}"] + }, + "mimeTypes": { + ".json": "application/json" + }, + "globalHeaders": { + "X-Frame-Options": "SAMEORIGIN", + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "strict-origin-when-cross-origin" + } +} diff --git a/styles-3APQE4AP.css b/styles-3APQE4AP.css new file mode 100644 index 0000000..1d6d5ea --- /dev/null +++ b/styles-3APQE4AP.css @@ -0,0 +1 @@ +:root{--body-font-family: "Roboto", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;--code-font-family: "Roboto Mono", monospace;--gutter: 1rem;--sidenav-width: 15rem;--sidenav-collapsed-width: 4rem;--body-background-color: var(--mat-sys-surface-container);--link-color: var(--mat-sys-primary);--link-hover-color: var(--mat-sys-primary);--code-border-color: light-dark(rgba(19, 28, 43, .08), rgba(218, 226, 249, .08));--code-background-color: light-dark(rgba(19, 28, 43, .04), rgba(218, 226, 249, .04));--header-background-color: light-dark(rgba(239, 237, 240, .8), rgba(31, 32, 34, .8));--user-panel-background-color: var(--mat-sys-surface-container);--user-panel-hover-background-color: var(--mat-sys-surface-variant);--sidemenu-heading-hover-background-color: var(--mat-sys-surface-container-highest);--sidemenu-active-heading-text-color: var(--mat-sys-primary);--sidemenu-active-heading-background-color: var(--mat-sys-primary-container);--sidemenu-active-heading-hover-background-color: var(--mat-sys-primary-container);--sidemenu-expanded-background-color: var(--mat-sys-surface-container);--topmenu-text-color: var(--mat-sys-on-background);--topmenu-background-color: var(--header-background-color);--topmenu-item-active-background-color: var(--mat-sys-primary-container);--topmenu-dropdown-item-active-text-color: var(--mat-sys-primary);--mat-toolbar-standard-height: 4rem;--mat-toolbar-mobile-height: 3.5rem}*,:after,:before{box-sizing:border-box}html,body{position:relative;height:100%;overflow:auto;background-color:var(--body-background-color)}body{margin:0;font-family:var(--body-font-family);line-height:1.5;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;text-size-adjust:100%;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}dl,ol,ul{margin-top:0;margin-bottom:1rem}code,kbd,pre,samp{font-family:var(--code-font-family)}code{padding:.125rem .25rem;font-size:80%;overflow-wrap:break-word;background-color:var(--code-background-color);border:1px solid var(--code-border-color);border-radius:.25rem}a{color:var(--link-color)}a:hover{color:var(--link-hover-color)}@media (width <= 720px){.hide-small{display:none!important}.show-small{display:block!important}}html{color-scheme:light;--mat-sys-background: light-dark(#faf9fd, #121316);--mat-sys-error: light-dark(#ba1a1a, #ffb4ab);--mat-sys-error-container: light-dark(#ffdad6, #93000a);--mat-sys-inverse-on-surface: light-dark(#f2f0f4, #2f3033);--mat-sys-inverse-primary: light-dark(#abc7ff, #005cbb);--mat-sys-inverse-surface: light-dark(#2f3033, #e3e2e6);--mat-sys-on-background: light-dark(#1a1b1f, #e3e2e6);--mat-sys-on-error: light-dark(#ffffff, #690005);--mat-sys-on-error-container: light-dark(#93000a, #ffdad6);--mat-sys-on-primary: light-dark(#ffffff, #002f65);--mat-sys-on-primary-container: light-dark(#00458f, #d7e3ff);--mat-sys-on-primary-fixed: light-dark(#001b3f, #001b3f);--mat-sys-on-primary-fixed-variant: light-dark(#00458f, #00458f);--mat-sys-on-secondary: light-dark(#ffffff, #283041);--mat-sys-on-secondary-container: light-dark(#3e4759, #dae2f9);--mat-sys-on-secondary-fixed: light-dark(#131c2b, #131c2b);--mat-sys-on-secondary-fixed-variant: light-dark(#3e4759, #3e4759);--mat-sys-on-surface: light-dark(#1a1b1f, #e3e2e6);--mat-sys-on-surface-variant: light-dark(#44474e, #e0e2ec);--mat-sys-on-tertiary: light-dark(#ffffff, #0001ac);--mat-sys-on-tertiary-container: light-dark(#0000ef, #e0e0ff);--mat-sys-on-tertiary-fixed: light-dark(#00006e, #00006e);--mat-sys-on-tertiary-fixed-variant: light-dark(#0000ef, #0000ef);--mat-sys-outline: light-dark(#74777f, #8e9099);--mat-sys-outline-variant: light-dark(#c4c6d0, #44474e);--mat-sys-primary: light-dark(#005cbb, #abc7ff);--mat-sys-primary-container: light-dark(#d7e3ff, #00458f);--mat-sys-primary-fixed: light-dark(#d7e3ff, #d7e3ff);--mat-sys-primary-fixed-dim: light-dark(#abc7ff, #abc7ff);--mat-sys-scrim: light-dark(#000000, #000000);--mat-sys-secondary: light-dark(#565e71, #bec6dc);--mat-sys-secondary-container: light-dark(#dae2f9, #3e4759);--mat-sys-secondary-fixed: light-dark(#dae2f9, #dae2f9);--mat-sys-secondary-fixed-dim: light-dark(#bec6dc, #bec6dc);--mat-sys-shadow: light-dark(#000000, #000000);--mat-sys-surface: light-dark(#faf9fd, #121316);--mat-sys-surface-bright: light-dark(#faf9fd, #38393c);--mat-sys-surface-container: light-dark(#efedf0, #1f2022);--mat-sys-surface-container-high: light-dark(#e9e7eb, #292a2c);--mat-sys-surface-container-highest: light-dark(#e3e2e6, #343537);--mat-sys-surface-container-low: light-dark(#f4f3f6, #1a1b1f);--mat-sys-surface-container-lowest: light-dark(#ffffff, #0d0e11);--mat-sys-surface-dim: light-dark(#dbd9dd, #121316);--mat-sys-surface-tint: light-dark(#005cbb, #abc7ff);--mat-sys-surface-variant: light-dark(#e0e2ec, #44474e);--mat-sys-tertiary: light-dark(#343dff, #bec2ff);--mat-sys-tertiary-container: light-dark(#e0e0ff, #0000ef);--mat-sys-tertiary-fixed: light-dark(#e0e0ff, #e0e0ff);--mat-sys-tertiary-fixed-dim: light-dark(#bec2ff, #bec2ff);--mat-sys-neutral-variant20: #2d3038;--mat-sys-neutral10: #1a1b1f;--mat-sys-level0: 0px 0px 0px 0px rgba(0, 0, 0, .2), 0px 0px 0px 0px rgba(0, 0, 0, .14), 0px 0px 0px 0px rgba(0, 0, 0, .12);--mat-sys-level1: 0px 2px 1px -1px rgba(0, 0, 0, .2), 0px 1px 1px 0px rgba(0, 0, 0, .14), 0px 1px 3px 0px rgba(0, 0, 0, .12);--mat-sys-level2: 0px 3px 3px -2px rgba(0, 0, 0, .2), 0px 3px 4px 0px rgba(0, 0, 0, .14), 0px 1px 8px 0px rgba(0, 0, 0, .12);--mat-sys-level3: 0px 3px 5px -1px rgba(0, 0, 0, .2), 0px 6px 10px 0px rgba(0, 0, 0, .14), 0px 1px 18px 0px rgba(0, 0, 0, .12);--mat-sys-level4: 0px 5px 5px -3px rgba(0, 0, 0, .2), 0px 8px 10px 1px rgba(0, 0, 0, .14), 0px 3px 14px 2px rgba(0, 0, 0, .12);--mat-sys-level5: 0px 7px 8px -4px rgba(0, 0, 0, .2), 0px 12px 17px 2px rgba(0, 0, 0, .14), 0px 5px 22px 4px rgba(0, 0, 0, .12);--mat-sys-body-large: 400 1rem / 1.5rem Roboto;--mat-sys-body-large-font: Roboto;--mat-sys-body-large-line-height: 1.5rem;--mat-sys-body-large-size: 1rem;--mat-sys-body-large-tracking: .031rem;--mat-sys-body-large-weight: 400;--mat-sys-body-medium: 400 .875rem / 1.25rem Roboto;--mat-sys-body-medium-font: Roboto;--mat-sys-body-medium-line-height: 1.25rem;--mat-sys-body-medium-size: .875rem;--mat-sys-body-medium-tracking: .016rem;--mat-sys-body-medium-weight: 400;--mat-sys-body-small: 400 .75rem / 1rem Roboto;--mat-sys-body-small-font: Roboto;--mat-sys-body-small-line-height: 1rem;--mat-sys-body-small-size: .75rem;--mat-sys-body-small-tracking: .025rem;--mat-sys-body-small-weight: 400;--mat-sys-display-large: 400 3.562rem / 4rem Roboto;--mat-sys-display-large-font: Roboto;--mat-sys-display-large-line-height: 4rem;--mat-sys-display-large-size: 3.562rem;--mat-sys-display-large-tracking: -.016rem;--mat-sys-display-large-weight: 400;--mat-sys-display-medium: 400 2.812rem / 3.25rem Roboto;--mat-sys-display-medium-font: Roboto;--mat-sys-display-medium-line-height: 3.25rem;--mat-sys-display-medium-size: 2.812rem;--mat-sys-display-medium-tracking: 0;--mat-sys-display-medium-weight: 400;--mat-sys-display-small: 400 2.25rem / 2.75rem Roboto;--mat-sys-display-small-font: Roboto;--mat-sys-display-small-line-height: 2.75rem;--mat-sys-display-small-size: 2.25rem;--mat-sys-display-small-tracking: 0;--mat-sys-display-small-weight: 400;--mat-sys-headline-large: 400 2rem / 2.5rem Roboto;--mat-sys-headline-large-font: Roboto;--mat-sys-headline-large-line-height: 2.5rem;--mat-sys-headline-large-size: 2rem;--mat-sys-headline-large-tracking: 0;--mat-sys-headline-large-weight: 400;--mat-sys-headline-medium: 400 1.75rem / 2.25rem Roboto;--mat-sys-headline-medium-font: Roboto;--mat-sys-headline-medium-line-height: 2.25rem;--mat-sys-headline-medium-size: 1.75rem;--mat-sys-headline-medium-tracking: 0;--mat-sys-headline-medium-weight: 400;--mat-sys-headline-small: 400 1.5rem / 2rem Roboto;--mat-sys-headline-small-font: Roboto;--mat-sys-headline-small-line-height: 2rem;--mat-sys-headline-small-size: 1.5rem;--mat-sys-headline-small-tracking: 0;--mat-sys-headline-small-weight: 400;--mat-sys-label-large: 500 .875rem / 1.25rem Roboto;--mat-sys-label-large-font: Roboto;--mat-sys-label-large-line-height: 1.25rem;--mat-sys-label-large-size: .875rem;--mat-sys-label-large-tracking: .006rem;--mat-sys-label-large-weight: 500;--mat-sys-label-large-weight-prominent: 700;--mat-sys-label-medium: 500 .75rem / 1rem Roboto;--mat-sys-label-medium-font: Roboto;--mat-sys-label-medium-line-height: 1rem;--mat-sys-label-medium-size: .75rem;--mat-sys-label-medium-tracking: .031rem;--mat-sys-label-medium-weight: 500;--mat-sys-label-medium-weight-prominent: 700;--mat-sys-label-small: 500 .688rem / 1rem Roboto;--mat-sys-label-small-font: Roboto;--mat-sys-label-small-line-height: 1rem;--mat-sys-label-small-size: .688rem;--mat-sys-label-small-tracking: .031rem;--mat-sys-label-small-weight: 500;--mat-sys-title-large: 400 1.375rem / 1.75rem Roboto;--mat-sys-title-large-font: Roboto;--mat-sys-title-large-line-height: 1.75rem;--mat-sys-title-large-size: 1.375rem;--mat-sys-title-large-tracking: 0;--mat-sys-title-large-weight: 400;--mat-sys-title-medium: 500 1rem / 1.5rem Roboto;--mat-sys-title-medium-font: Roboto;--mat-sys-title-medium-line-height: 1.5rem;--mat-sys-title-medium-size: 1rem;--mat-sys-title-medium-tracking: .009rem;--mat-sys-title-medium-weight: 500;--mat-sys-title-small: 500 .875rem / 1.25rem Roboto;--mat-sys-title-small-font: Roboto;--mat-sys-title-small-line-height: 1.25rem;--mat-sys-title-small-size: .875rem;--mat-sys-title-small-tracking: .006rem;--mat-sys-title-small-weight: 500;--mat-sys-corner-extra-large: 28px;--mat-sys-corner-extra-large-top: 28px 28px 0 0;--mat-sys-corner-extra-small: 4px;--mat-sys-corner-extra-small-top: 4px 4px 0 0;--mat-sys-corner-full: 9999px;--mat-sys-corner-large: 16px;--mat-sys-corner-large-end: 0 16px 16px 0;--mat-sys-corner-large-start: 16px 0 0 16px;--mat-sys-corner-large-top: 16px 16px 0 0;--mat-sys-corner-medium: 12px;--mat-sys-corner-none: 0;--mat-sys-corner-small: 8px;--mat-sys-dragged-state-layer-opacity: .16;--mat-sys-focus-state-layer-opacity: .12;--mat-sys-hover-state-layer-opacity: .08;--mat-sys-pressed-state-layer-opacity: .12}.theme-dark{color-scheme:dark}.text-red-0{color:#000!important}.text-red-10{color:#410000!important}.text-red-20{color:#690100!important}.text-red-25{color:#7e0100!important}.text-red-30{color:#930100!important}.text-red-35{color:#a90100!important}.text-red-40{color:#c00100!important}.text-red-50{color:#ef0000!important}.text-red-60{color:#ff5540!important}.text-red-70{color:#ff8a78!important}.text-red-80{color:#ffb4a8!important}.text-red-90{color:#ffdad4!important}.text-red-95{color:#ffedea!important}.text-red-98{color:#fff8f6!important}.text-red-99{color:#fffbff!important}.text-red-100{color:#fff!important}.text-green-0{color:#000!important}.text-green-10{color:#020!important}.text-green-20{color:#013a00!important}.text-green-25{color:#014600!important}.text-green-30{color:#015300!important}.text-green-35{color:#026100!important}.text-green-40{color:#026e00!important}.text-green-50{color:#038b00!important}.text-green-60{color:#03a800!important}.text-green-70{color:#03c700!important}.text-green-80{color:#02e600!important}.text-green-90{color:#77ff61!important}.text-green-95{color:#cbffb8!important}.text-green-98{color:#edffe1!important}.text-green-99{color:#f7ffee!important}.text-green-100{color:#fff!important}.text-blue-0{color:#000!important}.text-blue-10{color:#00006e!important}.text-blue-20{color:#0001ac!important}.text-blue-25{color:#0001cd!important}.text-blue-30{color:#0000ef!important}.text-blue-35{color:#1a21ff!important}.text-blue-40{color:#343dff!important}.text-blue-50{color:#5a64ff!important}.text-blue-60{color:#7c84ff!important}.text-blue-70{color:#9da3ff!important}.text-blue-80{color:#bec2ff!important}.text-blue-90{color:#e0e0ff!important}.text-blue-95{color:#f1efff!important}.text-blue-98{color:#fbf8ff!important}.text-blue-99{color:#fffbff!important}.text-blue-100{color:#fff!important}.text-yellow-0{color:#000!important}.text-yellow-10{color:#1d1d00!important}.text-yellow-20{color:#323200!important}.text-yellow-25{color:#3e3e00!important}.text-yellow-30{color:#494900!important}.text-yellow-35{color:#550!important}.text-yellow-40{color:#626200!important}.text-yellow-50{color:#7b7b00!important}.text-yellow-60{color:#969600!important}.text-yellow-70{color:#b1b100!important}.text-yellow-80{color:#cdcd00!important}.text-yellow-90{color:#eaea00!important}.text-yellow-95{color:#f9f900!important}.text-yellow-98{color:#fffeac!important}.text-yellow-99{color:#fffbff!important}.text-yellow-100{color:#fff!important}.text-cyan-0{color:#000!important}.text-cyan-10{color:#002020!important}.text-cyan-20{color:#003737!important}.text-cyan-25{color:#004343!important}.text-cyan-30{color:#004f4f!important}.text-cyan-35{color:#005c5c!important}.text-cyan-40{color:#006a6a!important}.text-cyan-50{color:#008585!important}.text-cyan-60{color:#00a1a1!important}.text-cyan-70{color:#00bebe!important}.text-cyan-80{color:#0dd!important}.text-cyan-90{color:#00fbfb!important}.text-cyan-95{color:#adfffe!important}.text-cyan-98{color:#e2fffe!important}.text-cyan-99{color:#f1fffe!important}.text-cyan-100{color:#fff!important}.text-magenta-0{color:#000!important}.text-magenta-10{color:#380038!important}.text-magenta-20{color:#5b005b!important}.text-magenta-25{color:#6e006e!important}.text-magenta-30{color:#810081!important}.text-magenta-35{color:#950094!important}.text-magenta-40{color:#a900a9!important}.text-magenta-50{color:#d200d2!important}.text-magenta-60{color:#fe00fe!important}.text-magenta-70{color:#ff76f6!important}.text-magenta-80{color:#ffabf3!important}.text-magenta-90{color:#ffd7f5!important}.text-magenta-95{color:#ffebf8!important}.text-magenta-98{color:#fff7f9!important}.text-magenta-99{color:#fffbff!important}.text-magenta-100{color:#fff!important}.text-orange-0{color:#000!important}.text-orange-10{color:#311300!important}.text-orange-20{color:#502400!important}.text-orange-25{color:#612d00!important}.text-orange-30{color:#723600!important}.text-orange-35{color:#843f00!important}.text-orange-40{color:#964900!important}.text-orange-50{color:#bc5d00!important}.text-orange-60{color:#e37100!important}.text-orange-70{color:#ff8e36!important}.text-orange-80{color:#ffb787!important}.text-orange-90{color:#ffdcc7!important}.text-orange-95{color:#ffede4!important}.text-orange-98{color:#fff8f5!important}.text-orange-99{color:#fffbff!important}.text-orange-100{color:#fff!important}.text-chartreuse-0{color:#000!important}.text-chartreuse-10{color:#0b2000!important}.text-chartreuse-20{color:#173800!important}.text-chartreuse-25{color:#1e4400!important}.text-chartreuse-30{color:#245100!important}.text-chartreuse-35{color:#2b5e00!important}.text-chartreuse-40{color:#326b00!important}.text-chartreuse-50{color:#418700!important}.text-chartreuse-60{color:#50a400!important}.text-chartreuse-70{color:#60c100!important}.text-chartreuse-80{color:#70e000!important}.text-chartreuse-90{color:#82ff10!important}.text-chartreuse-95{color:#cfffa9!important}.text-chartreuse-98{color:#eeffdc!important}.text-chartreuse-99{color:#f8ffeb!important}.text-chartreuse-100{color:#fff!important}.text-spring-green-0{color:#000!important}.text-spring-green-10{color:#00210b!important}.text-spring-green-20{color:#003917!important}.text-spring-green-25{color:#00461e!important}.text-spring-green-30{color:#005225!important}.text-spring-green-35{color:#00602c!important}.text-spring-green-40{color:#006d33!important}.text-spring-green-50{color:#008942!important}.text-spring-green-60{color:#00a751!important}.text-spring-green-70{color:#00c561!important}.text-spring-green-80{color:#00e472!important}.text-spring-green-90{color:#63ff94!important}.text-spring-green-95{color:#c4ffcb!important}.text-spring-green-98{color:#eaffe9!important}.text-spring-green-99{color:#f5fff2!important}.text-spring-green-100{color:#fff!important}.text-azure-0{color:#000!important}.text-azure-10{color:#001b3f!important}.text-azure-20{color:#002f65!important}.text-azure-25{color:#003a7a!important}.text-azure-30{color:#00458f!important}.text-azure-35{color:#0050a5!important}.text-azure-40{color:#005cbb!important}.text-azure-50{color:#0074e9!important}.text-azure-60{color:#438fff!important}.text-azure-70{color:#7cabff!important}.text-azure-80{color:#abc7ff!important}.text-azure-90{color:#d7e3ff!important}.text-azure-95{color:#ecf0ff!important}.text-azure-98{color:#f9f9ff!important}.text-azure-99{color:#fdfbff!important}.text-azure-100{color:#fff!important}.text-violet-0{color:#000!important}.text-violet-10{color:#270057!important}.text-violet-20{color:#42008a!important}.text-violet-25{color:#5000a4!important}.text-violet-30{color:#5f00c0!important}.text-violet-35{color:#6e00dc!important}.text-violet-40{color:#7d00fa!important}.text-violet-50{color:#944aff!important}.text-violet-60{color:#a974ff!important}.text-violet-70{color:#bf98ff!important}.text-violet-80{color:#d5baff!important}.text-violet-90{color:#ecdcff!important}.text-violet-95{color:#f7edff!important}.text-violet-98{color:#fef7ff!important}.text-violet-99{color:#fffbff!important}.text-violet-100{color:#fff!important}.text-rose-0{color:#000!important}.text-rose-10{color:#3f001b!important}.text-rose-20{color:#65002f!important}.text-rose-25{color:#7a003a!important}.text-rose-30{color:#8f0045!important}.text-rose-35{color:#a40050!important}.text-rose-40{color:#ba005c!important}.text-rose-50{color:#e80074!important}.text-rose-60{color:#ff4a8e!important}.text-rose-70{color:#ff84a9!important}.text-rose-80{color:#ffb1c5!important}.text-rose-90{color:#ffd9e1!important}.text-rose-95{color:#ffecef!important}.text-rose-98{color:#fff8f8!important}.text-rose-99{color:#fffbff!important}.text-rose-100,.text-white{color:#fff!important}.text-black{color:#000!important}.text-light{color:#ffffffde!important}.text-dark{color:#000000de!important}.bg-red-0{background-color:#000!important}.bg-red-10{background-color:#410000!important}.bg-red-20{background-color:#690100!important}.bg-red-25{background-color:#7e0100!important}.bg-red-30{background-color:#930100!important}.bg-red-35{background-color:#a90100!important}.bg-red-40{background-color:#c00100!important}.bg-red-50{background-color:#ef0000!important}.bg-red-60{background-color:#ff5540!important}.bg-red-70{background-color:#ff8a78!important}.bg-red-80{background-color:#ffb4a8!important}.bg-red-90{background-color:#ffdad4!important}.bg-red-95{background-color:#ffedea!important}.bg-red-98{background-color:#fff8f6!important}.bg-red-99{background-color:#fffbff!important}.bg-red-100{background-color:#fff!important}.bg-green-0{background-color:#000!important}.bg-green-10{background-color:#020!important}.bg-green-20{background-color:#013a00!important}.bg-green-25{background-color:#014600!important}.bg-green-30{background-color:#015300!important}.bg-green-35{background-color:#026100!important}.bg-green-40{background-color:#026e00!important}.bg-green-50{background-color:#038b00!important}.bg-green-60{background-color:#03a800!important}.bg-green-70{background-color:#03c700!important}.bg-green-80{background-color:#02e600!important}.bg-green-90{background-color:#77ff61!important}.bg-green-95{background-color:#cbffb8!important}.bg-green-98{background-color:#edffe1!important}.bg-green-99{background-color:#f7ffee!important}.bg-green-100{background-color:#fff!important}.bg-blue-0{background-color:#000!important}.bg-blue-10{background-color:#00006e!important}.bg-blue-20{background-color:#0001ac!important}.bg-blue-25{background-color:#0001cd!important}.bg-blue-30{background-color:#0000ef!important}.bg-blue-35{background-color:#1a21ff!important}.bg-blue-40{background-color:#343dff!important}.bg-blue-50{background-color:#5a64ff!important}.bg-blue-60{background-color:#7c84ff!important}.bg-blue-70{background-color:#9da3ff!important}.bg-blue-80{background-color:#bec2ff!important}.bg-blue-90{background-color:#e0e0ff!important}.bg-blue-95{background-color:#f1efff!important}.bg-blue-98{background-color:#fbf8ff!important}.bg-blue-99{background-color:#fffbff!important}.bg-blue-100{background-color:#fff!important}.bg-yellow-0{background-color:#000!important}.bg-yellow-10{background-color:#1d1d00!important}.bg-yellow-20{background-color:#323200!important}.bg-yellow-25{background-color:#3e3e00!important}.bg-yellow-30{background-color:#494900!important}.bg-yellow-35{background-color:#550!important}.bg-yellow-40{background-color:#626200!important}.bg-yellow-50{background-color:#7b7b00!important}.bg-yellow-60{background-color:#969600!important}.bg-yellow-70{background-color:#b1b100!important}.bg-yellow-80{background-color:#cdcd00!important}.bg-yellow-90{background-color:#eaea00!important}.bg-yellow-95{background-color:#f9f900!important}.bg-yellow-98{background-color:#fffeac!important}.bg-yellow-99{background-color:#fffbff!important}.bg-yellow-100{background-color:#fff!important}.bg-cyan-0{background-color:#000!important}.bg-cyan-10{background-color:#002020!important}.bg-cyan-20{background-color:#003737!important}.bg-cyan-25{background-color:#004343!important}.bg-cyan-30{background-color:#004f4f!important}.bg-cyan-35{background-color:#005c5c!important}.bg-cyan-40{background-color:#006a6a!important}.bg-cyan-50{background-color:#008585!important}.bg-cyan-60{background-color:#00a1a1!important}.bg-cyan-70{background-color:#00bebe!important}.bg-cyan-80{background-color:#0dd!important}.bg-cyan-90{background-color:#00fbfb!important}.bg-cyan-95{background-color:#adfffe!important}.bg-cyan-98{background-color:#e2fffe!important}.bg-cyan-99{background-color:#f1fffe!important}.bg-cyan-100{background-color:#fff!important}.bg-magenta-0{background-color:#000!important}.bg-magenta-10{background-color:#380038!important}.bg-magenta-20{background-color:#5b005b!important}.bg-magenta-25{background-color:#6e006e!important}.bg-magenta-30{background-color:#810081!important}.bg-magenta-35{background-color:#950094!important}.bg-magenta-40{background-color:#a900a9!important}.bg-magenta-50{background-color:#d200d2!important}.bg-magenta-60{background-color:#fe00fe!important}.bg-magenta-70{background-color:#ff76f6!important}.bg-magenta-80{background-color:#ffabf3!important}.bg-magenta-90{background-color:#ffd7f5!important}.bg-magenta-95{background-color:#ffebf8!important}.bg-magenta-98{background-color:#fff7f9!important}.bg-magenta-99{background-color:#fffbff!important}.bg-magenta-100{background-color:#fff!important}.bg-orange-0{background-color:#000!important}.bg-orange-10{background-color:#311300!important}.bg-orange-20{background-color:#502400!important}.bg-orange-25{background-color:#612d00!important}.bg-orange-30{background-color:#723600!important}.bg-orange-35{background-color:#843f00!important}.bg-orange-40{background-color:#964900!important}.bg-orange-50{background-color:#bc5d00!important}.bg-orange-60{background-color:#e37100!important}.bg-orange-70{background-color:#ff8e36!important}.bg-orange-80{background-color:#ffb787!important}.bg-orange-90{background-color:#ffdcc7!important}.bg-orange-95{background-color:#ffede4!important}.bg-orange-98{background-color:#fff8f5!important}.bg-orange-99{background-color:#fffbff!important}.bg-orange-100{background-color:#fff!important}.bg-chartreuse-0{background-color:#000!important}.bg-chartreuse-10{background-color:#0b2000!important}.bg-chartreuse-20{background-color:#173800!important}.bg-chartreuse-25{background-color:#1e4400!important}.bg-chartreuse-30{background-color:#245100!important}.bg-chartreuse-35{background-color:#2b5e00!important}.bg-chartreuse-40{background-color:#326b00!important}.bg-chartreuse-50{background-color:#418700!important}.bg-chartreuse-60{background-color:#50a400!important}.bg-chartreuse-70{background-color:#60c100!important}.bg-chartreuse-80{background-color:#70e000!important}.bg-chartreuse-90{background-color:#82ff10!important}.bg-chartreuse-95{background-color:#cfffa9!important}.bg-chartreuse-98{background-color:#eeffdc!important}.bg-chartreuse-99{background-color:#f8ffeb!important}.bg-chartreuse-100{background-color:#fff!important}.bg-spring-green-0{background-color:#000!important}.bg-spring-green-10{background-color:#00210b!important}.bg-spring-green-20{background-color:#003917!important}.bg-spring-green-25{background-color:#00461e!important}.bg-spring-green-30{background-color:#005225!important}.bg-spring-green-35{background-color:#00602c!important}.bg-spring-green-40{background-color:#006d33!important}.bg-spring-green-50{background-color:#008942!important}.bg-spring-green-60{background-color:#00a751!important}.bg-spring-green-70{background-color:#00c561!important}.bg-spring-green-80{background-color:#00e472!important}.bg-spring-green-90{background-color:#63ff94!important}.bg-spring-green-95{background-color:#c4ffcb!important}.bg-spring-green-98{background-color:#eaffe9!important}.bg-spring-green-99{background-color:#f5fff2!important}.bg-spring-green-100{background-color:#fff!important}.bg-azure-0{background-color:#000!important}.bg-azure-10{background-color:#001b3f!important}.bg-azure-20{background-color:#002f65!important}.bg-azure-25{background-color:#003a7a!important}.bg-azure-30{background-color:#00458f!important}.bg-azure-35{background-color:#0050a5!important}.bg-azure-40{background-color:#005cbb!important}.bg-azure-50{background-color:#0074e9!important}.bg-azure-60{background-color:#438fff!important}.bg-azure-70{background-color:#7cabff!important}.bg-azure-80{background-color:#abc7ff!important}.bg-azure-90{background-color:#d7e3ff!important}.bg-azure-95{background-color:#ecf0ff!important}.bg-azure-98{background-color:#f9f9ff!important}.bg-azure-99{background-color:#fdfbff!important}.bg-azure-100{background-color:#fff!important}.bg-violet-0{background-color:#000!important}.bg-violet-10{background-color:#270057!important}.bg-violet-20{background-color:#42008a!important}.bg-violet-25{background-color:#5000a4!important}.bg-violet-30{background-color:#5f00c0!important}.bg-violet-35{background-color:#6e00dc!important}.bg-violet-40{background-color:#7d00fa!important}.bg-violet-50{background-color:#944aff!important}.bg-violet-60{background-color:#a974ff!important}.bg-violet-70{background-color:#bf98ff!important}.bg-violet-80{background-color:#d5baff!important}.bg-violet-90{background-color:#ecdcff!important}.bg-violet-95{background-color:#f7edff!important}.bg-violet-98{background-color:#fef7ff!important}.bg-violet-99{background-color:#fffbff!important}.bg-violet-100{background-color:#fff!important}.bg-rose-0{background-color:#000!important}.bg-rose-10{background-color:#3f001b!important}.bg-rose-20{background-color:#65002f!important}.bg-rose-25{background-color:#7a003a!important}.bg-rose-30{background-color:#8f0045!important}.bg-rose-35{background-color:#a40050!important}.bg-rose-40{background-color:#ba005c!important}.bg-rose-50{background-color:#e80074!important}.bg-rose-60{background-color:#ff4a8e!important}.bg-rose-70{background-color:#ff84a9!important}.bg-rose-80{background-color:#ffb1c5!important}.bg-rose-90{background-color:#ffd9e1!important}.bg-rose-95{background-color:#ffecef!important}.bg-rose-98{background-color:#fff8f8!important}.bg-rose-99{background-color:#fffbff!important}.bg-rose-100,.bg-white{background-color:#fff!important}.bg-black{background-color:#000!important}.bg-light{background-color:#ffffffde!important}.bg-dark{background-color:#000000de!important}.border-red-0{border-color:#000!important}.border-red-10{border-color:#410000!important}.border-red-20{border-color:#690100!important}.border-red-25{border-color:#7e0100!important}.border-red-30{border-color:#930100!important}.border-red-35{border-color:#a90100!important}.border-red-40{border-color:#c00100!important}.border-red-50{border-color:#ef0000!important}.border-red-60{border-color:#ff5540!important}.border-red-70{border-color:#ff8a78!important}.border-red-80{border-color:#ffb4a8!important}.border-red-90{border-color:#ffdad4!important}.border-red-95{border-color:#ffedea!important}.border-red-98{border-color:#fff8f6!important}.border-red-99{border-color:#fffbff!important}.border-red-100{border-color:#fff!important}.border-green-0{border-color:#000!important}.border-green-10{border-color:#020!important}.border-green-20{border-color:#013a00!important}.border-green-25{border-color:#014600!important}.border-green-30{border-color:#015300!important}.border-green-35{border-color:#026100!important}.border-green-40{border-color:#026e00!important}.border-green-50{border-color:#038b00!important}.border-green-60{border-color:#03a800!important}.border-green-70{border-color:#03c700!important}.border-green-80{border-color:#02e600!important}.border-green-90{border-color:#77ff61!important}.border-green-95{border-color:#cbffb8!important}.border-green-98{border-color:#edffe1!important}.border-green-99{border-color:#f7ffee!important}.border-green-100{border-color:#fff!important}.border-blue-0{border-color:#000!important}.border-blue-10{border-color:#00006e!important}.border-blue-20{border-color:#0001ac!important}.border-blue-25{border-color:#0001cd!important}.border-blue-30{border-color:#0000ef!important}.border-blue-35{border-color:#1a21ff!important}.border-blue-40{border-color:#343dff!important}.border-blue-50{border-color:#5a64ff!important}.border-blue-60{border-color:#7c84ff!important}.border-blue-70{border-color:#9da3ff!important}.border-blue-80{border-color:#bec2ff!important}.border-blue-90{border-color:#e0e0ff!important}.border-blue-95{border-color:#f1efff!important}.border-blue-98{border-color:#fbf8ff!important}.border-blue-99{border-color:#fffbff!important}.border-blue-100{border-color:#fff!important}.border-yellow-0{border-color:#000!important}.border-yellow-10{border-color:#1d1d00!important}.border-yellow-20{border-color:#323200!important}.border-yellow-25{border-color:#3e3e00!important}.border-yellow-30{border-color:#494900!important}.border-yellow-35{border-color:#550!important}.border-yellow-40{border-color:#626200!important}.border-yellow-50{border-color:#7b7b00!important}.border-yellow-60{border-color:#969600!important}.border-yellow-70{border-color:#b1b100!important}.border-yellow-80{border-color:#cdcd00!important}.border-yellow-90{border-color:#eaea00!important}.border-yellow-95{border-color:#f9f900!important}.border-yellow-98{border-color:#fffeac!important}.border-yellow-99{border-color:#fffbff!important}.border-yellow-100{border-color:#fff!important}.border-cyan-0{border-color:#000!important}.border-cyan-10{border-color:#002020!important}.border-cyan-20{border-color:#003737!important}.border-cyan-25{border-color:#004343!important}.border-cyan-30{border-color:#004f4f!important}.border-cyan-35{border-color:#005c5c!important}.border-cyan-40{border-color:#006a6a!important}.border-cyan-50{border-color:#008585!important}.border-cyan-60{border-color:#00a1a1!important}.border-cyan-70{border-color:#00bebe!important}.border-cyan-80{border-color:#0dd!important}.border-cyan-90{border-color:#00fbfb!important}.border-cyan-95{border-color:#adfffe!important}.border-cyan-98{border-color:#e2fffe!important}.border-cyan-99{border-color:#f1fffe!important}.border-cyan-100{border-color:#fff!important}.border-magenta-0{border-color:#000!important}.border-magenta-10{border-color:#380038!important}.border-magenta-20{border-color:#5b005b!important}.border-magenta-25{border-color:#6e006e!important}.border-magenta-30{border-color:#810081!important}.border-magenta-35{border-color:#950094!important}.border-magenta-40{border-color:#a900a9!important}.border-magenta-50{border-color:#d200d2!important}.border-magenta-60{border-color:#fe00fe!important}.border-magenta-70{border-color:#ff76f6!important}.border-magenta-80{border-color:#ffabf3!important}.border-magenta-90{border-color:#ffd7f5!important}.border-magenta-95{border-color:#ffebf8!important}.border-magenta-98{border-color:#fff7f9!important}.border-magenta-99{border-color:#fffbff!important}.border-magenta-100{border-color:#fff!important}.border-orange-0{border-color:#000!important}.border-orange-10{border-color:#311300!important}.border-orange-20{border-color:#502400!important}.border-orange-25{border-color:#612d00!important}.border-orange-30{border-color:#723600!important}.border-orange-35{border-color:#843f00!important}.border-orange-40{border-color:#964900!important}.border-orange-50{border-color:#bc5d00!important}.border-orange-60{border-color:#e37100!important}.border-orange-70{border-color:#ff8e36!important}.border-orange-80{border-color:#ffb787!important}.border-orange-90{border-color:#ffdcc7!important}.border-orange-95{border-color:#ffede4!important}.border-orange-98{border-color:#fff8f5!important}.border-orange-99{border-color:#fffbff!important}.border-orange-100{border-color:#fff!important}.border-chartreuse-0{border-color:#000!important}.border-chartreuse-10{border-color:#0b2000!important}.border-chartreuse-20{border-color:#173800!important}.border-chartreuse-25{border-color:#1e4400!important}.border-chartreuse-30{border-color:#245100!important}.border-chartreuse-35{border-color:#2b5e00!important}.border-chartreuse-40{border-color:#326b00!important}.border-chartreuse-50{border-color:#418700!important}.border-chartreuse-60{border-color:#50a400!important}.border-chartreuse-70{border-color:#60c100!important}.border-chartreuse-80{border-color:#70e000!important}.border-chartreuse-90{border-color:#82ff10!important}.border-chartreuse-95{border-color:#cfffa9!important}.border-chartreuse-98{border-color:#eeffdc!important}.border-chartreuse-99{border-color:#f8ffeb!important}.border-chartreuse-100{border-color:#fff!important}.border-spring-green-0{border-color:#000!important}.border-spring-green-10{border-color:#00210b!important}.border-spring-green-20{border-color:#003917!important}.border-spring-green-25{border-color:#00461e!important}.border-spring-green-30{border-color:#005225!important}.border-spring-green-35{border-color:#00602c!important}.border-spring-green-40{border-color:#006d33!important}.border-spring-green-50{border-color:#008942!important}.border-spring-green-60{border-color:#00a751!important}.border-spring-green-70{border-color:#00c561!important}.border-spring-green-80{border-color:#00e472!important}.border-spring-green-90{border-color:#63ff94!important}.border-spring-green-95{border-color:#c4ffcb!important}.border-spring-green-98{border-color:#eaffe9!important}.border-spring-green-99{border-color:#f5fff2!important}.border-spring-green-100{border-color:#fff!important}.border-azure-0{border-color:#000!important}.border-azure-10{border-color:#001b3f!important}.border-azure-20{border-color:#002f65!important}.border-azure-25{border-color:#003a7a!important}.border-azure-30{border-color:#00458f!important}.border-azure-35{border-color:#0050a5!important}.border-azure-40{border-color:#005cbb!important}.border-azure-50{border-color:#0074e9!important}.border-azure-60{border-color:#438fff!important}.border-azure-70{border-color:#7cabff!important}.border-azure-80{border-color:#abc7ff!important}.border-azure-90{border-color:#d7e3ff!important}.border-azure-95{border-color:#ecf0ff!important}.border-azure-98{border-color:#f9f9ff!important}.border-azure-99{border-color:#fdfbff!important}.border-azure-100{border-color:#fff!important}.border-violet-0{border-color:#000!important}.border-violet-10{border-color:#270057!important}.border-violet-20{border-color:#42008a!important}.border-violet-25{border-color:#5000a4!important}.border-violet-30{border-color:#5f00c0!important}.border-violet-35{border-color:#6e00dc!important}.border-violet-40{border-color:#7d00fa!important}.border-violet-50{border-color:#944aff!important}.border-violet-60{border-color:#a974ff!important}.border-violet-70{border-color:#bf98ff!important}.border-violet-80{border-color:#d5baff!important}.border-violet-90{border-color:#ecdcff!important}.border-violet-95{border-color:#f7edff!important}.border-violet-98{border-color:#fef7ff!important}.border-violet-99{border-color:#fffbff!important}.border-violet-100{border-color:#fff!important}.border-rose-0{border-color:#000!important}.border-rose-10{border-color:#3f001b!important}.border-rose-20{border-color:#65002f!important}.border-rose-25{border-color:#7a003a!important}.border-rose-30{border-color:#8f0045!important}.border-rose-35{border-color:#a40050!important}.border-rose-40{border-color:#ba005c!important}.border-rose-50{border-color:#e80074!important}.border-rose-60{border-color:#ff4a8e!important}.border-rose-70{border-color:#ff84a9!important}.border-rose-80{border-color:#ffb1c5!important}.border-rose-90{border-color:#ffd9e1!important}.border-rose-95{border-color:#ffecef!important}.border-rose-98{border-color:#fff8f8!important}.border-rose-99{border-color:#fffbff!important}.border-rose-100,.border-white{border-color:#fff!important}.border-black{border-color:#000!important}.border-light{border-color:#ffffffde!important}.border-dark{border-color:#000000de!important}.row{display:flex;flex-wrap:wrap;margin-right:calc(var(--gutter) * -.5);margin-left:calc(var(--gutter) * -.5)}.no-gutters{--gutter: 0}.col,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col-sm,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-md,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-lg,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-xl,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12{position:relative;width:100%;padding-right:calc(var(--gutter) * .5);padding-left:calc(var(--gutter) * .5)}.col{flex-grow:1;flex-basis:0;max-width:100%}.col-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.offset-1{margin-left:8.3333333333%}.col-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.offset-2{margin-left:16.6666666667%}.col-3{flex:0 0 25%;max-width:25%}.offset-3{margin-left:25%}.col-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.offset-4{margin-left:33.3333333333%}.col-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.offset-5{margin-left:41.6666666667%}.col-6{flex:0 0 50%;max-width:50%}.offset-6{margin-left:50%}.col-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.offset-7{margin-left:58.3333333333%}.col-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.offset-8{margin-left:66.6666666667%}.col-9{flex:0 0 75%;max-width:75%}.offset-9{margin-left:75%}.col-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.offset-10{margin-left:83.3333333333%}.col-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.offset-11{margin-left:91.6666666667%}.col-12{flex:0 0 100%;max-width:100%}.offset-12{margin-left:100%}@media (min-width: 600px){.col-sm{flex-grow:1;flex-basis:0;max-width:100%}.col-sm-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.offset-sm-1{margin-left:8.3333333333%}.col-sm-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.offset-sm-2{margin-left:16.6666666667%}.col-sm-3{flex:0 0 25%;max-width:25%}.offset-sm-3{margin-left:25%}.col-sm-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.offset-sm-4{margin-left:33.3333333333%}.col-sm-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.offset-sm-5{margin-left:41.6666666667%}.col-sm-6{flex:0 0 50%;max-width:50%}.offset-sm-6{margin-left:50%}.col-sm-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.offset-sm-7{margin-left:58.3333333333%}.col-sm-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.offset-sm-8{margin-left:66.6666666667%}.col-sm-9{flex:0 0 75%;max-width:75%}.offset-sm-9{margin-left:75%}.col-sm-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.offset-sm-10{margin-left:83.3333333333%}.col-sm-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.offset-sm-11{margin-left:91.6666666667%}.col-sm-12{flex:0 0 100%;max-width:100%}.offset-sm-12{margin-left:100%}}@media (min-width: 960px){.col-md{flex-grow:1;flex-basis:0;max-width:100%}.col-md-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.offset-md-1{margin-left:8.3333333333%}.col-md-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.offset-md-2{margin-left:16.6666666667%}.col-md-3{flex:0 0 25%;max-width:25%}.offset-md-3{margin-left:25%}.col-md-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.offset-md-4{margin-left:33.3333333333%}.col-md-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.offset-md-5{margin-left:41.6666666667%}.col-md-6{flex:0 0 50%;max-width:50%}.offset-md-6{margin-left:50%}.col-md-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.offset-md-7{margin-left:58.3333333333%}.col-md-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.offset-md-8{margin-left:66.6666666667%}.col-md-9{flex:0 0 75%;max-width:75%}.offset-md-9{margin-left:75%}.col-md-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.offset-md-10{margin-left:83.3333333333%}.col-md-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.offset-md-11{margin-left:91.6666666667%}.col-md-12{flex:0 0 100%;max-width:100%}.offset-md-12{margin-left:100%}}@media (min-width: 1280px){.col-lg{flex-grow:1;flex-basis:0;max-width:100%}.col-lg-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.offset-lg-1{margin-left:8.3333333333%}.col-lg-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.offset-lg-2{margin-left:16.6666666667%}.col-lg-3{flex:0 0 25%;max-width:25%}.offset-lg-3{margin-left:25%}.col-lg-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.offset-lg-4{margin-left:33.3333333333%}.col-lg-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.offset-lg-5{margin-left:41.6666666667%}.col-lg-6{flex:0 0 50%;max-width:50%}.offset-lg-6{margin-left:50%}.col-lg-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.offset-lg-7{margin-left:58.3333333333%}.col-lg-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.offset-lg-8{margin-left:66.6666666667%}.col-lg-9{flex:0 0 75%;max-width:75%}.offset-lg-9{margin-left:75%}.col-lg-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.offset-lg-10{margin-left:83.3333333333%}.col-lg-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.offset-lg-11{margin-left:91.6666666667%}.col-lg-12{flex:0 0 100%;max-width:100%}.offset-lg-12{margin-left:100%}}@media (min-width: 1920px){.col-xl{flex-grow:1;flex-basis:0;max-width:100%}.col-xl-1{flex:0 0 8.3333333333%;max-width:8.3333333333%}.offset-xl-1{margin-left:8.3333333333%}.col-xl-2{flex:0 0 16.6666666667%;max-width:16.6666666667%}.offset-xl-2{margin-left:16.6666666667%}.col-xl-3{flex:0 0 25%;max-width:25%}.offset-xl-3{margin-left:25%}.col-xl-4{flex:0 0 33.3333333333%;max-width:33.3333333333%}.offset-xl-4{margin-left:33.3333333333%}.col-xl-5{flex:0 0 41.6666666667%;max-width:41.6666666667%}.offset-xl-5{margin-left:41.6666666667%}.col-xl-6{flex:0 0 50%;max-width:50%}.offset-xl-6{margin-left:50%}.col-xl-7{flex:0 0 58.3333333333%;max-width:58.3333333333%}.offset-xl-7{margin-left:58.3333333333%}.col-xl-8{flex:0 0 66.6666666667%;max-width:66.6666666667%}.offset-xl-8{margin-left:66.6666666667%}.col-xl-9{flex:0 0 75%;max-width:75%}.offset-xl-9{margin-left:75%}.col-xl-10{flex:0 0 83.3333333333%;max-width:83.3333333333%}.offset-xl-10{margin-left:83.3333333333%}.col-xl-11{flex:0 0 91.6666666667%;max-width:91.6666666667%}.offset-xl-11{margin-left:91.6666666667%}.col-xl-12{flex:0 0 100%;max-width:100%}.offset-xl-12{margin-left:100%}}.static{position:static!important}.fixed{position:fixed!important}.absolute{position:absolute!important}.relative{position:relative!important}.sticky{position:sticky!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-col{flex-direction:column!important}.flex-col-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.w-0{width:0!important}.w-20{width:20%!important}.w-25{width:25%!important}.w-40{width:40%!important}.w-50{width:50%!important}.w-60{width:60%!important}.w-75{width:75%!important}.w-80{width:80%!important}.w-full{width:100%!important}.w-auto{width:auto!important}.h-0{height:0!important}.h-20{height:20%!important}.h-25{height:25%!important}.h-40{height:40%!important}.h-50{height:50%!important}.h-60{height:60%!important}.h-75{height:75%!important}.h-80{height:80%!important}.h-full{height:100%!important}.h-auto{height:auto!important}.m-0{margin:0!important}.m-2{margin:.125rem!important}.m-4{margin:.25rem!important}.m-8{margin:.5rem!important}.m-12{margin:.75rem!important}.m-16{margin:1rem!important}.m-24{margin:1.5rem!important}.m-32{margin:2rem!important}.m-48{margin:3rem!important}.m-auto{margin:auto!important}.m-x-0{margin-left:0!important;margin-right:0!important}.m-x-2{margin-left:.125rem!important;margin-right:.125rem!important}.m-x-4{margin-left:.25rem!important;margin-right:.25rem!important}.m-x-8{margin-left:.5rem!important;margin-right:.5rem!important}.m-x-12{margin-left:.75rem!important;margin-right:.75rem!important}.m-x-16{margin-left:1rem!important;margin-right:1rem!important}.m-x-24{margin-left:1.5rem!important;margin-right:1.5rem!important}.m-x-32{margin-left:2rem!important;margin-right:2rem!important}.m-x-48{margin-left:3rem!important;margin-right:3rem!important}.m-x-auto{margin-left:auto!important;margin-right:auto!important}.m-y-0{margin-top:0!important;margin-bottom:0!important}.m-y-2{margin-top:.125rem!important;margin-bottom:.125rem!important}.m-y-4{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-y-8{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-y-12{margin-top:.75rem!important;margin-bottom:.75rem!important}.m-y-16{margin-top:1rem!important;margin-bottom:1rem!important}.m-y-24{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-y-32{margin-top:2rem!important;margin-bottom:2rem!important}.m-y-48{margin-top:3rem!important;margin-bottom:3rem!important}.m-y-auto{margin-top:auto!important;margin-bottom:auto!important}.m-t-0{margin-top:0!important}.m-t-2{margin-top:.125rem!important}.m-t-4{margin-top:.25rem!important}.m-t-8{margin-top:.5rem!important}.m-t-12{margin-top:.75rem!important}.m-t-16{margin-top:1rem!important}.m-t-24{margin-top:1.5rem!important}.m-t-32{margin-top:2rem!important}.m-t-48{margin-top:3rem!important}.m-t-auto{margin-top:auto!important}.m-r-0{margin-right:0!important}.m-r-2{margin-right:.125rem!important}.m-r-4{margin-right:.25rem!important}.m-r-8{margin-right:.5rem!important}.m-r-12{margin-right:.75rem!important}.m-r-16{margin-right:1rem!important}.m-r-24{margin-right:1.5rem!important}.m-r-32{margin-right:2rem!important}.m-r-48{margin-right:3rem!important}.m-r-auto{margin-right:auto!important}.m-b-0{margin-bottom:0!important}.m-b-2{margin-bottom:.125rem!important}.m-b-4{margin-bottom:.25rem!important}.m-b-8{margin-bottom:.5rem!important}.m-b-12{margin-bottom:.75rem!important}.m-b-16{margin-bottom:1rem!important}.m-b-24{margin-bottom:1.5rem!important}.m-b-32{margin-bottom:2rem!important}.m-b-48{margin-bottom:3rem!important}.m-b-auto{margin-bottom:auto!important}.m-l-0{margin-left:0!important}.m-l-2{margin-left:.125rem!important}.m-l-4{margin-left:.25rem!important}.m-l-8{margin-left:.5rem!important}.m-l-12{margin-left:.75rem!important}.m-l-16{margin-left:1rem!important}.m-l-24{margin-left:1.5rem!important}.m-l-32{margin-left:2rem!important}.m-l-48{margin-left:3rem!important}.m-l-auto{margin-left:auto!important}.m--2{margin:-.125rem!important}.m--4{margin:-.25rem!important}.m--8{margin:-.5rem!important}.m--12{margin:-.75rem!important}.m--16{margin:-1rem!important}.m--24{margin:-1.5rem!important}.m--32{margin:-2rem!important}.m--48{margin:-3rem!important}.m-x--2{margin-left:-.125rem!important;margin-right:-.125rem!important}.m-x--4{margin-left:-.25rem!important;margin-right:-.25rem!important}.m-x--8{margin-left:-.5rem!important;margin-right:-.5rem!important}.m-x--12{margin-left:-.75rem!important;margin-right:-.75rem!important}.m-x--16{margin-left:-1rem!important;margin-right:-1rem!important}.m-x--24{margin-left:-1.5rem!important;margin-right:-1.5rem!important}.m-x--32{margin-left:-2rem!important;margin-right:-2rem!important}.m-x--48{margin-left:-3rem!important;margin-right:-3rem!important}.m-y--2{margin-top:-.125rem!important;margin-bottom:-.125rem!important}.m-y--4{margin-top:-.25rem!important;margin-bottom:-.25rem!important}.m-y--8{margin-top:-.5rem!important;margin-bottom:-.5rem!important}.m-y--12{margin-top:-.75rem!important;margin-bottom:-.75rem!important}.m-y--16{margin-top:-1rem!important;margin-bottom:-1rem!important}.m-y--24{margin-top:-1.5rem!important;margin-bottom:-1.5rem!important}.m-y--32{margin-top:-2rem!important;margin-bottom:-2rem!important}.m-y--48{margin-top:-3rem!important;margin-bottom:-3rem!important}.m-t--2{margin-top:-.125rem!important}.m-t--4{margin-top:-.25rem!important}.m-t--8{margin-top:-.5rem!important}.m-t--12{margin-top:-.75rem!important}.m-t--16{margin-top:-1rem!important}.m-t--24{margin-top:-1.5rem!important}.m-t--32{margin-top:-2rem!important}.m-t--48{margin-top:-3rem!important}.m-r--2{margin-right:-.125rem!important}.m-r--4{margin-right:-.25rem!important}.m-r--8{margin-right:-.5rem!important}.m-r--12{margin-right:-.75rem!important}.m-r--16{margin-right:-1rem!important}.m-r--24{margin-right:-1.5rem!important}.m-r--32{margin-right:-2rem!important}.m-r--48{margin-right:-3rem!important}.m-b--2{margin-bottom:-.125rem!important}.m-b--4{margin-bottom:-.25rem!important}.m-b--8{margin-bottom:-.5rem!important}.m-b--12{margin-bottom:-.75rem!important}.m-b--16{margin-bottom:-1rem!important}.m-b--24{margin-bottom:-1.5rem!important}.m-b--32{margin-bottom:-2rem!important}.m-b--48{margin-bottom:-3rem!important}.m-l--2{margin-left:-.125rem!important}.m-l--4{margin-left:-.25rem!important}.m-l--8{margin-left:-.5rem!important}.m-l--12{margin-left:-.75rem!important}.m-l--16{margin-left:-1rem!important}.m-l--24{margin-left:-1.5rem!important}.m-l--32{margin-left:-2rem!important}.m-l--48{margin-left:-3rem!important}.p-0{padding:0!important}.p-2{padding:.125rem!important}.p-4{padding:.25rem!important}.p-8{padding:.5rem!important}.p-12{padding:.75rem!important}.p-16{padding:1rem!important}.p-24{padding:1.5rem!important}.p-32{padding:2rem!important}.p-48{padding:3rem!important}.p-x-0{padding-left:0!important;padding-right:0!important}.p-x-2{padding-left:.125rem!important;padding-right:.125rem!important}.p-x-4{padding-left:.25rem!important;padding-right:.25rem!important}.p-x-8{padding-left:.5rem!important;padding-right:.5rem!important}.p-x-12{padding-left:.75rem!important;padding-right:.75rem!important}.p-x-16{padding-left:1rem!important;padding-right:1rem!important}.p-x-24{padding-left:1.5rem!important;padding-right:1.5rem!important}.p-x-32{padding-left:2rem!important;padding-right:2rem!important}.p-x-48{padding-left:3rem!important;padding-right:3rem!important}.p-y-0{padding-top:0!important;padding-bottom:0!important}.p-y-2{padding-top:.125rem!important;padding-bottom:.125rem!important}.p-y-4{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-y-8{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-y-12{padding-top:.75rem!important;padding-bottom:.75rem!important}.p-y-16{padding-top:1rem!important;padding-bottom:1rem!important}.p-y-24{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-y-32{padding-top:2rem!important;padding-bottom:2rem!important}.p-y-48{padding-top:3rem!important;padding-bottom:3rem!important}.p-t-0{padding-top:0!important}.p-t-2{padding-top:.125rem!important}.p-t-4{padding-top:.25rem!important}.p-t-8{padding-top:.5rem!important}.p-t-12{padding-top:.75rem!important}.p-t-16{padding-top:1rem!important}.p-t-24{padding-top:1.5rem!important}.p-t-32{padding-top:2rem!important}.p-t-48{padding-top:3rem!important}.p-r-0{padding-right:0!important}.p-r-2{padding-right:.125rem!important}.p-r-4{padding-right:.25rem!important}.p-r-8{padding-right:.5rem!important}.p-r-12{padding-right:.75rem!important}.p-r-16{padding-right:1rem!important}.p-r-24{padding-right:1.5rem!important}.p-r-32{padding-right:2rem!important}.p-r-48{padding-right:3rem!important}.p-b-0{padding-bottom:0!important}.p-b-2{padding-bottom:.125rem!important}.p-b-4{padding-bottom:.25rem!important}.p-b-8{padding-bottom:.5rem!important}.p-b-12{padding-bottom:.75rem!important}.p-b-16{padding-bottom:1rem!important}.p-b-24{padding-bottom:1.5rem!important}.p-b-32{padding-bottom:2rem!important}.p-b-48{padding-bottom:3rem!important}.p-l-0{padding-left:0!important}.p-l-2{padding-left:.125rem!important}.p-l-4{padding-left:.25rem!important}.p-l-8{padding-left:.5rem!important}.p-l-12{padding-left:.75rem!important}.p-l-16{padding-left:1rem!important}.p-l-24{padding-left:1.5rem!important}.p-l-32{padding-left:2rem!important}.p-l-48{padding-left:3rem!important}.gap-0{gap:0!important}.gap-2{gap:.125rem!important}.gap-4{gap:.25rem!important}.gap-8{gap:.5rem!important}.gap-12{gap:.75rem!important}.gap-16{gap:1rem!important}.gap-24{gap:1.5rem!important}.gap-32{gap:2rem!important}.gap-48{gap:3rem!important}.b-0{border:0!important}.b-1{border:1px solid var(--mat-divider-color)!important}.b-2{border:2px solid var(--mat-divider-color)!important}.b-4{border:4px solid var(--mat-divider-color)!important}.b-8{border:8px solid var(--mat-divider-color)!important}.b-t-0{border-top:0!important}.b-t-1{border-top:1px solid var(--mat-divider-color)!important}.b-t-2{border-top:2px solid var(--mat-divider-color)!important}.b-t-4{border-top:4px solid var(--mat-divider-color)!important}.b-t-8{border-top:8px solid var(--mat-divider-color)!important}.b-b-0{border-bottom:0!important}.b-b-1{border-bottom:1px solid var(--mat-divider-color)!important}.b-b-2{border-bottom:2px solid var(--mat-divider-color)!important}.b-b-4{border-bottom:4px solid var(--mat-divider-color)!important}.b-b-8{border-bottom:8px solid var(--mat-divider-color)!important}.b-l-0{border-left:0!important}.b-l-1{border-left:1px solid var(--mat-divider-color)!important}.b-l-2{border-left:2px solid var(--mat-divider-color)!important}.b-l-4{border-left:4px solid var(--mat-divider-color)!important}.b-l-8{border-left:8px solid var(--mat-divider-color)!important}.b-r-0{border-right:0!important}.b-r-1{border-right:1px solid var(--mat-divider-color)!important}.b-r-2{border-right:2px solid var(--mat-divider-color)!important}.b-r-4{border-right:4px solid var(--mat-divider-color)!important}.b-r-8{border-right:8px solid var(--mat-divider-color)!important}.b-y-0{border-top:0!important;border-bottom:0!important}.b-y-1{border-top:1px solid var(--mat-divider-color)!important;border-bottom:1px solid var(--mat-divider-color)!important}.b-y-2{border-top:2px solid var(--mat-divider-color)!important;border-bottom:2px solid var(--mat-divider-color)!important}.b-y-4{border-top:4px solid var(--mat-divider-color)!important;border-bottom:4px solid var(--mat-divider-color)!important}.b-y-8{border-top:8px solid var(--mat-divider-color)!important;border-bottom:8px solid var(--mat-divider-color)!important}.b-x-0{border-left:0!important;border-right:0!important}.b-x-1{border-left:1px solid var(--mat-divider-color)!important;border-right:1px solid var(--mat-divider-color)!important}.b-x-2{border-left:2px solid var(--mat-divider-color)!important;border-right:2px solid var(--mat-divider-color)!important}.b-x-4{border-left:4px solid var(--mat-divider-color)!important;border-right:4px solid var(--mat-divider-color)!important}.b-x-8{border-left:8px solid var(--mat-divider-color)!important;border-right:8px solid var(--mat-divider-color)!important}.border-0{border-width:0!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-4{border-width:4px!important}.border-8{border-width:8px!important}.border-t-0{border-top-width:0!important}.border-t-1{border-top-width:1px!important}.border-t-2{border-top-width:2px!important}.border-t-4{border-top-width:4px!important}.border-t-8{border-top-width:8px!important}.border-b-0{border-bottom-width:0!important}.border-b-1{border-bottom-width:1px!important}.border-b-2{border-bottom-width:2px!important}.border-b-4{border-bottom-width:4px!important}.border-b-8{border-bottom-width:8px!important}.border-l-0{border-left-width:0!important}.border-l-1{border-left-width:1px!important}.border-l-2{border-left-width:2px!important}.border-l-4{border-left-width:4px!important}.border-l-8{border-left-width:8px!important}.border-r-0{border-right-width:0!important}.border-r-1{border-right-width:1px!important}.border-r-2{border-right-width:2px!important}.border-r-4{border-right-width:4px!important}.border-r-8{border-right-width:8px!important}.border-y-0{border-top-width:0!important;border-bottom-width:0!important}.border-y-1{border-top-width:1px!important;border-bottom-width:1px!important}.border-y-2{border-top-width:2px!important;border-bottom-width:2px!important}.border-y-4{border-top-width:4px!important;border-bottom-width:4px!important}.border-y-8{border-top-width:8px!important;border-bottom-width:8px!important}.border-x-0{border-left-width:0!important;border-right-width:0!important}.border-x-1{border-left-width:1px!important;border-right-width:1px!important}.border-x-2{border-left-width:2px!important;border-right-width:2px!important}.border-x-4{border-left-width:4px!important;border-right-width:4px!important}.border-x-8{border-left-width:8px!important;border-right-width:8px!important}.border-solid{border-style:solid!important}.border-dashed{border-style:dashed!important}.border-dotted{border-style:dotted!important}.border-double{border-style:double!important}.border-hidden{border-style:hidden!important}.border-none{border-style:none!important}.border-t-solid{border-top-style:solid!important}.border-t-dashed{border-top-style:dashed!important}.border-t-dotted{border-top-style:dotted!important}.border-t-double{border-top-style:double!important}.border-t-hidden{border-top-style:hidden!important}.border-t-none{border-top-style:none!important}.border-b-solid{border-bottom-style:solid!important}.border-b-dashed{border-bottom-style:dashed!important}.border-b-dotted{border-bottom-style:dotted!important}.border-b-double{border-bottom-style:double!important}.border-b-hidden{border-bottom-style:hidden!important}.border-b-none{border-bottom-style:none!important}.border-l-solid{border-left-style:solid!important}.border-l-dashed{border-left-style:dashed!important}.border-l-dotted{border-left-style:dotted!important}.border-l-double{border-left-style:double!important}.border-l-hidden{border-left-style:hidden!important}.border-l-none{border-left-style:none!important}.border-r-solid{border-right-style:solid!important}.border-r-dashed{border-right-style:dashed!important}.border-r-dotted{border-right-style:dotted!important}.border-r-double{border-right-style:double!important}.border-r-hidden{border-right-style:hidden!important}.border-r-none{border-right-style:none!important}.border-y-solid{border-top-style:solid!important;border-bottom-style:solid!important}.border-y-dashed{border-top-style:dashed!important;border-bottom-style:dashed!important}.border-y-dotted{border-top-style:dotted!important;border-bottom-style:dotted!important}.border-y-double{border-top-style:double!important;border-bottom-style:double!important}.border-y-hidden{border-top-style:hidden!important;border-bottom-style:hidden!important}.border-y-none{border-top-style:none!important;border-bottom-style:none!important}.border-x-solid{border-left-style:solid!important;border-right-style:solid!important}.border-x-dashed{border-left-style:dashed!important;border-right-style:dashed!important}.border-x-dotted{border-left-style:dotted!important;border-right-style:dotted!important}.border-x-double{border-left-style:double!important;border-right-style:double!important}.border-x-hidden{border-left-style:hidden!important;border-right-style:hidden!important}.border-x-none{border-left-style:none!important;border-right-style:none!important}.r-0{border-radius:0!important}.r-4{border-radius:.25rem!important}.r-8{border-radius:.5rem!important}.r-12{border-radius:.75rem!important}.r-16{border-radius:1rem!important}.r-full{border-radius:9999px!important}.r-t-l-0{border-top-left-radius:0!important}.r-t-l-4{border-top-left-radius:.25rem!important}.r-t-l-8{border-top-left-radius:.5rem!important}.r-t-l-12{border-top-left-radius:.75rem!important}.r-t-l-16{border-top-left-radius:1rem!important}.r-t-l-full{border-top-left-radius:9999px!important}.r-t-r-0{border-top-right-radius:0!important}.r-t-r-4{border-top-right-radius:.25rem!important}.r-t-r-8{border-top-right-radius:.5rem!important}.r-t-r-12{border-top-right-radius:.75rem!important}.r-t-r-16{border-top-right-radius:1rem!important}.r-t-r-full{border-top-right-radius:9999px!important}.r-b-r-0{border-bottom-right-radius:0!important}.r-b-r-4{border-bottom-right-radius:.25rem!important}.r-b-r-8{border-bottom-right-radius:.5rem!important}.r-b-r-12{border-bottom-right-radius:.75rem!important}.r-b-r-16{border-bottom-right-radius:1rem!important}.r-b-r-full{border-bottom-right-radius:9999px!important}.r-b-l-0{border-bottom-left-radius:0!important}.r-b-l-4{border-bottom-left-radius:.25rem!important}.r-b-l-8{border-bottom-left-radius:.5rem!important}.r-b-l-12{border-bottom-left-radius:.75rem!important}.r-b-l-16{border-bottom-left-radius:1rem!important}.r-b-l-full{border-bottom-left-radius:9999px!important}.r-t-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.r-t-4{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.r-t-8{border-top-left-radius:.5rem!important;border-top-right-radius:.5rem!important}.r-t-12{border-top-left-radius:.75rem!important;border-top-right-radius:.75rem!important}.r-t-16{border-top-left-radius:1rem!important;border-top-right-radius:1rem!important}.r-t-full{border-top-left-radius:9999px!important;border-top-right-radius:9999px!important}.r-b-0{border-bottom-left-radius:0!important;border-bottom-right-radius:0!important}.r-b-4{border-bottom-left-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.r-b-8{border-bottom-left-radius:.5rem!important;border-bottom-right-radius:.5rem!important}.r-b-12{border-bottom-left-radius:.75rem!important;border-bottom-right-radius:.75rem!important}.r-b-16{border-bottom-left-radius:1rem!important;border-bottom-right-radius:1rem!important}.r-b-full{border-bottom-left-radius:9999px!important;border-bottom-right-radius:9999px!important}.r-l-0{border-top-left-radius:0!important;border-bottom-left-radius:0!important}.r-l-4{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.r-l-8{border-top-left-radius:.5rem!important;border-bottom-left-radius:.5rem!important}.r-l-12{border-top-left-radius:.75rem!important;border-bottom-left-radius:.75rem!important}.r-l-16{border-top-left-radius:1rem!important;border-bottom-left-radius:1rem!important}.r-l-full{border-top-left-radius:9999px!important;border-bottom-left-radius:9999px!important}.r-r-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.r-r-4{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.r-r-8{border-top-right-radius:.5rem!important;border-bottom-right-radius:.5rem!important}.r-r-12{border-top-right-radius:.75rem!important;border-bottom-right-radius:.75rem!important}.r-r-16{border-top-right-radius:1rem!important;border-bottom-right-radius:1rem!important}.r-r-full{border-top-right-radius:9999px!important;border-bottom-right-radius:9999px!important}.f-s-0{font-size:0!important}.f-s-10{font-size:.625rem!important}.f-s-12{font-size:.75rem!important}.f-s-14{font-size:.875rem!important}.f-s-16{font-size:1rem!important}.f-s-18{font-size:1.125rem!important}.f-s-20{font-size:1.25rem!important}.f-w-100{font-weight:100!important}.f-w-200{font-weight:200!important}.f-w-300{font-weight:300!important}.f-w-400{font-weight:400!important}.f-w-500{font-weight:500!important}.f-w-600{font-weight:600!important}.f-w-700{font-weight:700!important}.f-w-800{font-weight:800!important}.f-w-900{font-weight:900!important}.font-italic{font-style:italic!important}.font-normal{font-style:normal!important}.text-center{text-align:center!important}.text-right{text-align:right!important}.text-left{text-align:left!important}.text-none{text-decoration:none!important}.text-underline{text-decoration:underline!important}.text-line-through{text-decoration:line-through!important}.text-capitalize{text-transform:capitalize!important}.text-uppercase{text-transform:uppercase!important}.text-lowercase{text-transform:lowercase!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-ellipsis{text-overflow:ellipsis!important}.text-reset{color:inherit!important}.text-current{color:currentColor!important}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.cursor-default{cursor:default!important}.cursor-pointer{cursor:pointer!important}.cursor-text{cursor:text!important}.cursor-move{cursor:move!important}.cursor-copy{cursor:copy!important}.cursor-not-allowed{cursor:not-allowed!important}.select-all{-webkit-user-select:all!important;user-select:all!important}.select-auto{-webkit-user-select:auto!important;user-select:auto!important}.select-none{-webkit-user-select:none!important;user-select:none!important}.pointer-events-none{pointer-events:none!important}.pointer-events-auto{pointer-events:auto!important}.object-contain{object-fit:contain!important}.object-cover{object-fit:cover!important}.object-fill{object-fit:fill!important}.object-none{object-fit:none!important}.object-scale-down{object-fit:scale-down!important}.object-top{object-position:top!important}.object-bottom{object-position:bottom!important}.object-center{object-position:center!important}.object-left{object-position:left!important}.object-left-top{object-position:left top!important}.object-left-bottom{object-position:left bottom!important}.object-right{object-position:right!important}.object-right-top{object-position:right top!important}.object-right-bottom{object-position:right bottom!important}.photoviewer-modal{position:absolute;z-index:1100;width:320px;height:320px;color:#333;background-color:#fff;border-radius:6px;box-shadow:inset 0 0 1px #fff9,0 0 1px #0009,0 8px 32px -4px #0006;outline:none}.photoviewer-modal:focus-visible{outline:4px solid rgba(0,0,0,.2)}.photoviewer-inner{position:absolute;inset:0}.photoviewer-maximized{position:fixed;inset:0;width:auto;height:auto}.photoviewer-maximized.photoviewer-modal{border-width:0;border-radius:0}.photoviewer-maximized .photoviewer-header{border-radius:0}.photoviewer-maximized .photoviewer-resizable-handle{display:none}.photoviewer-button{display:inline-block;min-width:40px;height:40px;box-sizing:border-box;font-size:16px;line-height:1;background:none;border:none;color:inherit;cursor:pointer;outline:none}.photoviewer-button:hover{color:#111}.photoviewer-button:focus{background-color:#0000001a}.photoviewer-button svg{width:1em;height:1em;vertical-align:-.125em}.photoviewer-header{position:relative;z-index:1;height:30px;border-radius:6px 6px 0 0;overflow:hidden}.photoviewer-header .photoviewer-toolbar{float:right}[dir=rtl] .photoviewer-header .photoviewer-toolbar{float:left}.photoviewer-header .photoviewer-button{height:30px}.photoviewer-header .photoviewer-button:hover{background-color:#0000001a}.photoviewer-header .photoviewer-button-close:hover{color:#fff;background-color:#ff4545}.photoviewer-title{padding:8px 10px;font-size:14px;line-height:1;white-space:nowrap;text-overflow:ellipsis;-webkit-user-select:none;user-select:none;overflow:hidden}.photoviewer-stage{position:absolute;inset:30px 0 40px;border-top:1px solid rgba(0,0,0,.2);border-bottom:1px solid rgba(0,0,0,.2);overflow:hidden}.photoviewer-image{position:absolute;display:inline-block;min-width:auto;max-width:none}.photoviewer-footer{position:absolute;bottom:0;z-index:1;width:100%;height:40px;text-align:center}.photoviewer-align-center{text-align:center}.photoviewer-align-center:before{content:"";display:inline-block;height:100%;vertical-align:middle;overflow:hidden}.photoviewer-align-center .photoviewer-image{position:static;max-width:100%;max-height:100%;vertical-align:middle}.photoviewer-image-error{display:none}.photoviewer-error-msg{vertical-align:middle}[dir=rtl] .photoviewer-button-prev,[dir=rtl] .photoviewer-button-next{transform:rotate(180deg)}.photoviewer-resizable-handle{position:absolute;z-index:1}.photoviewer-resizable-handle-e,.photoviewer-resizable-handle-w{top:0;bottom:0;width:8px;cursor:ew-resize}.photoviewer-resizable-handle-e{right:-4px}.photoviewer-resizable-handle-w{left:-4px}.photoviewer-resizable-handle-s,.photoviewer-resizable-handle-n{right:0;left:0;height:8px;cursor:ns-resize}.photoviewer-resizable-handle-s{bottom:-4px}.photoviewer-resizable-handle-n{top:-4px}.photoviewer-resizable-handle-se,.photoviewer-resizable-handle-sw,.photoviewer-resizable-handle-ne,.photoviewer-resizable-handle-nw{width:8px;height:8px}.photoviewer-resizable-handle-se{right:-4px;bottom:-4px;cursor:nwse-resize}.photoviewer-resizable-handle-sw{bottom:-4px;left:-4px;cursor:nesw-resize}.photoviewer-resizable-handle-ne{top:-4px;right:-4px;cursor:nesw-resize}.photoviewer-resizable-handle-nw{top:-4px;left:-4px;cursor:nwse-resize}.photoviewer-modal:fullscreen{inset:0!important;width:100%!important;height:100%!important;background-color:#000;border-width:0;border-radius:0}.photoviewer-modal:fullscreen .photoviewer-header,.photoviewer-modal:fullscreen .photoviewer-footer,.photoviewer-modal:fullscreen .photoviewer-resizable-handle{display:none}.photoviewer-modal:fullscreen .photoviewer-stage{inset:0;border-width:0;background-color:transparent}.is-grab{cursor:move;cursor:-webkit-grab;cursor:grab}.is-grabbing{cursor:move;cursor:-webkit-grabbing;cursor:grabbing}.photoviewer-loader{position:absolute;inset:0;text-align:center;color:#333}.photoviewer-loader:before{content:"";position:relative;display:inline-block;width:32px;height:32px;box-sizing:border-box;border-width:4px;border-style:solid;border-color:rgba(0,0,0,.5) rgba(0,0,0,.5) rgba(0,0,0,.5) rgba(255,255,255,.5);border-radius:100%;vertical-align:middle;animation:photoviewerLoading 1s infinite linear}.photoviewer-loader:after{content:"";display:inline-block;width:0;height:100%;vertical-align:middle;overflow:hidden}@keyframes photoviewerLoading{0%{transform:rotate(0) translateZ(0)}to{transform:rotate(360deg) translateZ(0)}}@keyframes hotToastEnter{0%{transform:scale(0)}to{transform:scale(1)}}.hot-toast-animated-icon{animation:var(--hot-toast-animated-icon-animation, hotToastEnter .3s ease-in-out forwards);position:var(--hot-toast-animated-icon-position, relative);transform:var(--hot-toast-animated-icon-transform, scale(0))}@media (prefers-reduced-motion: reduce){.hot-toast-animated-icon{animation-duration:var(--hot-toast-animated-icon-reduced-motion-animation-duration, none);opacity:var(--hot-toast-animated-icon-reduced-motion-opacity, 1);transform:var(--hot-toast-animated-icon-reduced-motion-transform, scale(1))}}@keyframes hotToastCircleAnimation{0%{opacity:0;transform:scale(0) rotate(45deg)}to{opacity:1;transform:scale(1) rotate(45deg)}}@keyframes hotToastCheckmarkAnimation{0%{height:0;opacity:0;width:0}40%{height:0;opacity:1;width:6px}to{height:10px;opacity:1}}.hot-toast-checkmark-icon{animation:var(--hot-toast-checkmark-icon-animation, hotToastCircleAnimation .3s cubic-bezier(.175, .885, .32, 1.275) forwards);animation-delay:var(--hot-toast-checkmark-icon-animation-delay, .1s);background-color:var(--hot-toast-checkmark-icon-background-color, var(--check-primary, #61d345));border-radius:var(--hot-toast-checkmark-icon-border-radius, 10px);height:var(--hot-toast-checkmark-icon-height, 20px);opacity:var(--hot-toast-checkmark-icon-opacity, 0);position:var(--hot-toast-checkmark-icon-position, relative);transform:var(--hot-toast-checkmark-icon-transform, rotate(45deg));width:var(--hot-toast-checkmark-icon-width, 20px)}@media (prefers-reduced-motion: reduce){.hot-toast-checkmark-icon{animation-duration:var(--hot-toast-checkmark-icon-reduced-motion-animation-duration, 0ms)}}.hot-toast-checkmark-icon:after{animation:var(--hot-toast-checkmark-icon-after-animation, hotToastCheckmarkAnimation .2s ease-out forwards);animation-delay:var(--hot-toast-checkmark-icon-after-animation-delay, .2s);border-bottom-style:solid;border-bottom-width:var(--hot-toast-checkmark-icon-after-border-bottom, 2px);border-color:var(--hot-toast-checkmark-icon-after-border-color, var(--check-secondary, #fff));border-right-style:solid;border-right-width:var(--hot-toast-checkmark-icon-after-border-right, 2px);bottom:var(--hot-toast-checkmark-icon-after-bottom, 6px);box-sizing:var(--hot-toast-checkmark-icon-after-box-sizing, border-box);content:var(--hot-toast-checkmark-icon-after-content, "");height:var(--hot-toast-checkmark-icon-after-height, 10px);left:var(--hot-toast-checkmark-icon-after-left, 6px);opacity:var(--hot-toast-checkmark-icon-after-opacity, 0);position:var(--hot-toast-checkmark-icon-after-position, absolute);width:var(--hot-toast-checkmark-icon-after-width, 6px)}@media (prefers-reduced-motion: reduce){.hot-toast-checkmark-icon:after{animation-duration:var(--hot-toast-checkmark-icon-after-reduced-motion-animation-duration, 0ms)}}@keyframes hotToastErrorCircleAnimation{0%{opacity:0;transform:scale(0) rotate(45deg)}to{opacity:1;transform:scale(1) rotate(45deg)}}@keyframes hotToastFirstLineAnimation{0%{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}@keyframes hotToastSecondLineAnimation{0%{opacity:0;transform:scale(0) rotate(90deg)}to{opacity:1;transform:scale(1) rotate(90deg)}}.hot-toast-error-icon{animation:var(--hot-toast-error-icon-animation, hotToastErrorCircleAnimation .3s cubic-bezier(.175, .885, .32, 1.275) forwards);animation-delay:var(--hot-toast-error-icon-animation-delay, .1s);background:var(--hot-toast-error-icon-background, var(--error-primary, #ff4b4b));border-radius:var(--hot-toast-error-icon-border-radius, 10px);height:var(--hot-toast-error-icon-height, 20px);opacity:var(--hot-toast-error-icon-opacity, 0);position:var(--hot-toast-error-icon-position, relative);transform:var(--hot-toast-error-icon-transform, rotate(45deg));width:var(--hot-toast-error-icon-width, 20px)}@media (prefers-reduced-motion: reduce){.hot-toast-error-icon{animation-duration:var(--hot-toast-error-icon-reduced-motion-animation-duration, 0ms)}}.hot-toast-error-icon:after,.hot-toast-error-icon:before{animation:var(--hot-toast-error-icon-after-before-animation, hotToastFirstLineAnimation .15s ease-out forwards);animation-delay:var(--hot-toast-error-icon-after-before-animation-delay, .15s);background:var(--hot-toast-error-icon-after-before-background, var(--error-secondary, #fff));border-radius:var(--hot-toast-error-icon-after-before-border-radius, 3px);bottom:var(--hot-toast-error-icon-after-before-bottom, 9px);content:var(--hot-toast-error-icon-after-before-content, "");height:var(--hot-toast-error-icon-after-before-height, 2px);left:var(--hot-toast-error-icon-after-before-left, 4px);opacity:var(--hot-toast-error-icon-after-before-opacity, 0);position:var(--hot-toast-error-icon-after-before-position, absolute);width:var(--hot-toast-error-icon-after-before-width, 12px)}@media (prefers-reduced-motion: reduce){.hot-toast-error-icon:after,.hot-toast-error-icon:before{animation-duration:var(--hot-toast-error-icon-after-before-reduced-motion-animation-duration, 0ms)}}.hot-toast-error-icon:before{animation:var(--hot-toast-error-icon-before-animation, hotToastSecondLineAnimation .15s ease-out forwards);animation-delay:var(--hot-toast-error-icon-before-animation-delay, .18s);transform:var(--hot-toast-error-icon-before-transform, rotate(90deg))}@media (prefers-reduced-motion: reduce){.hot-toast-error-icon:before{animation-duration:var(--hot-toast-error-icon-before-reduced-motion-animation-duration, 0ms)}}.hot-toast-bar-base{align-items:var(--hot-toast-align-items, center);background-color:var(--hot-toast-bg, #fff);border-radius:var(--hot-toast-border-radius, 4px);box-shadow:var(--hot-toast-shadow, 0 3px 10px rgba(0, 0, 0, .1), 0 3px 3px rgba(0, 0, 0, .05));color:var(--hot-toast-color, #363636);display:var(--hot-toast-display, flex);line-height:var(--hot-toast-line, 1.3);margin:var(--hot-toast-margin, 16px);max-width:var(--hot-toast-max-width, 350px);padding:var(--hot-toast-padding, 8px 10px);pointer-events:var(--hot-toast-pointer-events, auto);width:var(--hot-toast-width, fit-content);transition-property:border-bottom-left-radius,border-bottom-right-radius;transition-duration:.23s;transition-timing-function:ease-out}.hot-toast-bar-base:hover,.hot-toast-bar-base:focus{animation-play-state:var(--hot-toast-animation-state, paused)!important}@media (prefers-reduced-motion: reduce){.hot-toast-bar-base{animation-duration:var(--hot-toast-reduced-motion-animation-duration, 10ms)!important}}.expanded .hot-toast-bar-base{border-bottom-left-radius:0;border-bottom-right-radius:0}.hot-toast-message{color:var(--hot-toast-message-color, inherit);display:var(--hot-toast-message-display, flex);flex:var(--hot-toast-message-flex, 1);justify-content:var(--hot-toast-message-justify-content, center);margin:var(--hot-toast-message-margin, 4px 10px)}.hot-toast-bar-base-container{display:var(--hot-toast-container-display, flex);pointer-events:var(--hot-toast-container-pointer-events, none);position:var(--hot-toast-container-position, absolute);transition:var(--hot-toast-container-transition, transform .23s cubic-bezier(.21, 1.02, .73, 1))}@media (prefers-reduced-motion: reduce){.hot-toast-bar-base-container{transition-duration:var(--hot-toast-container-reduced-motion-transition-duration, 10ms)!important}}.hot-toast-bar-base-container.hot-toast-theme-snackbar .hot-toast-bar-base{background:var(--hot-toast-snackbar-bg, #323232);box-shadow:var(--hot-toast-snackbar-shadow, 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12));color:var(--hot-toast-snackbar-color, rgb(255, 255, 255))}.hot-toast-bar-base-container.hot-toast-theme-snackbar .hot-toast-close-btn{filter:var(--hot-toast-snackbar-close-btn-filter, invert(1) grayscale(100%) brightness(200%))}.hot-toast-close-btn{align-self:var(--hot-toast-close-btn-align-self, flex-start);background-color:var(--hot-toast-close-btn-background-color, transparent);background-image:var(--hot-toast-close-btn-background-image, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e"));background-position:var(--hot-toast-close-btn-background-position, center);background-repeat:var(--hot-toast-close-btn-background-repeat, no-repeat);background-size:var(--hot-toast-close-btn-background-size, .75em);border:var(--hot-toast-close-btn-border, 0);border-radius:var(--hot-toast-close-btn-border-radius, .25rem);box-sizing:var(--hot-toast-close-btn-box-sizing, content-box);display:var(--hot-toast-close-btn-display, flex);height:var(--hot-toast-close-btn-height, .8em);margin-top:var(--hot-toast-close-btn-margin-top, .25em);opacity:var(--hot-toast-close-btn-opacity, .5);padding:var(--hot-toast-close-btn-padding, .25em);width:var(--hot-toast-close-btn-width, .8em)}.hot-toast-close-btn:focus{box-shadow:var(--hot-toast-close-btn-box-shadow, 0 0 0 .125rem rgba(13, 110, 253, .25));outline:var(--hot-toast-close-btn-outline, none)}.hot-toast-close-btn:hover,.hot-toast-close-btn:focus{opacity:var(--hot-toast-close-btn-opacity, .75)}.hot-toast-group-btn{align-self:var(--hot-toast-group-btn-align-self, flex-start);background-color:var(--hot-toast-group-btn-background-color, transparent);background-image:var(--hot-toast-group-btn-background-image, url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.75745 10.5858L9.17166 9.17154L12.0001 12L14.8285 9.17157L16.2427 10.5858L12.0001 14.8284L7.75745 10.5858Z' fill='currentColor' /%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21Z' fill='currentColor' /%3E%3C/svg%3E"));background-position:var(--hot-toast-group-btn-background-position, center);background-repeat:var(--hot-toast-group-btn-background-repeat, no-repeat);background-size:var(--hot-toast-group-btn-background-size, 1.3em);border:var(--hot-toast-group-btn-border, 0);border-radius:var(--hot-toast-group-btn-border-radius, .25rem);box-sizing:var(--hot-toast-group-btn-box-sizing, content-box);display:var(--hot-toast-group-btn-display, flex);height:var(--hot-toast-group-btn-height, .8em);margin-top:var(--hot-toast-group-btn-margin-top, .25em);opacity:var(--hot-toast-group-btn-opacity, .5);padding:var(--hot-toast-group-btn-padding, .25em);width:var(--hot-toast-group-btn-width, .8em);will-change:var(--hot-toast-group-btn-will-change, transform);transition:var(--hot-toast-group-btn-transition, transform .23s cubic-bezier(.21, 1.02, .73, 1))}.hot-toast-group-btn:focus{box-shadow:var(--hot-toast-group-btn-box-shadow, 0 0 0 .125rem rgba(13, 110, 253, .25));outline:var(--hot-toast-group-btn-outline, none)}.hot-toast-group-btn:hover,.hot-toast-group-btn:focus{opacity:var(--hot-toast-group-btn-opacity, .75)}.expanded .hot-toast-group-btn{transform:rotate(var(--hot-toast-group-btn-expanded-rotate, 180deg))}.hot-toast-icon{align-self:var(--hot-toast-icon-align-self, flex-start);padding-top:var(--hot-toast-icon-padding-top, .25em)}.hot-toast-bar-base-wrapper{pointer-events:all}.hot-toast-bar-base-group{--hot-toast-shadow: none;background-color:var(--hot-toast-group-bg, #fff);margin:var(--hot-toast-margin, 16px);margin-top:calc(-1 * var(--hot-toast-margin, 16px));border-bottom-left-radius:var(--hot-toast-border-radius, 4px);border-bottom-right-radius:var(--hot-toast-border-radius, 4px);height:0;overflow:hidden;transition-property:height;transition-duration:.23s;transition-timing-function:ease-in-out;position:relative;box-shadow:var(--hot-toast-group-after-shadow, 0 3px 10px rgba(0, 0, 0, .1), 0 3px 3px rgba(0, 0, 0, .05))}.expanded .hot-toast-bar-base-group{height:var(--hot-toast-group-height)}.hot-toast-bar-base-group .hot-toast-bar-base{margin:0}@keyframes hotToastEnterAnimationNegative{0%{opacity:.5;transform:translate3d(0,-80px,0) scale(.6)}to{opacity:1;transform:translateZ(0) scale(1)}}@keyframes hotToastEnterAnimationPositive{0%{opacity:.5;transform:translate3d(0,80px,0) scale(.6)}to{opacity:1;transform:translateZ(0) scale(1)}}@keyframes hotToastExitAnimationPositive{0%{opacity:1;transform:translateZ(-1px) scale(1)}to{opacity:0;transform:translate3d(0,var(--hot-toast-exit-positive-y, 130px),-1px) scale(.5)}}@keyframes hotToastExitAnimationNegative{0%{opacity:1;transform:translateZ(-1px) scale(1)}to{opacity:0;transform:translate3d(0,var(--hot-toast-exit-negative-y, -130px),-1px) scale(.5)}}@keyframes hotToastEnterSoftAnimationNegative{0%{opacity:.5;transform:translate3d(0,-14px,0)}to{opacity:1;transform:translateZ(0)}}@keyframes hotToastEnterSoftAnimationPositive{0%{opacity:.5;transform:translate3d(0,14px,0)}to{opacity:1;transform:translateZ(0)}}@keyframes hotToastExitSoftAnimationPositive{0%{opacity:1;transform:translateZ(-1px)}to{opacity:0;transform:translate3d(0,14px,-1px)}}@keyframes hotToastExitSoftAnimationNegative{0%{opacity:1;transform:translateZ(-1px)}to{opacity:0;transform:translate3d(0,-14px,-1px)}}.hot-toast-indicator-wrapper{align-items:var(--hot-toast-indicator-wrapper-align-items, center);display:var(--hot-toast-indicator-wrapper-display, flex);justify-content:var(--hot-toast-indicator-wrapper-justify-content, center);min-height:var(--hot-toast-indicator-wrapper-min-height, 20px);min-width:var(--hot-toast-indicator-wrapper-min-width, 20px);position:var(--hot-toast-indicator-wrapper-position, relative)}.hot-toast-status-wrapper{position:var(--hot-toast-status-wrapper-position, absolute)}@keyframes animate-info-background{0%{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}@keyframes animate-info-line{0%{height:0;opacity:0}40%{height:4.8px;opacity:1}to{height:8px;opacity:1}}.hot-toast-info-icon{animation:var(--hot-toast-info-icon-animation, animate-info-background .3s cubic-bezier(.175, .885, .32, 1.275) forwards);animation-delay:var(--hot-toast-info-icon-animation-delay, .1s);background-color:var(--hot-toast-info-icon-background-color, var(--info-primary, #0d6efd));border-radius:var(--hot-toast-info-icon-border-radius, 50%);display:var(--hot-toast-info-icon-display, block);height:var(--hot-toast-info-icon-height, 20px);opacity:var(--hot-toast-info-icon-opacity, 0);position:var(--hot-toast-info-icon-position, relative);transform:var(--hot-toast-info-icon-transform, scale(0));width:var(--hot-toast-info-icon-width, 20px)}.hot-toast-info-icon:after,.hot-toast-info-icon:before{background-color:var(--hot-toast-info-icon-after-before-background-color, var(--info-secondary, #fff));border-radius:var(--hot-toast-info-icon-after-before-border-radius, 3px);box-sizing:var(--hot-toast-info-icon-after-before-box-sizing, border-box);content:var(--hot-toast-info-icon-after-before-content, "");display:var(--hot-toast-info-icon-after-before-display, block);left:var(--hot-toast-info-icon-after-before-left, 8.5px);position:var(--hot-toast-info-icon-after-before-position, absolute);width:var(--hot-toast-info-icon-after-before-width, 2.5px)}.hot-toast-info-icon:after{animation:var(--hot-toast-info-icon-after-animation, animate-info-line .2s ease-out forwards);animation-delay:var(--hot-toast-info-icon-after-animation-delay, .2s);height:var(--hot-toast-info-icon-after-height, 0);opacity:var(--hot-toast-info-icon-after-opacity, 0);top:var(--hot-toast-info-icon-after-bottom, 8px)}.hot-toast-info-icon:before{height:var(--hot-toast-info-icon-before-height, 2px);top:var(--hot-toast-info-icon-before-top, 4px)}@keyframes hotToastRotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.hot-toast-loader-icon{animation:var(--hot-toast-loader-icon-animation, hotToastRotate 1s linear infinite);border:var(--hot-toast-loader-icon-border, 2px solid);border-bottom-color:var(--hot-toast-loader-icon-border-color, #e0e0e0);border-left-color:var(--hot-toast-loader-icon-border-color, #e0e0e0);border-radius:var(--hot-toast-loader-icon-border-radius, 100%);border-right-color:var(--hot-toast-loader-icon-border-right-color, #616161);border-top-color:var(--hot-toast-loader-icon-border-color, #e0e0e0);box-sizing:var(--hot-toast-loader-icon-box-sizing, border-box);height:var(--hot-toast-loader-icon-height, 18px);padding-top:var(--hot-toast-loader-icon-padding-top, 2px);width:var(--hot-toast-loader-icon-width, 18px)}@media (prefers-reduced-motion: reduce){.hot-toast-loader-icon{animation-duration:var(--hot-toast-loader-icon-reduced-motion-animation-duration, 5s)}}@keyframes animate-warn-background{0%{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}@keyframes animate-warn-line{0%{height:0;opacity:0}40%{height:4.8px;opacity:1}to{height:8px;opacity:1}}.hot-toast-warning-icon{animation:var(--hot-toast-warning-icon-animation, animate-warn-background .3s cubic-bezier(.175, .885, .32, 1.275) forwards);animation-delay:var(--hot-toast-warning-icon-animation-delay, .1s);background-color:var(--hot-toast-warning-icon-background-color, var(--warn-primary, #ffab00));border-radius:var(--hot-toast-warning-icon-border-radius, 50%);display:var(--hot-toast-warning-icon-display, block);height:var(--hot-toast-warning-icon-height, 20px);opacity:var(--hot-toast-warning-icon-opacity, 0);position:var(--hot-toast-warning-icon-position, relative);transform:var(--hot-toast-warning-icon-transform, scale(0));width:var(--hot-toast-warning-icon-width, 20px)}.hot-toast-warning-icon:after,.hot-toast-warning-icon:before{background-color:var(--hot-toast-warning-icon-after-before-background-color, var(--warn-secondary, #fff));border-radius:var(--hot-toast-warning-icon-after-before-border-radius, 3px);box-sizing:var(--hot-toast-warning-icon-after-before-box-sizing, border-box);content:var(--hot-toast-warning-icon-after-before-content, "");display:var(--hot-toast-warning-icon-after-before-display, block);left:var(--hot-toast-warning-icon-after-before-left, 8.5px);position:var(--hot-toast-warning-icon-after-before-position, absolute);width:var(--hot-toast-warning-icon-after-before-width, 2.5px)}.hot-toast-warning-icon:after{animation:var(--hot-toast-warning-icon-after-animation, animate-warn-line .2s ease-out forwards);animation-delay:var(--hot-toast-warning-icon-after-animation-delay, .2s);height:var(--hot-toast-warning-icon-after-height, 0);opacity:var(--hot-toast-warning-icon-after-opacity, 0);top:var(--hot-toast-warning-icon-after-top, 4px)}.hot-toast-warning-icon:before{bottom:var(--hot-toast-warning-icon-before-bottom, 4px);height:var(--hot-toast-warning-icon-before-height, 2px)}.hot-toast-theme-toast{--hot-toast-padding: 6px 10px;--hot-toast-border-radius: 8px;--hot-toast-line: 1.25;--hot-toast-message-margin: 4px 8px;--hot-toast-icon-padding-top: 4px;--hot-toast-close-btn-width: 12px;--hot-toast-close-btn-height: 12px;--hot-toast-close-btn-padding: 4px;--hot-toast-close-btn-margin-top: 4px}formly-wrapper-mat-form-field .mat-mdc-form-field{width:100%}mat-card{margin-bottom:var(--gutter)}mat-card-header+mat-card-content{margin-top:var(--gutter)}.form-field-full .mat-mdc-form-field{width:100%} diff --git a/sync-master-to-develop-auto.sh b/sync-master-to-develop-auto.sh deleted file mode 100644 index cd02978..0000000 --- a/sync-master-to-develop-auto.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash - -# Script to sync master/main branch to develop branch in all submodules (auto-push version) -# Usage: bash sync-master-to-develop-auto.sh - -set -e # Exit on error - -echo "=========================================" -echo "Syncing master/main to develop in all submodules (AUTO-PUSH)" -echo "=========================================" -echo "" - -# Get list of submodules -submodules=$(git submodule --quiet foreach --recursive 'echo $sm_path') - -# Process each submodule -for submodule in $submodules; do - echo "----------------------------------------" - echo "Processing: $submodule" - echo "----------------------------------------" - - cd "$submodule" - - # Determine if repo uses 'main' or 'master' - if git show-ref --verify --quiet refs/heads/main; then - MAIN_BRANCH="main" - else - MAIN_BRANCH="master" - fi - - echo "Main branch: $MAIN_BRANCH" - - # Check if develop branch exists - if ! git show-ref --verify --quiet refs/heads/develop; then - echo "⚠️ No develop branch found in $submodule, skipping..." - cd - > /dev/null - continue - fi - - # Fetch latest changes - echo "Fetching latest changes..." - git fetch origin - - # Checkout and update main/master - echo "Updating $MAIN_BRANCH..." - git checkout "$MAIN_BRANCH" - git pull origin "$MAIN_BRANCH" - - # Checkout and update develop - echo "Updating develop..." - git checkout develop - git pull origin develop - - # Merge main/master into develop - echo "Merging $MAIN_BRANCH into develop..." - if git merge "$MAIN_BRANCH" --no-edit; then - echo "✅ Merge successful" - - # Auto-push to remote - echo "Pushing to origin/develop..." - git push origin develop - echo "✅ Pushed to origin/develop" - else - echo "❌ Merge conflict detected!" - echo "Please resolve conflicts manually in: $submodule" - echo "Then run: git add . && git commit && git push origin develop" - cd - > /dev/null - exit 1 - fi - - # Return to parent directory - cd - > /dev/null - echo "" -done - -echo "=========================================" -echo "✅ All submodules processed successfully" -echo "=========================================" -echo "" -echo "Next steps:" -echo "1. Update parent repo to reference new commits:" -echo " git add ." -echo " git commit -m 'Update submodules after syncing master to develop'" -echo " git push" diff --git a/sync-master-to-develop.sh b/sync-master-to-develop.sh deleted file mode 100644 index 11d2d78..0000000 --- a/sync-master-to-develop.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Script to sync master/main branch to develop branch in all submodules -# Usage: bash sync-master-to-develop.sh - -set -e # Exit on error - -echo "=========================================" -echo "Syncing master/main to develop in all submodules" -echo "=========================================" -echo "" - -# Get list of submodules -submodules=$(git submodule --quiet foreach --recursive 'echo $sm_path') - -# Process each submodule -for submodule in $submodules; do - echo "----------------------------------------" - echo "Processing: $submodule" - echo "----------------------------------------" - - cd "$submodule" - - # Determine if repo uses 'main' or 'master' - if git show-ref --verify --quiet refs/heads/main; then - MAIN_BRANCH="main" - else - MAIN_BRANCH="master" - fi - - echo "Main branch: $MAIN_BRANCH" - - # Check if develop branch exists - if ! git show-ref --verify --quiet refs/heads/develop; then - echo "⚠️ No develop branch found in $submodule, skipping..." - cd - > /dev/null - continue - fi - - # Fetch latest changes - echo "Fetching latest changes..." - git fetch origin - - # Checkout and update main/master - echo "Updating $MAIN_BRANCH..." - git checkout "$MAIN_BRANCH" - git pull origin "$MAIN_BRANCH" - - # Checkout and update develop - echo "Updating develop..." - git checkout develop - git pull origin develop - - # Merge main/master into develop - echo "Merging $MAIN_BRANCH into develop..." - if git merge "$MAIN_BRANCH" --no-edit; then - echo "✅ Merge successful" - - # Push to remote - read -p "Push to origin/develop? (y/n) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - git push origin develop - echo "✅ Pushed to origin/develop" - else - echo "⏭️ Skipped push (you can push manually later)" - fi - else - echo "❌ Merge conflict detected!" - echo "Please resolve conflicts manually in: $submodule" - echo "Then run: git add . && git commit && git push origin develop" - cd - > /dev/null - exit 1 - fi - - # Return to parent directory - cd - > /dev/null - echo "" -done - -echo "=========================================" -echo "✅ All submodules processed successfully" -echo "=========================================" -echo "" -echo "Next steps:" -echo "1. Update parent repo to reference new commits:" -echo " git add ." -echo " git commit -m 'Update submodules after syncing master to develop'" -echo " git push"