diff --git a/.env.sample b/.env.sample index 65926e57f..e94748a2e 100644 --- a/.env.sample +++ b/.env.sample @@ -15,7 +15,7 @@ POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DB=db POSTGRES_USER=pguser -POSTGRES_PASSWORD=pgpassword +POSTGRES_PASSWORD= ANTHROPIC_API_KEY=none ANTHROPIC_MAX_TOKENS=0 diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 120ccf259..e1e936204 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -49,9 +49,29 @@ jobs: run: | echo "🏗️ Building the solution..." cd /src/_src && dotnet build --verbosity normal + - name: install dependencies + run: | + echo "📦 Installing dependencies..." + apt install -y texlive-binaries texlive-luatex texlive-pictures \ + texlive-fonts-extra texlive-lang-french texlive-lang-english \ + texlive-lang-portuguese texlive-lang-spanish \ + texlive-latex-recommended texlive-latex-extra - name: Test + artifacts: + name: test-results + path: test-results.html + run: | echo "🚀 Lancement des tests..." cd /src/_src && dotnet test \ - --verbosity normal \ - --filter="Category!=Platform-Android" + --report-xunit \ + --report-xunit-html \ + --report-xunit-html-filename "test-results.html" + TEST_EXIT_CODE=$? + echo "Test exit code: $TEST_EXIT_CODE" + if [ $TEST_EXIT_CODE -ne 0 ]; then + echo "❌ Tests failed with exit code $TEST_EXIT_CODE" + exit $TEST_EXIT_CODE + else + echo "✅ All tests passed successfully." + fi diff --git a/.gitignore b/.gitignore index 056cf69d2..32d9c064d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,8 @@ DataDir/ *.log +# gitleaks scan output (local secret-scan report, not for the repo) +/gitleaks-report.json + +# Exclude test reports (local test report, not for the repo) +/test-reports diff --git a/.vscode/launch.json b/.vscode/launch.json index d5088f01d..87cf40dbb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,11 +22,9 @@ }, { "name": "Android Debug", - "type": "mono", - "preLaunchTask": "run-debug-android", - "request": "attach", - "address": "localhost", - "port": 55555 + "type": "maui", + "request": "launch", + "project": "${workspaceFolder}/src/PostIt/PostIt.Android/PostIt.Android.csproj" }, { "name": "Android Attach - Debug", @@ -43,9 +41,15 @@ }, { "name": "Yavsc Org", - "type": "dotnet", + "type": "coreclr", "request": "launch", - "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj", + "program": "${workspaceFolder}/src/Yavsc.Org/bin/Debug/net10.0/Yavsc.Org.dll", + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "cwd": "${workspaceFolder}/src/Yavsc.Org", + "preLaunchTask": "build-web-yavsc-org" + }, { "name": "Yavsc Blogs", diff --git a/.vscode/settings.json b/.vscode/settings.json index 83a17ae38..e6f2aee1d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,5 +45,10 @@ ] } }, - "dotnet.defaultSolution": "yavsc.sln" + "dotnet.defaultSolution": "yavsc.sln", + "axaml.selectedSolution": "/home/paul/Workspace/yavsc/yavsc.sln", + "chat.tools.terminal.autoApprove": { + "dotnet build": true, + "dotnet test": true + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 71384cd8c..0b0bb63a5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,27 +1,17 @@ { "version": "2.0.0", "isRoot": true, - "problemMatcher": [ - { - "owner": "dotnet", - "fileLocation": ["relative", "${workspaceFolder}"], - "source": "dotnet", - "pattern": { - "regexp": "^\\s*(.*)\\((\\d+),(\\d+)\\):\\s+(error|warning) (.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "code": 5, - "message": 6 - } - } - ], + "tasks": [ { - "label": "run-debug-android", + "label": "build-debug-android", "command": "dotnet", + "detail": "Build the PostIt Android project in debug mode", "type": "shell", + "runOptions": { + + }, + "problemMatcher": ["$msCompile"], "options": { "cwd": "${workspaceFolder}/src/PostIt/PostIt.Android", "env": { @@ -43,6 +33,7 @@ "label": "build", "command": "dotnet", "type": "process", + "problemMatcher": ["$msCompile"], "args": ["build", "/property:GenerateFullPaths=true"], "group": "build", "isBuildCommand": true, @@ -102,6 +93,34 @@ }, "isBackground": true }, + { + "label": "build-web-yavsc-org", + "type": "process", + "problemMatcher": ["$msCompile"], + "command": "dotnet", + "args": ["build"], + "options": { + "cwd": "src/Yavsc.Org" + }, + "group": { + "kind": "build" + }, + "isBackground": true + }, + { + "label": "build-web-yavsc-blogs", + "type": "process", + "problemMatcher": ["$msCompile"], + "command": "dotnet", + "args": ["build"], + "options": { + "cwd": "src/Yavsc.Blogs" + }, + "group": { + "kind": "build" + }, + "isBackground": true + }, { "label": "dotnet: build-postit-desktop", "type": "process", @@ -109,6 +128,7 @@ "isTestCommand": false, "isBackground": true, "command": "dotnet", + "problemMatcher": ["$msCompile"], "args": ["build", "/property:GenerateFullPaths=true"], "options": { "cwd": "src/PostIt/PostIt.Desktop" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f8253c70..e36d75dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,251 @@ # Changelog + +## [1.0.8-rc19] - Blogs UX + +### Fixed + +La saisie du billet est maintenant plutôt possible, +en petit écran. + +## [1.0.8-rc18] - unstable + +* [PostIt] Page "About" avec lines vers le dépôt et les issues Forgejo + +* Accès aux fichiers personnels PostIt rendu fonctionnel de bout en bout +(arbre de stockage indexé par le login, pas par le `sub`), boutons +d'action des listes réparés (binding `$parent`), revendication `name` +enfin présente dans les access tokens (branchement du `ProfileService`), +et négociation HTTP/3 côté client PostIt. + + +### Added + +* [PostIt] Négociation HTTP/3 (QUIC) pour les appels API Yavsc : + `DefaultRequestVersion = 3.0`, `DefaultVersionPolicy = + RequestVersionOrLower` sur le `HttpClient` partagé de `YavscApiClient`, + avec repli h2/h1.1 où QUIC n'est pas disponible (WASM fetch, Android/ + desktop sans libmsquic). +* [Tests] `MyFilesPageHeadlessTests` : monte `MyFilesPage` via + `NavigationPage` + `Window.Show()` et vérifie que chaque bouton de + ligne (Télécharger / Supprimer) a sa `Command` liée, est armé + (`CanExecute`) et déclenche l'appel API au clic. +* [Tests] `Remoting.PasswordToken_carries_name_claim_equal_to_login` : + mint un vrai JWT via le token endpoint IdentityServer (password grant, + `openid profile`) et asserte que l'access token porte `name`=login et + que `GetUserName()` résout vers ce login. +* [Tests] `FileSystemApiTests.Get_fs_lists_files_stored_under_the_user_login` : + découple `sub` et login, téléverse sous le login, et asserte que + `GET /api/v1/fs` retourne le fichier (et que le répertoire nommé d'après + le `sub` n'existe pas). + +### Changed + +* [Yavsc.Org] Branchement du `ProfileService` custom + (`AddProfileService()` après + `AddAspNetIdentity()`). La classe était du code mort : + IdentityServer8 retombait sur son profile service par défaut, qui + n'émet que les claims listés dans les `UserClaims` des API resources. + `name` n'en faisait jamais partie, donc tout access token était sans + `name` et `GetUserName()` résolvait à `null` sur les hosts ressources + (Blogs/Api) — cause du 500 sur l'upload MyFiles et du + `User.Identity.Name` injoignable. Le service émet désormais + `name`=`user.UserName`, `email` et les rôles pour tout client dont + les `AllowedScopes` contiennent `profile`, indépendamment du caller. +* [Yavsc.Server] `ServiceExtensions` : `NameClaimType = + JwtClaimTypes.Name`, `MapInboundClaims = false` (conserver les noms + courts de claims tels qu'émis). +* [Yavsc.Blogs] `FileSystemHelpers.EnsureDestinationDirectory` utilise + `GetUserName()` (claim `name`) au lieu de `User.Identity.Name` pour + la racine de stockage par utilisateur. +* [Tests] `WebServerFixture` : `AccessTokenType = 0` (Jwt dans IS8 ; + 1 = Reference, l'inverse de Duende) ; `openid`+`profile` sur le client + de test ; réensemencement des identity resources openid/profile dans + `ReseedAuthTestData` (un `ResetAndMigrateDatabase` vide le store + in-memory partagé et les laissait absentes → `invalid_scope`). +* [Tests] `TestTokenIssuer.Issue` gagne un paramètre `name` optionnel + (défaut = subject) pour refléter la forme production où + `sub` (GUID) ≠ `name` (login). +* [Tests] `BillingServiceTests` : `[Collection("Yavsc Server")]` pour + sérialiser avec le build d'hôte qui mute les registres statiques + `BillingService`/`Config` (race parallèle pré-existante). + +### Fixed + +* [Yavsc.Blogs] La liste `GET /api/v1/fs` cherchait sous + `{Blog}/{sub-GUID}` (`User.GetUserId()`), alors que l'arbre de + stockage perso est indexé par le login partout ailleurs + (`EnsureDestinationDirectory` écrit sous `{Blog}/{name-claim}`, + le endpoint de téléchargement et les avatars utilisent + `user.UserName`). Une fois l'upload corrigé, les fichiers + atterrissaient sous `{Blog}/{login}` tandis que la liste cherchait + sous `{Blog}/{GUID}` — un arbre vide — d'où la page PostIt + « Mes fichiers » qui ne listait rien. `GetDir` résout désormais + l'utilisateur en base et liste sous `user.UserName`. + `UploadedFile.OwnerId` reste `sub`, donc `EnrichWithFileIds` + interpole toujours par `uid`. +* [PostIt] Boutons d'action des lignes de liste grisés et inactifs au + clic (Télécharger / Supprimer / Joindre sur MyFiles ; download/detach + sur les listes de pièces jointes de `BillingQueryDetailsPage` et + `EstimateEditionPage`). Le binding `$parent[vm:XxxViewModel].Cmd` + cherchait un ancêtre CONTRÔLE de type `XxxViewModel` — un viewmodel, + pas un contrôle — donc résolvait à `null` et le `Button` restait + désactivé (grisé) et inerte au clic. Corrigé en + `$parent[ContentPage].((vm:XxxViewModel)DataContext).Cmd` (syntaxe + déjà en vigueur dans `CirclesPage` / `EstimateListPage`). +* [PostIt] Les messages de la barre de statut (`StatusBar`) n'étaient + pas sélectionnables/copiables : le texte était un `TextBlock` (non + sélectionnable dans Avalonia). Remplacé par `SelectableTextBlock`. +* [Yavsc.Blogs] La suppression d'un fichier (`DELETE api/v1/fs/{name}`) + renvoyait un 40x alors que le fichier était bien supprimé du disque. + `DeleteUserDirOrFile` (et `DeleteUserFile`) lisaient + `FileInfo.Length` **après** `fi.Delete()` ; or `FileInfo.Length` + lève `FileNotFoundException` une fois le fichier disparu — le `catch` + du contrôleur transformait cette exception en 400, d'où le message + perçu « le fichier n'existe pas sur le serveur » tandis que la + suppression avait réussi. La taille est désormais capturée avant la + suppression. +* PostIt default settings + +## [1.0.8-rc17] - unstable + +Fichiers utilisateur et pièces jointes sur les devis/demandes (PostIt + +API), système de génération `contrib` (vhosts nginx HTTP/3 edge + +appsettings jq + QUIC logiciel Kestrel), et correction du bug de +production où `IsPublished` disparaissait du payload de liste du blog API. + +### Added + +* [PostIt] Espace de stockage personnel (« MyFiles ») : page de gestion + des fichiers de l'utilisateur, entrée depuis la page d'accueil. +* [PostIt] Pièces jointes aux devis et aux demandes : attachement depuis + le détail de demande et la page d'édition de devis. +* [Yavsc.Api] Endpoints d'attachement de fichiers sur les devis + (`EstimateApiController`) et les demandes front-office + (`FrontOfficeApiController`). +* [Yavsc.Api.Client] `UserFilesApiClient` + DTOs associés ; méthodes + d'attachement sur `EstimateApiClient` et `FrontOfficeApiClient`. +* [Yavsc.Server] Modèles `EstimateAttachedFile`, `QueryAttachedFile`, + `BlogAttachedFile`, `AttachFileRequest` ; migration EF Core + `UserFileAttachments`. +* [contrib] Système de génération `Makefile` : vhosts nginx HTTP/3 edge + (`template.vhost`, `QUIC_OPTS`), appsettings générés via jq + (`template.appsettings-{org,api,blogs}.json`, échappement JSON-safe des + secrets), QUIC logiciel Kestrel (`Http1AndHttp2AndHttp3`), et + `README.nginx.md`. +* [Tests] Projet `Yavsc.Server.Tests` : tests du service + `BlogSpotService.Details` + `ServerServicesFixture` (SQLite `:memory:` + par défaut, Npgsql opt-in via `YAVSC_SERVER_TEST_DB_PROVIDER`). +* [Tests] `IBlogPostWireShapeTests` (forme du wire blog), + `EstimateAttachmentsApiTests`, `FrontOfficeQueryAttachmentsApiTests`, + `FileSystemApiTests` (pièces jointes et stockage perso). +* [CI] Intégration Gitleaks / analyse de secrets (`.pre-commit-config.yaml`). + +### Changed + +* [contrib] `QUIC_OPTS` (`reuseport`) désormais activé uniquement en + production sur le vhost Org. La preprod et la prod partageant la même + IP, `reuseport` — légal une seule fois par `addr:port` (443/udp) — + serait sinon en conflit entre preprod-Org et prod-Org et ferait refuser + la config par nginx. +* [Yavsc.Api.Test] `ApiWebServerFixture` : la connexion admin PostgreSQL + provient de la variable d'environnement + `YAVSC_API_TEST_NPGSQL_ADMIN_CONNECTION` ; plus de mot de passe en + source. + +### Fixed + +* [Yavsc.Blogs] Bug de production : `IsPublished` était absent du payload + de la liste du blog API. Le endpoint `GetBlogspot` retourne + `IEnumerable` sérialisé par System.Text.Json d'après le type + déclaré (l'interface), qui omet les propriétés du type concret — + `IsPublished` vivait sur `BlogPost` / `BlogPostDto` mais pas sur + `IBlogPost`. PostIt lisait donc `IsPublished == false` pour tout billet, + y compris publiés (https://yavsc.pschneider.fr/BlogSpot/Details/13). + Corrigé en déclarant `IsPublished` sur `IBlogPost`. +* [Yavsc.Org] Accès au Swagger UI : les actions MVC scaffoldées de + `CommentsController` (Index/Details/…) atterrissaient sur la même route + `POST api/v1/blogcomments` que l'action d'API `Post`, provoquant une + `SwaggerGeneratorException` (conflit method/path) au chargement de + `/swagger`. Corrigé par `[ApiExplorerSettings(IgnoreApi = true)]` sur + les actions scaffoldées. + +## [1.0.8-rc16] - unstable + +Flux de signature et de génération de devis : un devis peut désormais être +signé par les deux parties (fournisseur et client) puis téléchargé en PDF +ou TeX depuis PostIt, avec les signatures dessinées en vecteur. + +### Added + +* [PostIt] Pad de signature sur la page de validation d'un devis : on valide + ou on refuse en joignant une signature (côté fournisseur ou client), capturée + au format filaire PostIt (strokes) et envoyée sur l'acceptation. +* [Yavsc.Api] Finalisation de `POST /api/v1/front/query/accept` et `.../reject` : + autorisation selon le rôle (fournisseur/client/admin), signature optionnelle + persistée sur l'estimate avec horodatage `ProviderValidationDate` / + `ClientValidationDate` selon le côté signataire. +* [Yavsc.Api] `QueryStatus.Accepted` éclaté en `ProAccepted` / `ClientAccepted` + pour distinguer l'acceptation de chaque partie. +* [Yavsc.Api] Endpoints de devis `GET /api/v1/front/query/{id}/estimate.tex` et + `estimate.pdf` (clé par id de query). Le flux de génération TeX/PDF est + rapatrié de Yavsc.Org vers Yavsc.Api (`TeXHelpers`, `PdfGenerationViewModel`, + `BillViewComponent`, modèles Bill/Estimate, `_ViewImports` ; `Program.cs` + câble `AddControllersWithViews` + `SiteSettings`). +* [Yavsc.Api] Signatures du devis dessinées en TikZ vectoriel + (`TeXHelpers.SignatureToTikz`) en bas du document, fournisseur et client + côte à côte dans deux minipages ; plus de dépendance à un PNG de signature. +* [Yavsc.Api.Client] `DownloadAsync` sur le client + `GetEstimateTexAsync` / + `GetEstimatePdfAsync` ; boutons « Télécharger PDF » / « Télécharger TeX » + dans PostIt sur la page de validation du devis et le détail de commande, + avec un helper `StorageProvider` d'enregistrement de fichier. +* [PostIt] La liste des devis se rafraîchit au retour de navigation (pop-back), + de sorte qu'un devis validé quitte immédiatement la liste du signataire. +* [PostIt] Renommage des titres de liste : « Devis en attente de signature + fournisseur » / « Devis en attente de signature client ». +* [Yavsc.Abstract] `SignatureType` déplacé dans `Yavsc.Abstract.Billing`. +* [CLI] Option `-c configuration-file.json` au lancement. +* [Tests] `EstimatePdfGenerationTests` : tests unitaires du pipeline + signature → TikZ → PDF (devis signé des deux côtés), assertant que le PDF + existe au chemin de sortie attendu et que seul le PDF persiste (aux/log + nettoyés) ; skip propre si `lualatex` est absent. +* [README] Dépendances système au runtime pour la génération PDF : + `texlive-binaries` (lualatex), `texlive-luatex` (luaotfload), + `texlive-pictures` (tikz), `texlive-fonts-extra` (bera). + +### Changed + +* [Yavsc.Api] `GetOngoingEstimatesAsProvider` filtre désormais sur + `ProviderValidationDate` : un devis signé par le fournisseur quitte sa liste + « en attente ». +* [Yavsc.Api] La génération PDF compile via le `lualatex` système, le TeX fourni + sur l'entrée standard (`/dev/stdin`), le PDF écrit directement dans le + répertoire des factures (`Site.Bills`), les artefacts `.aux`/`.log` nettoyés + afin que seul le `.pdf` persiste. + +### Fixed + +* [Yavsc.Api] `Emergency stop` à la génération PDF : `lualatex` sans argument + fichier lit stdin en mode terminal et s'arrête après la première ligne d'un + document multi-lignes. Corrigé en passant `/dev/stdin` comme nom de fichier + d'entrée — stdin est alors traité comme un fichier régulier et le document + multi-lignes compile. C'était la cause racine de l'échec en production, pas + `bera` ni `luaotfload` (problèmes réels mais séparés, comblés côté paquets). +* [Yavsc.Api] `I can't write on file 'estimate-1.log'` : le chemin relatif + `Site.Bills` (`"bills"`) était imbriqué quand il servait à la fois de + `-output-directory` et de répertoire de travail ; résolu en chemin absolu. +* [Yavsc.Api] NRE dans `RenderViewToString` (RouteData / `ViewData.Model` + nuls) : reconstruit sur le `ControllerContext` du contrôleur ; `IViewEngine` + résolu via `IRazorViewEngine` (.NET 10 n'enregistre pas de `IViewEngine` + unique). +* [Yavsc.Api] `Layout = "null"` (chaîne) dans les modèles TeX cherchait + `null.cshtml` ; corrigé en `Layout = null` (null C#). +* [Yavsc.Api] `texi2pdf` (outil Texinfo, inadapté au LaTeX) remplacé par + `lualatex`. +* [Yavsc.Api] Les diagnostics lualatex étaient perdus (écrits dans le `.log` + qu'on supprimait) ; stdout/stderr sont désormais capturés dans le message + d'erreur, et le `.log` est conservé en cas d'échec. + ## [1.0.8-rc15] - unstable ### Added @@ -369,6 +615,10 @@ the ACL now comes along with the article, the same user-visible switch without a schema change. [Unreleased]: https://forgejo.pschneider.fr/notazof/yavsc/compare/HEAD +[1.0.8-rc16]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.8-rc15...1.0.8-rc16 +[1.0.8-rc17]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.8-rc16...1.0.8-rc17 +[1.0.8-rc18]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.8-rc17...1.0.8-rc18 +[1.0.8-rc15]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.8-rc14...1.0.8-rc15 [1.0.8-rc1]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.7...1.0.8-rc1 [1.0.7]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.6...1.0.7 [1.0.6]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.5...1.0.6 diff --git a/Directory.Packages.props b/Directory.Packages.props index feea66ec9..b0b055b98 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,44 +6,69 @@ - + - - - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/Dockerfile b/Dockerfile index 795b70ab3..8ebbb73f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,41 +24,24 @@ # ===================================================================== # syntax=docker/dockerfile:1.7 -ARG BUILD_ENV_TAG=debian12-dotnet10-android36-v1 +ARG BUILD_ENV_TAG=debian13-dotnet10-android36-jdk21-v2 FROM pazof/yavsc-build-env:${BUILD_ENV_TAG} AS build-env WORKDIR /src -# (1) Manifest + fichiers de projet pour le cache de restore -COPY *.sln ./ -COPY Directory.*.props ./ -COPY src/Yavsc.Org/*.csproj ./src/Yavsc.Org/ -COPY src/Yavsc.Abstract/*.csproj ./src/Yavsc.Abstract/ -COPY src/Yavsc.Server/*.csproj ./src/Yavsc.Server/ -COPY src/Yavsc.Api/*.csproj ./src/Yavsc.Api/ -COPY src/Yavsc.Blogs/*.csproj ./src/Yavsc.Blogs/ -COPY src/cli/*.csproj ./src/cli/ -COPY src/Yavsc.Org.Tests/*.csproj ./src/Yavsc.Org.Tests/ -COPY src/PostIt/PostIt/*.csproj ./src/PostIt/PostIt/ -COPY src/PostIt/PostIt.Android/*.csproj ./src/PostIt/PostIt.Android/ -COPY src/PostIt/PostIt.Browser/*.csproj ./src/PostIt/PostIt.Browser/ -COPY src/PostIt/PostIt.Desktop/*.csproj ./src/PostIt/PostIt.Desktop/ - -# (2) Tout le code source COPY . . -# (4) Restore RUN dotnet restore -# (5) Build des trois services web principaux +# Build des trois services web principaux RUN dotnet build src/Yavsc.Org/Yavsc.Org.csproj -c Release --no-restore -clp:ErrorsOnly RUN dotnet build src/Yavsc.Api/Yavsc.Api.csproj -c Release --no-restore -clp:ErrorsOnly RUN dotnet build src/Yavsc.Blogs/Yavsc.Blogs.csproj -c Release --no-restore -clp:ErrorsOnly -# (6) Build APK Android (utilisé par le workflow docker-publish-android). +# Build APK Android (utilisé par le workflow docker-publish-android). ARG ANDROID_TARGET_RID=android-arm64 RUN dotnet build src/PostIt/PostIt.Android/PostIt.Android.csproj -c Release --no-restore -clp:ErrorsOnly -r ${ANDROID_TARGET_RID} -# (7) Publish des trois services web (les artifacts sont consommés par +# Publish des trois services web (les artifacts sont consommés par # les stages publish-org / publish-api / publish-blogs ci-dessous). RUN mkdir -p /app/publish RUN dotnet publish src/Yavsc.Org/Yavsc.Org.csproj -c Release --no-build -o /app/publish/Yavsc.Org diff --git a/Makefile b/Makefile index d99fe5e5e..1893a910e 100644 --- a/Makefile +++ b/Makefile @@ -13,11 +13,37 @@ all: clean: dotnet clean -c $(CONFIG) +brutal_clean: + @find -name "obj" -exec rm -rf {} \; + @find -name "bin" -exec rm -rf {} \; + src/Yavsc/bin/output/wwwroot: dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish test: - ASPNETCORE_ENVIRONMENT=Development dotnet test + ASPNETCORE_ENVIRONMENT=Development dotnet test --report-xunit \ + --report-xunit-html \ + --results-directory "test-reports" + +test-postit: + ASPNETCORE_ENVIRONMENT=Development dotnet test \ + --report-xunit \ + --report-xunit-html \ + --results-directory "test-reports" \ + --report-xunit-html-filename "postit-test-results.html" \ + --project src/PostIt/PostIt.Tests/PostIt.Tests.csproj + +backend-tests: Blogs-backend-test Org-backend-test Api-backend-test +Api-backend-test: +Blogs-backend-test: +Org-backend-test: +%-backend-test: + ASPNETCORE_ENVIRONMENT=Development dotnet test \ + --report-xunit \ + --report-xunit-html \ + --results-directory "test-reports" \ + --report-xunit-html-filename "$*-backend-test-results.html" \ + --project src/test/Yavsc.$*.Tests/Yavsc.$*.Tests.csproj watch: dotnet watch -p:Configuration=$(CONFIG) --project src/Yavsc/Yavsc.csproj diff --git a/README.md b/README.md index 549348f4a..fefbdd48d 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,40 @@ make test ### les services et l'API +#### Dépendances système au runtime + +La génération des devis et factures en PDF (`/api/v1/front/query/{id}/estimate.pdf`, +`BillingController` → `BillViewComponent`) compile un source LaTeX via la commande +système `lualatex` : le TeX est fourni sur l'entrée standard et le PDF est écrit +directement dans le répertoire des factures (`Site.Bills`). `lualatex` doit donc +être installé et opérationnel sur l'hôte qui exécute l'API — typiquement le service +systemd `yavscApi`. + +Sous Debian/Ubuntu : + +```bash +sudo apt install texlive-binaries texlive-luatex texlive-pictures texlive-fonts-extra +``` + +- `texlive-binaries` fournit l'exécutable `lualatex` (collection basic). +- `texlive-luatex` fournit les paquets de support chargés au démarrage de lualatex, + notamment `luaotfload`. Sans lui, lualatex démarre mais s'arrête sur + `module 'luaotfload-main' not found … Emergency stop` et ne produit aucun PDF. + (Sur les Debian récents, `luaotfload` a migré de `texlive-latex-extra` vers + `texlive-luatex` — avoir `texlive-latex-extra` seul ne suffit plus.) +- `texlive-pictures` fournit `tikz`/`pgf`, utilisé pour dessiner les signatures + du client et du fournisseur en bas du devis (vecteur, pas de PNG). +- `texlive-fonts-extra` fournit la fonte `bera` chargée par le modèle de devis. + Sans elle, lualatex s'arrête sur `! LaTeX Error: File 'bera.sty' not found` + (`Emergency stop`, aucun PDF). + +Sur un hôte sans `lualatex`, les endpoints PDF renvoient une erreur 500 « PDF +generation failed » ; l'endpoint TeX (`estimate.tex`) reste utilisable et permet +de compiler le document hors-ligne. + ### La Prod -`cd srv/Yavsc` : `make pushInProd CONFIGURATION=Release`. +`cd srv/Yavsc/contrib` : `make reinstall CONFIGURATION=Release APP_INSTALL_ENV=production`. puis, pour une première installation `make install_service`. diff --git a/contrib/Makefile b/contrib/Makefile index 4e4ef4df6..deb55bcc6 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -1,23 +1,32 @@ +DESTDIR= +DOTNET_FRAMEWORK=net10.0 APP_PROJECT_NAMES=Org Blogs Api SLNDIR=.. +APP_INSTALL_ENV=preprod +APP_INSTALL_PREFIX=preprod- -include $(SLNDIR)/.env -include .env +include .$(APP_INSTALL_ENV).env -generated/: - @mkdir -p $@ +reinstall: copy-binaries + @for project in $(APP_PROJECT_NAMES); do \ + echo "🚀 Starting $(APP_INSTALL_PREFIX)yavsc$${project}" ; \ + sudo systemctl start $(APP_INSTALL_PREFIX)yavsc$${project} ; done + @echo "💯 backend started" -generated/yavscOrg.service: -generated/yavscBlogs.service: -generated/yavscApi.service: +reinstall-prod: + make reinstall APP_INSTALL_ENV= APP_INSTALL_PREFIX= APP_INSTALL_ENV=production -generated/yavsc%.service: generated/ template.service $(SLNDIR)/.env +generated/$(APP_INSTALL_PREFIX)yavscOrg.service: +generated/$(APP_INSTALL_PREFIX)yavscBlogs.service: +generated/$(APP_INSTALL_PREFIX)yavscApi.service: + +generated/$(APP_INSTALL_PREFIX)yavsc%.service: generated/ template.service $(SLNDIR)/.env @cat template.service | APP_NAME="$*" \ DESTDIR="$(DESTDIR)" \ HTTP_HOST="$(HTTP_HOST)" \ - HTTP_PORT="$*_$(HTTP_PORT)" \ - BASEAPPDIR="$(BASEAPPDIR)" \ - APP_SYSLOG_ID="yavsc$*" \ + HTTP_PORT="$($*_PORT)" \ + APPDIR="$(APPDIR)" \ + APP_SYSLOG_ID="$(APP_INSTALL_PREFIX)yavsc$*" \ POSTGRES_HOST="$(POSTGRES_HOST)" \ POSTGRES_PORT="$(POSTGRES_PORT)" \ POSTGRES_DB="$(POSTGRES_DB)" \ @@ -34,11 +43,83 @@ generated/yavsc%.service: generated/ template.service $(SLNDIR)/.env envsubst > $@ @echo Created service file: $@ +generate-service-units: generated/$(APP_INSTALL_PREFIX)yavscOrg.service generated/$(APP_INSTALL_PREFIX)yavscBlogs.service generated/$(APP_INSTALL_PREFIX)yavscApi.service -copy-services: copy-service-Org copy-service-Blogs copy-service-Api -copy-service-Org: /etc/systemd/system/yavscOrg.service -copy-service-Blogs: /etc/systemd/system/yavscBlogs.service -copy-service-Api: /etc/systemd/system/yavscApi.service +# --- Vhosts nginx (générés par envsubst, à l'image des unités systemd) --- +# make generate-nginx -> generated/nginx/preprod-yavsc{Org,Blogs,Api} +# make generate-nginx APP_INSTALL_ENV=production APP_INSTALL_PREFIX= -> prod (yavsc/api/blogs.pschneider.fr) +# +# HTTP/3 (QUIC) termine au edge nginx (plan contrib/quic.md). Requiert nginx +# avec http_v3_module + UDP 443 ouvert. reuseport sur Org uniquement +# (QUIC_OPTS). Les vhosts générés vont dans generated/nginx/ (gitignoré). +NGINX_TEMPLATE := template.vhost +NGINX_OUT_DIR := generated/nginx +NGINX_HOSTS := Org Blogs Api +DOMAIN_ROOT ?= pschneider.fr +# Schéma backend Kestrel : https en prod (Kestrel écoute HTTPS, cert Let's +# Encrypt), http en preprod (Kestrel écoute HTTP simple, cf. .preprod.env). +BACKEND_SCHEME ?= $(if $(filter production,$(APP_INSTALL_ENV)),https,http) +# sous-domaine par hôte : Org -> yavsc, Api -> api, Blogs -> blogs +nginx_subdomain = $(if $(filter Org,$1),yavsc,$(if $(filter Api,$1),api,$(if $(filter Blogs,$1),blogs,$1))) +# domaine complet : . +# prod Org -> yavsc.pschneider.fr +# preprod Org -> preprod-yavsc.pschneider.fr +nginx_domain = $(APP_INSTALL_PREFIX)$(call nginx_subdomain,$1).$(DOMAIN_ROOT) + +# QUIC_OPTS : "reuseport" n'est légal qu'une seule fois par addr:port +# (443/udp). preprod et prod partagent aujourd'hui la même IP, donc on ne +# l'active qu'en production, sur Org uniquement. En preprod, QUIC_OPTS est +# vide sur tous les vhosts (sinon nginx refuse la config : duplicate +# reuseport sur le même socket 443/udp entre preprod-Org et prod-Org). +$(NGINX_OUT_DIR)/$(APP_INSTALL_PREFIX)yavsc%: $(NGINX_OUT_DIR) $(NGINX_TEMPLATE) .$(APP_INSTALL_ENV).env + @cat $(NGINX_TEMPLATE) \ + | DOMAIN="$(call nginx_domain,$*)" \ + BACKEND_SCHEME="$(BACKEND_SCHEME)" \ + HTTP_HOST="$(HTTP_HOST)" \ + BACKEND_PORT="$($*_PORT)" \ + QUIC_OPTS="$(if $(filter production,$(APP_INSTALL_ENV)),$(if $(filter Org,$*), reuseport,),)" \ + envsubst '$$DOMAIN $$BACKEND_SCHEME $$HTTP_HOST $$BACKEND_PORT $$QUIC_OPTS' > $@ + @echo Created nginx vhost: $@ + +generate-nginx: $(addprefix $(NGINX_OUT_DIR)/$(APP_INSTALL_PREFIX)yavsc,$(NGINX_HOSTS)) + +generate-nginx-prod: + make generate-nginx APP_INSTALL_ENV=production APP_INSTALL_PREFIX= + +$(NGINX_OUT_DIR): + @mkdir -p $@ + +# --- appsettings (générés depuis template.appsettings-*.json) --- +# make generate-appsettings -> generated/appsettings/appsettings-{org,api,blogs}.json +# +# Substitue $DOMAIN (racine du domaine, cf. DOMAIN_ROOT) dans toutes les +# chaînes, et $SMTP_PASSWORD (secret du .env) dans Smtp.Password. +# On utilise jq (et non envsubst) car envsubst n'échappe pas le JSON : un +# secret contenant \ ou " casserait le JSON (le mot de passe SMTP actuel +# contient un \). jq lit le secret depuis l'environnement (export) -> +# échappement JSON correct, aucun shell-quoting ni exposition en ligne de cmd. +# Les templates actuels sont issus de la preprod (préfixe preprod-, ports +# 88-93) ; pour la prod il faudra un template prod (ports 82-87, sans préfixe). +export ASPNETCORE_Smtp__Password +APPSETTINGS_OUT_DIR := generated/appsettings +APPSETTINGS_HOSTS := org api blogs + +$(APPSETTINGS_OUT_DIR)/appsettings-%.json: $(APPSETTINGS_OUT_DIR) template.appsettings-%.json .$(APP_INSTALL_ENV).env + @jq --arg domain "$(DOMAIN_ROOT)" \ + 'walk(if type=="string" then gsub("\\$$DOMAIN"; $$domain) else . end) | if has("Smtp") then .Smtp.Password = env["ASPNETCORE_Smtp__Password"] else . end' \ + template.appsettings-$*.json > $@ + @echo Created appsettings: $@ + +generate-appsettings: $(addprefix $(APPSETTINGS_OUT_DIR)/appsettings-,$(addsuffix .json,$(APPSETTINGS_HOSTS))) + +$(APPSETTINGS_OUT_DIR): + @mkdir -p $@ + +install-service-%: /etc/systemd/system/$(APP_INSTALL_PREFIX)yavsc%.service + sudo systemctl daemon-reload + sudo systemctl enable $(APP_INSTALL_PREFIX)yavsc$* + +install-services: install-service-Org install-service-Blogs install-service-Api copy-binaries: build_publish_Org build_publish_Blogs build_publish_Api stop-services @for project in $(APP_PROJECT_NAMES); \ @@ -52,39 +133,36 @@ copy-binaries: build_publish_Org build_publish_Blogs build_publish_Api stop-serv rm -f $${BINDIR}/appsettings-$${LCAPI}.json ; \ sync ; \ ls $${BINDIR}/appsettings* 2>&1 ; \ - sudo cp -a $${BINDIR}/* $(BASEAPPDIR) ; \ + sudo cp -a $${BINDIR}/* $(DESTDIR)$(APPDIR) ; \ done - @sudo chown -R $(USER_AND_GROUP) $(BASEAPPDIR) + @sudo chown -R $(USER_AND_GROUP) $(DESTDIR)$(APPDIR) + @sync -/etc/systemd/system/yavsc%.service: generated/yavsc%.service +$(DESTDIR)/etc/systemd/system/$(APP_INSTALL_PREFIX)yavsc%.service: generated/$(APP_INSTALL_PREFIX)yavsc%.service sudo cp $^ $@ sudo chown root:root $@ build_publish_%: clean_publish_dir_% @ASPNETCORE_ENV=$(CONFIGURATION) dotnet publish $(SLNDIR)/src/Yavsc.$*/Yavsc.$*.csproj -build_publish: build_publish_Org build_publish_Blogs build_publish_Api - clean_publish_dir_%: @rm -rf $(SLNDIR)/src/Yavsc.$*/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish -install: build_publish copy-binaries copy-services +install: copy-binaries copy-services @sudo systemctl daemon-reload @for project in $(APP_PROJECT_NAMES); \ do \ - sudo systemctl enable yavsc$${project} ; \ - sudo systemctl start yavsc$${project} ; \ + echo "🖒 enable $(APP_INSTALL_PREFIX)yavsc$${project}" ; \ + sudo systemctl enable $(APP_INSTALL_PREFIX)yavsc$${project} ; \ + echo "🚀 start $(APP_INSTALL_PREFIX)yavsc$${project}" ; \ + sudo systemctl start $(APP_INSTALL_PREFIX)yavsc$${project} ; \ done -reinstall: copy-binaries - @sync - @for project in $(APP_PROJECT_NAMES); do \ - sudo systemctl start yavsc$${project} ; done - stop-services: @for project in $(APP_PROJECT_NAMES); \ do \ - sudo systemctl stop yavsc$${project} ; \ + echo "🔥 Stopping $(APP_INSTALL_PREFIX)yavsc$${project}" ; \ + sudo systemctl stop $(APP_INSTALL_PREFIX)yavsc$${project} ; \ done $(SLNDIR)/src/Yavsc.Org/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_publish @@ -93,7 +171,7 @@ $(SLNDIR)/src/Yavsc.Api/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_ showConfig: @echo CONFIGURATION: $(CONFIGURATION) - @echo BASEAPPDIR: $(BASEAPPDIR) + @echo APPDIR: $(APPDIR) showApiLogs: @sudo journalctl -u yavscApi.service -S "2 min ago" | tee yavscApi.log @@ -107,3 +185,5 @@ showBlogsLogs: clean: @rm -rf generated +generated/: + @mkdir -p $@ diff --git a/contrib/README.nginx.md b/contrib/README.nginx.md new file mode 100644 index 000000000..9564d4ffe --- /dev/null +++ b/contrib/README.nginx.md @@ -0,0 +1,134 @@ +# Vhosts nginx — génération par `make generate-nginx` + +Les vhosts nginx sont **générés** à l'image des unités de service systemd +(`template.service` + `envsubst` → `generated/*.service`) : un template +unique `contrib/template.vhost` est substitué par `envsubst` vers +`contrib/generated/nginx/`, qui est **gitignoré**. + +## Rôle de nginx : edge HTTP/3 (plan `contrib/quic.md`) + +nginx termine HTTP/3 (QUIC) au edge sur 443/udp, en coexistence avec +h1/h2 sur 443/tcp. Kestrel reste backend h1/h2 (`proxy_http_version 1.1`). +Le « QUIC logiciel » Kestrel (`Protocols: Http1AndHttp2AndHttp3` dans les +`template.appsettings-*.json`) est une capacité complémentaire — non +joignable par les clients derrière nginx (nginx OSS = upstream h1.1 +uniquement), mais disponible si Kestrel est un jour exposé en direct. + +Directives générées (par vhost) : +```nginx +listen 443 ssl; listen [::]:443 ssl; http2 on; +listen 443 quic $QUIC_OPTS; listen [::]:443 quic $QUIC_OPTS; http3 on; +add_header Alt-Svc 'h3=":443"; ma=86400' always; +ssl_protocols TLSv1.3; +``` +`$QUIC_OPTS = reuseport` sur **Org uniquement**, vide sur Api/Blogs (règle +`contrib/quic.md` : `reuseport` une seule fois par addr:port, tous vhosts +confondus). + +## Prérequis côté host + +1. **nginx avec `http_v3_module`** (≥ 1.25 ; la directive `http3 on;` + exige ≥ 1.27.4 — sur une version 1.25–1.27.3, retirer `http3 on;`, + `listen ... quic` suffit à activer h3) : + ```bash + nginx -V 2>&1 | grep -o 'http_v3_module' # doit être non vide + ``` + Debian 13 « trixie » fournit `nginx` avec `http_v3_module`. Sinon, + backports ou `nginx-full`. +2. **UDP 443 ouvert** : + ```bash + sudo ufw allow 443/udp + sudo ufw status | grep '443/udp' # → ALLOW + ``` +3. **IPv6** : le template émet `listen [::]:443 ...` (et `[::]:80`). Si le + host n'a pas d'IPv6, retirer/commenter ces lignes (sinon `nginx -t` + échoue sur le bind IPv6). +4. Certbot / certs Let's Encrypt déjà en place (chemins + `/etc/letsencrypt/live//{fullchain,privkey}.pem` référencés). +5. (Optionnel, pour le QUIC logiciel Kestrel) `apt install libmsquic` — + sinon Kestrel désactive h3 silencieusement (fallback h1/h2 gracieux). + +## Génération + +```bash +cd contrib + +# Preprod (défaut) -> generated/nginx/preprod-yavsc{Org,Blogs,Api} +make generate-nginx + +# Production -> generated/nginx/yavsc{Org,Blogs,Api} +make generate-nginx-prod +# ou manuellement : +make generate-nginx APP_INSTALL_ENV=production APP_INSTALL_PREFIX= +``` + +Ports backend et `HTTP_HOST` depuis `.preprod.env` / `.production.env` +(prod Org=83, Api=87, Blogs=85 ; preprod 89/93/91). Schéma backend +`https` en prod, `http` en preprod. + +| Hôte | Sous-domaine | `reuseport` | Backend prod | Backend preprod | +|-------|--------------|-------------|-------------------------|------------------------| +| Org | yavsc | **oui** | `https://localhost:83` | `http://localhost:89` | +| Api | api | non | `https://localhost:87` | `http://localhost:93` | +| Blogs | blogs | non | `https://localhost:85` | `http://localhost:91` | + +Domaines : `.pschneider.fr` +(prod `yavsc.pschneider.fr` ; preprod `preprod-yavsc.pschneider.fr`). + +## Déploiement sur le host + +```bash +sudo cp generated/nginx/yavscOrg /etc/nginx/sites-available/yavscOrg +sudo ln -sf /etc/nginx/sites-available/yavscOrg /etc/nginx/sites-enabled/yavscOrg +# (idem yavscApi, yavscBlogs) +sudo nginx -t && sudo systemctl reload nginx +``` + +## Vérification (sur le host) + +```bash +# 1. Module h3 présent +nginx -V 2>&1 | grep -o 'http_v3_module' + +# 2. UDP 443 ouvert +sudo ufw status | grep '443/udp' + +# 3. En-tête Alt-Svc annoncé +curl -sI https://yavsc.pschneider.fr/ | grep -i alt-svc # → h3=":443" + +# 4. Requête HTTP/3 effective (curl compilé avec HTTP/3) +curl -v --http3 https://yavsc.pschneider.fr/ 2>&1 | grep -i 'HTTP/3' + +# 5. Fallback h2 et h1 toujours fonctionnels +curl --http2 https://yavsc.pschneider.fr/ -o /dev/null -w '%{http_code}\n' +curl --http1.1 https://yavsc.pschneider.fr/ -o /dev/null -w '%{http_code}\n' +``` +Répéter 3-5 pour `api.pschneider.fr` et `blogs.pschneider.fr` (prod) et +les trois domaines preprod. Sans curl HTTP/3, un navigateur : DevTools → +Network → colonne « Protocole » → `h3` après mise à niveau. + +## Comment ça marche (template + envsubst) + +`contrib/template.vhost` contient les placeholders `$DOMAIN`, +`$BACKEND_SCHEME`, `$HTTP_HOST`, `$BACKEND_PORT`, `$QUIC_OPTS`. La règle +`generated/nginx/$(APP_INSTALL_PREFIX)yavsc%` du `Makefile` les substitue +via `envsubst` avec une **allowlist** : + +```makefile +envsubst '$DOMAIN $BACKEND_SCHEME $HTTP_HOST $BACKEND_PORT $QUIC_OPTS' +``` + +Cette allowlist limite la substitution aux 5 variables citées, de sorte +que les variables nginx intrinsèques (`$host`, `$scheme`, `$remote_addr`, +`$proxy_add_x_forwarded_for`, `$request_uri`) survivent intactes. Sans +allowlist, `envsubst` détruirait ces variables nginx. + +## Hors périmètre + +- `docker-compose` / `Dockerfile` : non touchés (déploiement bare-metal + systemd + nginx). +- Backend HTTP/2 vers Kestrel : nginx open-source ne supporte que + HTTP/1.1 en upstream ; on garde `proxy_http_version 1.1`. +- `ssl_protocols TLSv1.3` (suivant `contrib/quic.md`) désactive TLS 1.2 + côté TCP aussi. Pour conserver la compat TLS 1.2 sur le fallback TCP, + relâcher en `ssl_protocols TLSv1.2 TLSv1.3;` (QUIC reste en 1.3). \ No newline at end of file diff --git a/contrib/template.appsettings-api.json b/contrib/template.appsettings-api.json new file mode 100644 index 000000000..b364eeef1 --- /dev/null +++ b/contrib/template.appsettings-api.json @@ -0,0 +1,33 @@ +{ + "Site": { + "Authority": "https://preprod-yavsc.$DOMAIN", + "Audience": ["api"], + "CorsAllowedOrigins": [ + "https://preprod-blogs.$DOMAIN" + ], + "Bills": "bills", + "TempDir": "temp" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://localhost:92" + }, + "Https": { + "Url": "https://localhost:93", + "Certificate": { + "Path": "/etc/letsencrypt/live/preprod-api.$DOMAIN/fullchain.pem", + "KeyPath": "/etc/letsencrypt/live/preprod-api.$DOMAIN/privkey.pem" + }, + "Protocols": "Http1AndHttp2AndHttp3" + } + } + } +} diff --git a/contrib/template.appsettings-blogs.json b/contrib/template.appsettings-blogs.json new file mode 100644 index 000000000..f522feaca --- /dev/null +++ b/contrib/template.appsettings-blogs.json @@ -0,0 +1,40 @@ +{ + "Site": { + "Title": "Yavsc Blogs", + "Description": "A collection of blogs about software development, technology, and programming.", + "Keywords": "software development, technology, programming, coding, blogs", + "Authority": "https://preprod-yavsc.$DOMAIN", + "Audience": ["blogs"], + "CorsAllowedOrigins": [ + "https://preprod-api.$DOMAIN" + ], + "Blog": "blog" + }, + "ConnectionStrings": { + "YavscConnection": "Server=localhost;Port=5432;Database=lame-db-name;Username=lame-user-name;Password=lame-password;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.Authentication": "Debug" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://localhost:90" + }, + "Https": { + "Url": "https://localhost:91", + "Certificate": { + "Path": "/etc/letsencrypt/live/preprod-blogs.$DOMAIN/fullchain.pem", + "KeyPath": "/etc/letsencrypt/live/preprod-blogs.$DOMAIN/privkey.pem" + }, + "Protocols": "Http1AndHttp2AndHttp3" + } + } + } +} diff --git a/contrib/template.appsettings-org.json b/contrib/template.appsettings-org.json new file mode 100644 index 000000000..76203d9e9 --- /dev/null +++ b/contrib/template.appsettings-org.json @@ -0,0 +1,74 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*", + "Site": { + "Title": "yavsc", + "Slogan": "Yavsc!", + "StyleSheet": "/css/default.css", + "Authority": "https://preprod-yavsc.$DOMAIN", + "Audience": [ + "blogs", + "api" + ], + "ExternalUrl": "https://preprod-yavsc.$DOMAIN", + "ApiUrl": "https://preprod-api.$DOMAIN", + "CorsAllowedOrigins": [ + "https://preprod-blogs.$DOMAIN", + "https://preprod-api.$DOMAIN" + ], + "Banner": "/images/banner-dewang.jpg", + "Owner": { + "Name": "Paul Schneider", + "EMail": "paul@pschneider.fr" + }, + "Admin": { + "Name": "Paul Schneider", + "EMail": "paul@pschneider.fr" + }, + "Quota": 200000000, + "Blog": "blog", + "Avatars": "avatars", + "Bills": "bills", + "TempDir": "temp", + "DataDir": "data", + "GitRepository": "git" + }, + "Smtp": { + "Host": "mercure.pschneider.fr", + "Port": 465, + "SenderName": "Paul Schneider", + "SenderEmail": "paul@pschneider.fr", + "UserName": "paul", + "Password": "$SMTP_PASSWORD" + }, + "DataProtection": { + "Keys": { + "Dir": "DataProtection" + }, + "RSAParamFile": "RSA-Params.json", + "ExpiresInHours": 168 + }, + "Moderation": { + "AutoApproveThreshold": 0.7, + "AutoRejectThreshold": 0.9 + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://localhost:88" + }, + "Https": { + "Url": "https://localhost:89", + "Certificate": { + "Path": "/etc/letsencrypt/live/preprod-yavsc.$DOMAIN/fullchain.pem", + "KeyPath": "/etc/letsencrypt/live/preprod-yavsc.$DOMAIN/privkey.pem" + }, + "Protocols": "Http1AndHttp2AndHttp3" + } + } + } +} diff --git a/contrib/template.service b/contrib/template.service index b075c4da1..3959c7151 100644 --- a/contrib/template.service +++ b/contrib/template.service @@ -10,10 +10,10 @@ RestartSec=5s Type=simple User=yavsc Group=yavsc -WorkingDirectory=$DESTDIR -ExecStart=$DESTDIR/Yavsc.$APP_NAME +WorkingDirectory=$APPDIR +ExecStart=$APPDIR/Yavsc.$APP_NAME Restart=always -Environment="HOME=$DESTDIR" +Environment="HOME=$APPDIR" Environment="ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" Environment="ANTHROPIC_MAX_TOKENS=$ANTHROPIC_MAX_TOKENS" Environment="ASPNETCORE_Environment=$ENV" @@ -29,8 +29,6 @@ Environment="ASPNETCORE_Smtp__Password=\"$ASPNETCORE_Smtp__Password\"" CapabilityBoundingSet=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE -StandardOutput=syslog -StandardError=syslog SyslogIdentifier=$APP_SYSLOG_ID [Install] diff --git a/contrib/template.vhost b/contrib/template.vhost new file mode 100644 index 000000000..ea2c2cb6d --- /dev/null +++ b/contrib/template.vhost @@ -0,0 +1,61 @@ +# Généré depuis contrib/template.vhost par `make generate-nginx` — NE PAS ÉDITER. +# Vhost $DOMAIN — backend $BACKEND_SCHEME://$HTTP_HOST:$BACKEND_PORT +# +# HTTP/3 (QUIC) termine au edge nginx (plan contrib/quic.md). Requiert un +# nginx compilé avec http_v3_module (≥ 1.25 ; `http3 on` ≥ 1.27.4) + UDP 443 +# ouvert. Kestrel reste backend h1/h2 (mais configure aussi h3 côté logiciel, +# cf. template.appsettings-*.json — capacité, non joignable derrière nginx). +# +# Variables substituées par envsubst (allowlist) : +# DOMAIN BACKEND_SCHEME HTTP_HOST BACKEND_PORT QUIC_OPTS +# $QUIC_OPTS = "reuseport" sur un seul vhost (Org) ET en production +# uniquement ; vide en preprod et sur les autres hôtes. Règle : reuseport +# une seule fois par addr:port (443/udp) — or preprod et prod partagent la +# même IP, donc seule la prod (Org) l'active. +# Les variables nginx intrinsèques ($host, $scheme, $remote_addr, …) sont +# préservées (hors allowlist). + +# Port 80 : redirection vers HTTPS. (Certbot/nginx-plugin gère les challenges +# ACME ; ne pas servir l'app en HTTP clair.) +server { + listen 80; + listen [::]:80; + server_name $DOMAIN; + return 301 https://$host$request_uri; +} + +server { + # 1. TCP : HTTPS classique + HTTP/2 + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + # 2. UDP : HTTP/3 (QUIC). reuseport sur UN seul vhost (Org via QUIC_OPTS). + listen 443 quic$QUIC_OPTS; + listen [::]:443 quic$QUIC_OPTS; + http3 on; + + server_name $DOMAIN; + + # 3. Certificats Let's Encrypt (Certbot). + ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; # managed by Certbot + # QUIC exige TLS 1.3. (TLS 1.2 conservable pour le fallback TCP : + # `ssl_protocols TLSv1.2 TLSv1.3;` — QUIC reste en 1.3.) + ssl_protocols TLSv1.3; + + # 4. Annonce h3 au navigateur sur 443/udp. `always` = émis même en erreur. + add_header Alt-Svc 'h3=":443"; ma=86400' always; + + location / { + proxy_pass $BACKEND_SCHEME://$HTTP_HOST:$BACKEND_PORT; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 100s; + proxy_buffering off; + } + include server-status.conf; +} \ No newline at end of file diff --git a/contrib/yavsc-org b/contrib/yavsc-org deleted file mode 100644 index dc65aa9fe..000000000 --- a/contrib/yavsc-org +++ /dev/null @@ -1,54 +0,0 @@ - - -server { - if ($host = yavsc.pschneider.fr) { - return 301 https://$host$request_uri; - } # managed by Certbot - - - - listen 80; - server_name yavsc.pschneider.fr; - location / { - root /srv/www/yavsc/wwwroot; - dav_methods PUT; - - proxy_pass http://127.0.0.1:3003; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection keep-alive; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - -} - -server { - listen 443 ssl; - server_name yavsc.pschneider.fr; - ssl_certificate /etc/letsencrypt/live/yavsc.pschneider.fr-0001/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/yavsc.pschneider.fr-0001/privkey.pem; # managed by Certbot - # Configure the SignalR Endpoint - location / { - # App server url - proxy_pass http://localhost:3003; - - proxy_cache off; - # WebSockets were implemented after http/1.0 - proxy_http_version 1.1; - - # Configuration for ServerSentEvents - proxy_buffering off; - - # Configuration for LongPolling or if your KeepAliveInterval is longer than 60 seconds - proxy_read_timeout 100s; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} - diff --git a/docker-compose.yaml b/docker-compose.yaml index f478dcc9f..ea08e9009 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,7 +26,7 @@ services: dockerfile: Dockerfile target: web-runtime args: - BUILD_ENV_TAG: debian12-dotnet10-android36-v1 + BUILD_ENV_TAG: debian13-dotnet10-android36-jdk21-v2 secrets: - source: yavsc_appsettings target: yavsc_appsettings @@ -53,9 +53,7 @@ services: depends_on: db: condition: service_healthy - networks: - - yavsc-internal - - yavsc-public + network_mode: host # volumes: # # Indispensable pour que Yavsc.Org (IdentityServer8) démarre. # # Sans ce montage, 'docker compose up' échoue côté web avec @@ -73,7 +71,7 @@ services: dockerfile: Dockerfile target: api-runtime args: - BUILD_ENV_TAG: debian12-dotnet10-android36-v1 + BUILD_ENV_TAG: debian13-dotnet10-android36-jdk21-v2 secrets: - source: yavsc_appsettings target: yavsc_appsettings @@ -90,9 +88,7 @@ services: depends_on: db: condition: service_healthy - networks: - - yavsc-internal - - yavsc-public + network_mode: host # volumes: # - /etc/letsencrypt:/etc/letsencrypt:ro @@ -105,7 +101,7 @@ services: dockerfile: Dockerfile target: blogs-runtime args: - BUILD_ENV_TAG: debian12-dotnet10-android36-v1 + BUILD_ENV_TAG: debian13-dotnet10-android36-jdk21-v2 secrets: - source: yavsc_appsettings target: yavsc_appsettings @@ -122,9 +118,7 @@ services: depends_on: db: condition: service_healthy - networks: - - yavsc-internal - - yavsc-public + network_mode: host # volumes: # - /etc/letsencrypt:/etc/letsencrypt:ro diff --git a/global.json b/global.json new file mode 100644 index 000000000..9009caf0b --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index e8804a8bf..42d09cd55 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -4,28 +4,31 @@ true - 12.1.1 + 12.1.2 + - + - + + - - - + + + + - + \ No newline at end of file diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 549100804..f8fd5a0fd 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -2,6 +2,7 @@ using Android.App; using Android.Content; using Android.Content.PM; +using Android.Views; using AndroidX.Core.Provider; using AndroidX.Emoji2.Text; using Avalonia.Android; @@ -15,7 +16,9 @@ namespace PostIt.Android; Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, - ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] + WindowSoftInputMode = SoftInput.AdjustResize, + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode) + ] public class MainActivity : AvaloniaMainActivity { /// diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index a6d04cb33..18bfec459 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -11,8 +11,8 @@ false 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 @@ -27,4 +27,14 @@ - \ No newline at end of file + + + + + + + diff --git a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj index ff2f3f2b8..f2b2a2bbf 100644 --- a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj +++ b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj @@ -6,8 +6,8 @@ enable 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 diff --git a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj index 36ed1251e..46df5025f 100644 --- a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj +++ b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj @@ -7,8 +7,8 @@ enable 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 app.manifest diff --git a/src/PostIt/PostIt.Desktop/Program.cs b/src/PostIt/PostIt.Desktop/Program.cs index 0de3bd69e..a2b8cd66f 100644 --- a/src/PostIt/PostIt.Desktop/Program.cs +++ b/src/PostIt/PostIt.Desktop/Program.cs @@ -12,6 +12,8 @@ sealed class Program [STAThread] public static void Main(string[] args) { + CLIOptionHandler.HandleArgs(args); + // Short-circuit 2nd-instance launches (OS handing us the // postit://callback URL) BEFORE Avalonia spins up a window. // If we let Avalonia initialise, the new MainWindow flashes diff --git a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs index db70e25fa..17efd391b 100644 --- a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs @@ -22,7 +22,8 @@ public class EstimateEditionPageViewModelTests private static EstimateEditionPageViewModel CreateViewModel(StubEstimateApi api, BillingQuerySummaryDto? query = null) { var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - return new EstimateEditionPageViewModel(query ?? SampleQuery(), client); + var fsClient = new UserFilesApiClient(api, "https://blogs.example/api/v1/"); + return new EstimateEditionPageViewModel(query ?? SampleQuery(), client, fsClient); } [Fact] @@ -143,10 +144,8 @@ public class EstimateEditionPageViewModelTests await vm.SendCommand.ExecuteAsync(null); - Assert.Equal("https://business.example/api/v1/estimate", api.LastPath); - Assert.Equal(HttpMethod.Post, api.LastMethod); - - var payload = Assert.IsType(api.LastBody); + var postCall = Assert.Single(api.Calls, c => c.Method == HttpMethod.Post && c.Path == "https://business.example/api/v1/estimate"); + var payload = Assert.IsType(postCall.Body); Assert.Equal(42, payload.CommandId); Assert.Equal("cli-1", payload.ClientId); Assert.Equal("Brush", payload.CommandType); @@ -216,7 +215,8 @@ public class EstimateEditionPageViewModelTests await vm.SendCommand.ExecuteAsync(null); - var payload = Assert.IsType(api.LastBody); + var postCall = Assert.Single(api.Calls, c => c.Method == HttpMethod.Post && c.Path == "https://business.example/api/v1/estimate"); + var payload = Assert.IsType(postCall.Body); Assert.Equal(2, payload.Bill.Count); Assert.Equal(-5m, payload.Bill[1].UnitaryCost); Assert.True(vm.HasSent); @@ -230,11 +230,20 @@ public class EstimateEditionPageViewModelTests public object? LastBody { get; private set; } public Exception? Failure { get; init; } - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + /// Historique complet des appels (le plus récent en dernier). + public List<(HttpMethod Method, string Path, object? Body)> Calls { get; } = new(); + + private void Record(HttpMethod method, string path, object? body) { LastMethod = method; LastPath = path; LastBody = body; + Calls.Add((method, path, body)); + } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + Record(method, path, body); if (Failure is not null) { @@ -253,9 +262,7 @@ public class EstimateEditionPageViewModelTests public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) { - LastMethod = method; - LastPath = path; - LastBody = body; + Record(method, path, body); return Task.CompletedTask; } @@ -265,6 +272,12 @@ public class EstimateEditionPageViewModelTests public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) => CallAsync(method, path, (object?)null, ct); + public Task DownloadAsync(HttpMethod method, string path, CancellationToken ct = default) + { + Record(method, path, null); + return Task.FromResult(Array.Empty()); + } + public ValueTask DisposeAsync() => ValueTask.CompletedTask; } } diff --git a/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs index 6158b4132..eefd59f20 100644 --- a/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/EstimateListPageViewModelTests.cs @@ -11,12 +11,13 @@ public class EstimateListPageViewModelTests { var api = new StubEstimateListApi(); var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client, front); await vm.InitializeAsync(); Assert.Contains("https://business.example/api/v1/estimate/asclient", api.Paths); - Assert.Equal("Mes devis à valider", vm.Title); + Assert.Equal("Devis en attente de signature client", vm.Title); Assert.Equal(2, vm.Estimates.Count); // Most recent first Assert.Equal(2, vm.Estimates[0].Id); @@ -28,12 +29,13 @@ public class EstimateListPageViewModelTests { var api = new StubEstimateListApi(); var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider, front); await vm.InitializeAsync(); Assert.Contains("https://business.example/api/v1/estimate/asprovider", api.Paths); - Assert.Equal("Mes devis en attente", vm.Title); + Assert.Equal("Devis en attente de signature fournisseur", vm.Title); Assert.Equal(2, vm.Estimates.Count); } @@ -42,7 +44,8 @@ public class EstimateListPageViewModelTests { var api = new StubEstimateListApi(); var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Provider, front); await vm.InitializeAsync(); @@ -63,7 +66,8 @@ public class EstimateListPageViewModelTests { var api = new StubEstimateListApi { Empty = true }; var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateListPageViewModel(client, EstimateListPerspective.Client, front); await vm.InitializeAsync(); diff --git a/src/PostIt/PostIt.Tests/EstimateValidationPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateValidationPageViewModelTests.cs new file mode 100644 index 000000000..02a6c64bd --- /dev/null +++ b/src/PostIt/PostIt.Tests/EstimateValidationPageViewModelTests.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using PostIt.Controls; +using PostIt.ViewModels; +using Yavsc.Api.Client; +using Yavsc.Models.Billing; + +namespace PostIt.Tests; + +/// +/// Verifies wires the two +/// signature pads (provider + client) into the right +/// call. The view (XAML, rendering) +/// is not tested here; the VM is driven through the same +/// test hooks the signature tests use, +/// and the API client through a recording fake +/// . The test host's +/// TestApp makes GoBackAsync a no-op, so the success paths +/// run to completion without navigation side effects. +/// +public class EstimateValidationPageViewModelTests +{ + private static EstimateDto SampleEstimate(long? commandId = 42) => new() + { + Id = 7, + CommandId = commandId, + CommandType = "Rdv", + Title = "Devis plomberie", + Description = "Réparation", + ClientId = "bob", + OwnerId = "alice", + }; + + private static void DrawOneStroke(SignaturePadControl pad) + { + pad.AppendPointForTest(100, 200); + pad.AppendPointForTest(300, 400); + pad.SealStrokeForTest(); + } + + [Fact] + public async Task Validate_calls_accept_with_strokes_and_command_id() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + + var proPad = new SignaturePadControl(); + var clientPad = new SignaturePadControl(); + vm.Attach(proPad, clientPad); + DrawOneStroke(proPad); // provider signs on their writable pad + + await vm.ValidateAsync(); + + Assert.Equal(HttpMethod.Post, api.LastMethod); + Assert.Contains("front/query/accept", api.LastPath); + Assert.Contains("billingCode=Rdv", api.LastPath); + Assert.Contains("queryId=42", api.LastPath); + var body = Assert.IsType(api.LastBody); + Assert.NotNull(body.Strokes); + Assert.NotEmpty(body.Strokes); + Assert.Equal(10_000, body.CoordinateMax); + // The provider perspective declares the Pro side, so the + // server stores the signature under the provider's author + // regardless of role inference. + Assert.Equal(SignatureType.Pro, body.SignatureType); + } + + [Fact] + public async Task Validate_from_client_perspective_declares_client_side() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Client, front); + + var cliPad = new SignaturePadControl(); + vm.Attach(new SignaturePadControl(), cliPad); + DrawOneStroke(cliPad); // client signs on their writable pad + + await vm.ValidateAsync(); + + var body = Assert.IsType(api.LastBody); + Assert.Equal(SignatureType.Client, body.SignatureType); // Client side + } + + [Fact] + public async Task Reject_calls_reject_endpoint_with_no_body() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Client, front); + + vm.Attach(new SignaturePadControl(), new SignaturePadControl()); + + await vm.RejectAsync(); + + Assert.Equal(HttpMethod.Post, api.LastMethod); + Assert.Contains("front/query/reject", api.LastPath); + Assert.Contains("queryId=42", api.LastPath); + Assert.Null(api.LastBody); + } + + [Fact] + public async Task Validate_without_signature_warns_and_does_not_call() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + + vm.Attach(new SignaturePadControl(), new SignaturePadControl()); // empty pads + + await vm.ValidateAsync(); // no throw: returns before GoBack + + Assert.Null(api.LastMethod); + Assert.Contains("signer", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Validate_command_gates_on_signature_and_command() + { + var api = new RecordingApi(); + var front = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + var vm = new EstimateValidationPageViewModel( + SampleEstimate(commandId: null), EstimateListPerspective.Provider, front); + + var proPad = new SignaturePadControl(); + vm.Attach(proPad, new SignaturePadControl()); + + // No CommandId ⇒ cannot validate even after signing. + DrawOneStroke(proPad); + Assert.False(vm.ValidateCommand.CanExecute(null)); + + var vm2 = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + var proPad2 = new SignaturePadControl(); + vm2.Attach(proPad2, new SignaturePadControl()); + Assert.False(vm2.ValidateCommand.CanExecute(null)); // no signature yet + DrawOneStroke(proPad2); + Assert.True(vm2.ValidateCommand.CanExecute(null)); + } + + [Fact] + public void RoleLabel_reflects_perspective() + { + var front = new FrontOfficeApiClient(new RecordingApi(), "https://business.example/api/v1/"); + var pro = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + var cli = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Client, front); + + Assert.Contains("prestataire", pro.RoleLabel, StringComparison.OrdinalIgnoreCase); + Assert.Contains("client", cli.RoleLabel, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Only_the_author_pad_is_writable() + { + var front = new FrontOfficeApiClient(new RecordingApi(), "https://business.example/api/v1/"); + + var proVm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Provider, front); + var proPad = new SignaturePadControl(); + var cliPad = new SignaturePadControl(); + proVm.Attach(proPad, cliPad); + Assert.False(proPad.IsReadOnly); // provider writes the Pro pad + Assert.True(cliPad.IsReadOnly); + + var cliVm = new EstimateValidationPageViewModel( + SampleEstimate(), EstimateListPerspective.Client, front); + var proPad2 = new SignaturePadControl(); + var cliPad2 = new SignaturePadControl(); + cliVm.Attach(proPad2, cliPad2); + Assert.True(proPad2.IsReadOnly); + Assert.False(cliPad2.IsReadOnly); // client writes the Client pad + } + + [Fact] + public void Load_existing_signatures_renders_both_sides_in_their_pads() + { + var proStrokes = new[] { 1, 1000, 1000 }; + var cliStrokes = new[] { 1, 9000, 9000 }; + var front = new FrontOfficeApiClient(new RecordingApi(), "https://business.example/api/v1/"); + var estimate = SampleEstimate(); + estimate.SignaturePro = new EstimateSignatureDto { Strokes = proStrokes }; + estimate.SignatureClient = new EstimateSignatureDto { Strokes = cliStrokes }; + + // Client perspective: both pads show the stored signatures, but + // only the client pad is writable. + var vm = new EstimateValidationPageViewModel(estimate, EstimateListPerspective.Client, front); + var proPad = new SignaturePadControl(); + var cliPad = new SignaturePadControl(); + vm.Attach(proPad, cliPad); + + Assert.Equal(proStrokes, proPad.Snapshot().Strokes); + Assert.Equal(cliStrokes, cliPad.Snapshot().Strokes); + Assert.True(vm.HasProSignature); + Assert.True(vm.HasClientSignature); + } + + [Fact] + public void Load_existing_pro_signature_renders_in_pro_pad_only() + { + var strokes = new[] { 1, 5000, 5000 }; + var front = new FrontOfficeApiClient(new RecordingApi(), "https://business.example/api/v1/"); + var estimate = SampleEstimate(); + estimate.SignaturePro = new EstimateSignatureDto { Strokes = strokes, CoordinateMax = 10_000 }; + var vm = new EstimateValidationPageViewModel(estimate, EstimateListPerspective.Provider, front); + + var proPad = new SignaturePadControl(); + var cliPad = new SignaturePadControl(); + vm.Attach(proPad, cliPad); + + Assert.Equal(strokes, proPad.Snapshot().Strokes); + Assert.True(cliPad.Snapshot().IsEmpty); + Assert.True(vm.HasProSignature); + Assert.False(vm.HasClientSignature); + } + + private sealed class RecordingApi : IYavscApiClient + { + public HttpClient Http { get; } = new(); + public HttpMethod? LastMethod { get; private set; } + public string? LastPath { get; private set; } + public object? LastBody { get; private set; } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = body; + return Task.FromResult(default(T)!); + } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = body; + return Task.CompletedTask; + } + + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, (object?)null, ct); + + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, (object?)null, ct); + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/FrontOfficeApiClientTests.cs b/src/PostIt/PostIt.Tests/FrontOfficeApiClientTests.cs new file mode 100644 index 000000000..02f6ae149 --- /dev/null +++ b/src/PostIt/PostIt.Tests/FrontOfficeApiClientTests.cs @@ -0,0 +1,157 @@ +using System.Net.Http; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +/// +/// Verifies issues the right +/// api/v1/front/query/accept / .../reject requests with +/// the PostIt signature payload, using a recording fake +/// (no network). +/// +public class FrontOfficeApiClientTests +{ + private static QueryAcceptanceRequestDto SampleBody() => new() + { + Strokes = new[] { 2, 100, 200, 300, 400 }, + CoordinateMax = 10_000, + CapturedAtUtc = new DateTime(2026, 9, 21, 12, 0, 0, DateTimeKind.Utc), + }; + + [Fact] + public async Task Accept_posts_to_accept_endpoint_with_signature_body() + { + var api = new RecordingApi(); + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + var body = SampleBody(); + await client.AcceptQueryAsync("Rdv", 42, body); + + Assert.Equal(HttpMethod.Post, api.LastMethod); + Assert.Equal( + "https://business.example/api/v1/front/query/accept?billingCode=Rdv&queryId=42", + api.LastPath); + Assert.Same(body, api.LastBody); + } + + [Fact] + public async Task Reject_posts_to_reject_endpoint_with_no_body() + { + var api = new RecordingApi(); + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + await client.RejectQueryAsync("Rdv", 42); + + Assert.Equal(HttpMethod.Post, api.LastMethod); + Assert.Equal( + "https://business.example/api/v1/front/query/reject?billingCode=Rdv&queryId=42", + api.LastPath); + Assert.Null(api.LastBody); + } + + [Fact] + public async Task Accept_returns_deserialized_response() + { + var api = new RecordingApi + { + Response = new QueryAcceptanceResponseDto + { + QueryId = 42, + Status = "Accepted", + Signature = new QuerySignatureRefDto { Id = 9, EstimateId = 5, Type = "Pro" }, + }, + }; + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + var response = await client.AcceptQueryAsync("Rdv", 42, SampleBody()); + + Assert.Equal(42, response.QueryId); + Assert.Equal("Accepted", response.Status); + Assert.NotNull(response.Signature); + Assert.Equal("Pro", response.Signature!.Type); + } + + [Fact] + public async Task Accept_rejects_invalid_arguments() + { + var api = new RecordingApi(); + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + await Assert.ThrowsAsync(() => client.AcceptQueryAsync("", 42, SampleBody())); + await Assert.ThrowsAsync(() => client.AcceptQueryAsync("Rdv", 0, SampleBody())); + await Assert.ThrowsAsync(() => client.AcceptQueryAsync("Rdv", 42, null!)); + await Assert.ThrowsAsync(() => client.RejectQueryAsync("", 42)); + await Assert.ThrowsAsync(() => client.RejectQueryAsync("Rdv", 0)); + } + + [Fact] + public async Task GetEstimatePdf_gets_estimate_pdf_endpoint() + { + var api = new RecordingApi(); + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + await client.GetEstimatePdfAsync(42); + + Assert.Equal(HttpMethod.Get, api.LastMethod); + Assert.Equal( + "https://business.example/api/v1/front/query/42/estimate.pdf", + api.LastPath); + } + + [Fact] + public async Task GetEstimateTex_gets_estimate_tex_endpoint() + { + var api = new RecordingApi(); + var client = new FrontOfficeApiClient(api, "https://business.example/api/v1/"); + + await client.GetEstimateTexAsync(42); + + Assert.Equal(HttpMethod.Get, api.LastMethod); + Assert.Equal( + "https://business.example/api/v1/front/query/42/estimate.tex", + api.LastPath); + } + + private sealed class RecordingApi : IYavscApiClient + { + public HttpClient Http { get; } = new(); + public HttpMethod? LastMethod { get; private set; } + public string? LastPath { get; private set; } + public object? LastBody { get; private set; } + public QueryAcceptanceResponseDto? Response { get; set; } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = body; + + var response = Response ?? new QueryAcceptanceResponseDto(); + return Task.FromResult((T)(object)response); + } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = body; + return Task.CompletedTask; + } + + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, (object?)null, ct); + + public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) + => CallAsync(method, path, (object?)null, ct); + + public Task DownloadAsync(HttpMethod method, string path, CancellationToken ct = default) + { + LastMethod = method; + LastPath = path; + LastBody = null; + return Task.FromResult(Array.Empty()); + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs b/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs new file mode 100644 index 000000000..72619f3c5 --- /dev/null +++ b/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs @@ -0,0 +1,208 @@ +using System.Net.Http; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.VisualTree; +using CommunityToolkit.Mvvm.Input; +using PostIt.ViewModels; +using PostIt.Views; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +/// +/// Headless UI coverage for the "Mes fichiers" page +/// (). The user reported that once a file +/// is listed, "l'item est inerte au click, et ses boutons restent +/// grisés, désactivés" — neither the row nor its action buttons +/// (Télécharger / Supprimer) respond to a click. +/// +/// These tests mount a real under +/// the Avalonia headless platform with a stub +/// that lists one file, then walk the +/// realised visual tree to find the row's action buttons and +/// assert the two things that would silently break and reproduce +/// the "grayed / inert" symptom: +/// +/// The Command binding +/// ($parent[vm:MyFilesViewModel].DownloadFileCommand) +/// resolved to a non-null command — a null command leaves the +/// button disabled (grayed) and click-silent. +/// The command is armed +/// (CanExecute true) for the listed file — an +/// AsyncRelayCommand with +/// AllowConcurrentExecutions=false (the +/// +/// default) disables itself while running; a button stuck +/// "running" is exactly the grayed-and-inert report. +/// Executing the command actually invokes the +/// file download / delete on the API client — the row +/// "activates" on click instead of being inert. +/// +/// The OS save picker is absent under headless, so +/// FileSaveHelpers.SaveAsync throws or cancels and the +/// download command's catch sets an error status — but the +/// API download is awaited before the picker, so the call +/// is recorded regardless. We do not await the command's +/// ExecutionTask to completion, to avoid blocking on the +/// (platform-dependent) picker. +/// +public class MyFilesPageHeadlessTests +{ + private static (MyFilesPage page, MyFilesViewModel vm, StubFsApi api) CreatePage() + { + var api = new StubFsApi(); + var fsClient = new UserFilesApiClient(api, "https://blogs.example/api/v1/"); + var vm = new MyFilesViewModel(fsClient); + var page = new MyFilesPage { DataContext = vm }; + return (page, vm, api); + } + + /// + /// Mount the page the way the app does — pushed onto a + /// shown in a + /// (a realizes its content template via + /// the navigation host, cf. RdvPageHeadlessTests) — after + /// listing a single file by awaiting + /// . Populating + /// before the first layout + /// pass lets the ItemsControl realise its item templates + /// under headless. + /// + private static async Task<(MyFilesPage page, MyFilesViewModel vm, StubFsApi api)> MountAsync() + { + var (page, vm, api) = CreatePage(); + await vm.InitializeAsync(); + var nav = new NavigationPage(); + nav.PushAsync(page).GetAwaiter().GetResult(); + var window = new Window { Content = nav }; + window.Show(); + // Let bindings / the ItemsControl item templates apply. + await Task.Delay(50); + return (page, vm, api); + } + + /// + /// Find the first action button in a file row whose content + /// matches . The file rows live in an + /// ItemsControl DataTemplate (no x:Name), so we + /// walk the visual tree. + /// + private static Button? FindRowButton(MyFilesPage page, string content) + => page.GetVisualDescendants() + .OfType public enum QueryStatus: int { - Inserted, - Rejected, - Accepted, - InProgress, + Inserted = 0, + Rejected = 1, + Accepted = 2, + InProgress = 3, // final states - Failed, - Success + Failed = 4, + Success = 5, + // party-specific acceptance (see summary) + ProAccepted = 6, + ClientAccepted = 7, } -} +} \ No newline at end of file diff --git a/src/Yavsc.Abstract/Yavsc.Abstract.csproj b/src/Yavsc.Abstract/Yavsc.Abstract.csproj index 4df6c53c2..3de58d2db 100644 --- a/src/Yavsc.Abstract/Yavsc.Abstract.csproj +++ b/src/Yavsc.Abstract/Yavsc.Abstract.csproj @@ -11,7 +11,7 @@ latest 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 \ No newline at end of file diff --git a/src/Yavsc.Api.Client/Dtos/EstimateDtos.cs b/src/Yavsc.Api.Client/Dtos/EstimateDtos.cs index 43c09d94a..3e7a15052 100644 --- a/src/Yavsc.Api.Client/Dtos/EstimateDtos.cs +++ b/src/Yavsc.Api.Client/Dtos/EstimateDtos.cs @@ -39,6 +39,39 @@ public sealed class EstimateDto public string CommandType { get; set; } = string.Empty; public DateTime ProviderValidationDate { get; set; } public DateTime ClientValidationDate { get; set; } + + /// + /// The provider's signature on this estimate, or null when the + /// provider has not signed. Carries the PostIt wire-format + /// so the pad can + /// repaint a previously drawn signature on reopen without a second + /// round-trip. + /// + public EstimateSignatureDto? SignaturePro { get; set; } + + /// + /// The client's signature on this estimate, or null when the + /// client has not signed. + /// + public EstimateSignatureDto? SignatureClient { get; set; } +} + +/// +/// One stored signature on an estimate, mirroring the JSON shape of +/// the server-side Yavsc.Models.Billing.Signature entity. The +/// are the same PostIt wire-format payload that +/// was captured and are loaded back into the pad unchanged. The side +/// (Pro/Client) is implicit in which property of +/// carries this DTO, so no Type +/// field is needed. +/// +public sealed class EstimateSignatureDto +{ + public long Id { get; set; } + public long EstimateId { get; set; } + public int CoordinateMax { get; set; } + public DateTime CapturedAtUtc { get; set; } + public int[]? Strokes { get; set; } } /// diff --git a/src/Yavsc.Api.Client/Dtos/QueryAcceptanceDto.cs b/src/Yavsc.Api.Client/Dtos/QueryAcceptanceDto.cs new file mode 100644 index 000000000..dffbbf3ad --- /dev/null +++ b/src/Yavsc.Api.Client/Dtos/QueryAcceptanceDto.cs @@ -0,0 +1,56 @@ +using System; +using Yavsc.Models.Billing; + +namespace Yavsc.Api.Client; + +/// +/// Body of POST api/v1/front/query/accept and +/// .../query/reject, mirroring the server-side +/// Yavsc.ViewModels.FrontOffice.QueryAcceptanceRequest. +/// is optional: when non-empty on accept it is +/// the PostIt wire-format signature captured with the acceptance; null +/// or empty means no signature (the only valid value on reject). +/// +public sealed class QueryAcceptanceRequestDto +{ + public int[]? Strokes { get; set; } + + public int CoordinateMax { get; set; } = 10_000; + + public DateTime? CapturedAtUtc { get; set; } + + /// + /// Which signature is being submitted: + /// (the provider) or + /// . The PostIt client sets this + /// from the perspective the page was opened in (provider signs Pro, + /// client signs Client), so the server stores the signature under + /// the right author even when one user is both parties. Null lets + /// the server infer the role from the caller's identity. + /// + public SignatureType? SignatureType { get; set; } +} + +/// +/// Response of POST .../query/accept / .../query/reject +/// (Ok(new { queryId, status, signature = ... })). The +/// ref is non-null only when a signature was +/// staged on accept. +/// +public sealed class QueryAcceptanceResponseDto +{ + public long QueryId { get; set; } + + public string? Status { get; set; } + + public QuerySignatureRefDto? Signature { get; set; } +} + +public sealed class QuerySignatureRefDto +{ + public long Id { get; set; } + + public long EstimateId { get; set; } + + public string? Type { get; set; } +} \ No newline at end of file diff --git a/src/Yavsc.Api.Client/Dtos/UserFilesDtos.cs b/src/Yavsc.Api.Client/Dtos/UserFilesDtos.cs new file mode 100644 index 000000000..2243bbad2 --- /dev/null +++ b/src/Yavsc.Api.Client/Dtos/UserFilesDtos.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; + +namespace Yavsc.Api.Client; + +/// +/// Représentation d'un répertoire de l'espace de stockage personnel, +/// renvoyée par GET api/v1/fs/{subdir} (host Blogs). Ne dépend pas +/// des helpers serveur : c'est un pur DTO de désérialisation. +/// +public sealed class UserDirectoryDto +{ + public string? UserName { get; set; } + public string? SubPath { get; set; } + public FileEntryDto[] Files { get; set; } = Array.Empty(); + public DirEntryDto[] SubDirectories { get; set; } = Array.Empty(); +} + +public sealed class FileEntryDto +{ + /// Identifiant de la ligne UploadedFile, pour l'attachement par référence. + public long? Id { get; set; } + public string? Name { get; set; } + public long Size { get; set; } + public DateTime LastModified { get; set; } +} + +public sealed class DirEntryDto +{ + public string? Name { get; set; } +} + +/// +/// Métadonnées d'une pièce jointe (fichier de l'espace perso rattaché à +/// un devis ou une demande), renvoyées par les endpoints d'attachement. +/// +public sealed class AttachmentDto +{ + public long FileId { get; set; } + /// Chemin relatif dans l'espace perso du propriétaire. + public string? Path { get; set; } + public long Size { get; set; } + public string? ContentType { get; set; } + public string? OwnerId { get; set; } +} + +/// +/// Métadonnées d'un fichier reçu après un téléversement multipart vers +/// POST api/v1/fs/{subdir}. Porte notamment l'identifiant +/// de la ligne UploadedFile créée côté serveur. +/// +public sealed class FileReceivedDto +{ + public long? FileId { get; set; } + public string? FileName { get; set; } + public long Length { get; set; } + public string? ContentType { get; set; } + public bool QuotaOffense { get; set; } + public bool Overridden { get; set; } +} + +/// +/// Contenu d'un fichier à téléverser (nom, octets, type MIME), à l'image +/// de mais pour l'espace perso générique. +/// +public sealed record UploadFile(string FileName, byte[] Content, string? ContentType); \ No newline at end of file diff --git a/src/Yavsc.Api.Client/EstimateApiClient.cs b/src/Yavsc.Api.Client/EstimateApiClient.cs index 4a80bbe62..fc3d41fc9 100644 --- a/src/Yavsc.Api.Client/EstimateApiClient.cs +++ b/src/Yavsc.Api.Client/EstimateApiClient.cs @@ -113,6 +113,41 @@ public sealed class EstimateApiClient return _api.CallAsync(HttpMethod.Delete, Absolute($"{PathPrefix}/{id}"), ct: ct); } + /// + /// Liste les pièces jointes (fichiers de l'espace perso du fournisseur) + /// rattachées au devis par référence + /// (GET estimate/{id}/attachments). + /// + public Task> GetAttachmentsAsync(long id, CancellationToken ct = default) + { + if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); + return _api.CallAsync>(HttpMethod.Get, Absolute($"{PathPrefix}/{id}/attachments"), ct: ct); + } + + /// + /// Attache un fichier de l'espace perso du fournisseur au devis + /// par référence (POST estimate/{id}/attachments). + /// Réservé au fournisseur côté serveur. + /// + public Task AttachFileAsync(long id, long fileId, CancellationToken ct = default) + { + if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); + if (fileId <= 0) throw new ArgumentOutOfRangeException(nameof(fileId)); + return _api.CallAsync(HttpMethod.Post, Absolute($"{PathPrefix}/{id}/attachments"), + body: new { fileId }, ct: ct); + } + + /// + /// Détache un fichier du devis + /// (DELETE estimate/{id}/attachments/{fileId}). Réservé au fournisseur. + /// + public Task DetachFileAsync(long id, long fileId, CancellationToken ct = default) + { + if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); + if (fileId <= 0) throw new ArgumentOutOfRangeException(nameof(fileId)); + return _api.CallAsync(HttpMethod.Delete, Absolute($"{PathPrefix}/{id}/attachments/{fileId}"), ct: ct); + } + private string Absolute(string relativePath) => new Uri(ResolveBusinessBaseAddress(), relativePath).ToString(); private Uri ResolveBusinessBaseAddress() diff --git a/src/Yavsc.Api.Client/FrontOfficeApiClient.cs b/src/Yavsc.Api.Client/FrontOfficeApiClient.cs new file mode 100644 index 000000000..2e3877ef3 --- /dev/null +++ b/src/Yavsc.Api.Client/FrontOfficeApiClient.cs @@ -0,0 +1,177 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Yavsc.Api.Client; + +/// +/// HTTP client for the front-office query routes +/// (api/v1/front/query/accept / .../reject, served by +/// ). Same +/// DTO↔path mapper shape as : transport +/// (base URL, JSON, Bearer auth, silent refresh on 401) is delegated +/// to . +/// +/// The server resolves the caller's role (provider vs client vs admin) +/// from the authenticated user compared to the query's +/// PerformerId/ClientId; the client does not declare a +/// role. is the estimate's +/// and +/// is its . +/// +public sealed class FrontOfficeApiClient +{ + private const string PathPrefix = "front"; + + private readonly IYavscApiClient _api; + private readonly Func _businessBaseAddress; + + public FrontOfficeApiClient(IYavscApiClient api, string businessBaseAddress) + : this(api, () => businessBaseAddress) + { + } + + public FrontOfficeApiClient(IYavscApiClient api, Func businessBaseAddress) + { + _api = api ?? throw new ArgumentNullException(nameof(api)); + _businessBaseAddress = businessBaseAddress ?? throw new ArgumentNullException(nameof(businessBaseAddress)); + _ = ResolveBusinessBaseAddress(); + } + + /// + /// Accept (validate) the query under + /// billing code . When + /// carries a non-empty signature, the + /// server stages it on the query's linked estimate and stamps the + /// matching validation date. + /// + public async Task AcceptQueryAsync( + string billingCode, + long queryId, + QueryAcceptanceRequestDto body, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(billingCode)) + throw new ArgumentException("billingCode is required.", nameof(billingCode)); + if (queryId <= 0) + throw new ArgumentOutOfRangeException(nameof(queryId)); + if (body is null) + throw new ArgumentNullException(nameof(body)); + + var path = $"{PathPrefix}/query/accept?billingCode={Uri.EscapeDataString(billingCode)}&queryId={queryId}"; + var response = await _api.CallAsync( + HttpMethod.Post, + Absolute(path), + body: body, + ct: ct).ConfigureAwait(false); + + return response ?? new QueryAcceptanceResponseDto { QueryId = queryId }; + } + + /// + /// Reject the query under billing code + /// . A signature is not meaningful on + /// a rejection; the server refuses one with 400, so this sends no + /// body. + /// + public async Task RejectQueryAsync( + string billingCode, + long queryId, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(billingCode)) + throw new ArgumentException("billingCode is required.", nameof(billingCode)); + if (queryId <= 0) + throw new ArgumentOutOfRangeException(nameof(queryId)); + + var path = $"{PathPrefix}/query/reject?billingCode={Uri.EscapeDataString(billingCode)}&queryId={queryId}"; + var response = await _api.CallAsync( + HttpMethod.Post, + Absolute(path), + body: null, + ct: ct).ConfigureAwait(false); + + return response ?? new QueryAcceptanceResponseDto { QueryId = queryId }; + } + + /// + /// Download the estimate linked to query + /// as a LaTeX (.tex) source document + /// (GET api/v1/front/query/{queryId}/estimate.tex). Returns + /// the raw TeX bytes (UTF-8 text). + /// + public Task GetEstimateTexAsync(long queryId, CancellationToken ct = default) + { + if (queryId <= 0) throw new ArgumentOutOfRangeException(nameof(queryId)); + var path = $"{PathPrefix}/query/{queryId}/estimate.tex"; + return _api.DownloadAsync(HttpMethod.Get, Absolute(path), ct); + } + + /// + /// Download the estimate linked to query + /// as a compiled PDF document + /// (GET api/v1/front/query/{queryId}/estimate.pdf). Returns + /// the raw PDF bytes. Requires texi2pdf on the server host; + /// a non-2xx response throws . + /// + public Task GetEstimatePdfAsync(long queryId, CancellationToken ct = default) + { + if (queryId <= 0) throw new ArgumentOutOfRangeException(nameof(queryId)); + var path = $"{PathPrefix}/query/{queryId}/estimate.pdf"; + return _api.DownloadAsync(HttpMethod.Get, Absolute(path), ct); + } + + /// + /// Liste les pièces jointes (fichiers de l'espace perso du client) + /// rattachées à la demande par référence + /// (GET front/query/{queryId}/attachments). + /// + public Task> GetQueryAttachmentsAsync(string billingCode, long queryId, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(billingCode)) + throw new ArgumentException("billingCode is required.", nameof(billingCode)); + if (queryId <= 0) throw new ArgumentOutOfRangeException(nameof(queryId)); + var path = $"{PathPrefix}/query/{queryId}/attachments?billingCode={Uri.EscapeDataString(billingCode)}"; + return _api.CallAsync>(HttpMethod.Get, Absolute(path), ct: ct); + } + + /// + /// Attache un fichier de l'espace perso du client à sa demande + /// (POST front/query/{queryId}/attachments). Réservé au client. + /// + public Task AttachQueryFileAsync(string billingCode, long queryId, long fileId, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(billingCode)) + throw new ArgumentException("billingCode is required.", nameof(billingCode)); + if (queryId <= 0) throw new ArgumentOutOfRangeException(nameof(queryId)); + if (fileId <= 0) throw new ArgumentOutOfRangeException(nameof(fileId)); + var path = $"{PathPrefix}/query/{queryId}/attachments?billingCode={Uri.EscapeDataString(billingCode)}"; + return _api.CallAsync(HttpMethod.Post, Absolute(path), body: new { fileId }, ct: ct); + } + + /// + /// Détache un fichier de la demande + /// (DELETE front/query/{queryId}/attachments/{fileId}). Réservé au client. + /// + public Task DetachQueryFileAsync(string billingCode, long queryId, long fileId, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(billingCode)) + throw new ArgumentException("billingCode is required.", nameof(billingCode)); + if (queryId <= 0) throw new ArgumentOutOfRangeException(nameof(queryId)); + if (fileId <= 0) throw new ArgumentOutOfRangeException(nameof(fileId)); + var path = $"{PathPrefix}/query/{queryId}/attachments/{fileId}?billingCode={Uri.EscapeDataString(billingCode)}"; + return _api.CallAsync(HttpMethod.Delete, Absolute(path), ct: ct); + } + + private string Absolute(string relativePath) => new Uri(ResolveBusinessBaseAddress(), relativePath).ToString(); + + private Uri ResolveBusinessBaseAddress() + { + var raw = _businessBaseAddress(); + if (string.IsNullOrWhiteSpace(raw)) + throw new InvalidOperationException("Business base address is required."); + + return new Uri(raw, UriKind.Absolute); + } +} \ No newline at end of file diff --git a/src/Yavsc.Api.Client/IYavscApiClient.cs b/src/Yavsc.Api.Client/IYavscApiClient.cs index 1d470b682..e2dd56acd 100644 --- a/src/Yavsc.Api.Client/IYavscApiClient.cs +++ b/src/Yavsc.Api.Client/IYavscApiClient.cs @@ -73,4 +73,23 @@ public interface IYavscApiClient : IAsyncDisposable string path, Func contentFactory, CancellationToken ct = default); + + /// + /// Download a binary resource (PDF, TeX, image, …). Unlike + /// , + /// which JSON-decodes the response, this returns the raw response + /// bytes. The same Bearer-token attach + 401-refresh-retry-once + /// contract applies. + /// + /// + /// A default interface implementation is provided so that test + /// fakes that do not exercise binary download still compile without + /// declaring the member; the real + /// overrides it with the authenticated transport. + /// + Task DownloadAsync( + HttpMethod method, + string path, + CancellationToken ct = default) + => Task.FromResult(Array.Empty()); } diff --git a/src/Yavsc.Api.Client/UserFilesApiClient.cs b/src/Yavsc.Api.Client/UserFilesApiClient.cs new file mode 100644 index 000000000..0b7e63671 --- /dev/null +++ b/src/Yavsc.Api.Client/UserFilesApiClient.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace Yavsc.Api.Client; + +/// +/// Client HTTP de l'espace de stockage personnel +/// (api/v1/fs, servi par le host Yavsc.Blogs). Même forme +/// que : le transport (URL de base, JSON, +/// Bearer, refresh silencieux sur 401) est délégué à +/// ; cette classe n'est qu'un mappeur +/// DTO↔chemin. +/// +/// Convention d'URL. Le pathPrefix est relatif au +/// segment de version déjà porté par l'URL de base +/// (…/api/v1/) : "fs" résout en …/api/v1/fs, qui +/// correspond au [Route(APIPrefix + "/fs")] de +/// FileSystemApiController. Ne pas ré-inclure api/. +/// +public sealed class UserFilesApiClient +{ + private const string PathPrefix = "fs"; + + private readonly IYavscApiClient _api; + private readonly Uri _baseAddress; + + public UserFilesApiClient(IYavscApiClient api, string blogsBaseAddress) + { + _api = api ?? throw new ArgumentNullException(nameof(api)); + if (string.IsNullOrEmpty(blogsBaseAddress)) + throw new ArgumentException("Base address is required.", nameof(blogsBaseAddress)); + + _baseAddress = new Uri(blogsBaseAddress); + _api.Http.BaseAddress = _baseAddress; + } + + /// + /// Liste le contenu du sous-répertoire de + /// l'espace perso de l'utilisateur (GET fs/{subdir}). + /// + public Task ListAsync(string? subdir = null, CancellationToken ct = default) + { + var path = string.IsNullOrWhiteSpace(subdir) ? PathPrefix : $"{PathPrefix}/{subdir.TrimStart('/')}"; + return _api.CallAsync(HttpMethod.Get, path, ct: ct); + } + + /// + /// Téléverse des fichiers dans le sous-répertoire + /// (POST fs/{subdir} multipart). + /// Renvoie les métadonnées des fichiers reçus, dont l'identifiant + /// servant à l'attachement. + /// + public Task> UploadAsync(string subdir, IReadOnlyCollection files, CancellationToken ct = default) + { + if (files is null || files.Count == 0) + throw new ArgumentException("At least one file is required.", nameof(files)); + + var path = string.IsNullOrWhiteSpace(subdir) + ? PathPrefix + : $"{PathPrefix}/{subdir.TrimStart('/')}"; + + return _api.CallAsync>(HttpMethod.Post, path, () => CreateMultipartContent(files), ct); + } + + /// + /// Supprime un fichier ou répertoire de l'espace perso + /// (DELETE fs/{path}). + /// + public Task DeleteAsync(string path, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("Path is required.", nameof(path)); + return _api.CallAsync(HttpMethod.Delete, $"{PathPrefix}/{path.TrimStart('/')}", ct: ct); + } + + /// + /// Télécharge une pièce jointe référencée par son identifiant + /// / + /// (GET fs/file/{fileId}). Renvoie les octets bruts ; le serveur + /// (host Blogs) autorise l'appelant (propriétaire ou partie au + /// devis/demande auquel le fichier est rattaché). + /// + public Task DownloadFileAsync(long fileId, CancellationToken ct = default) + { + if (fileId <= 0) throw new ArgumentOutOfRangeException(nameof(fileId)); + return _api.DownloadAsync(HttpMethod.Get, $"{PathPrefix}/file/{fileId}", ct); + } + + /// + /// Construit le corps multipart/form-data d'un téléversement : + /// une part file par fichier (type MIME retombant sur + /// application/octet-stream). Exposé public pour les + /// tests backend qui assertent la forme filaire. + /// + public static HttpContent CreateMultipartContent(IReadOnlyCollection files) + { + var content = new MultipartFormDataContent(); + foreach (var file in files) + { + var fileContent = new ByteArrayContent(file.Content); + fileContent.Headers.ContentType = new MediaTypeHeaderValue( + string.IsNullOrWhiteSpace(file.ContentType) ? "application/octet-stream" : file.ContentType); + content.Add(fileContent, "file", file.FileName); + } + return content; + } +} \ No newline at end of file diff --git a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj index f0cdb55c9..80cdf0ae5 100644 --- a/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj +++ b/src/Yavsc.Api.Client/Yavsc.Api.Client.csproj @@ -17,8 +17,8 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 diff --git a/src/Yavsc.Api.Test/Directory.Packages.props b/src/Yavsc.Api.Test/Directory.Packages.props deleted file mode 100644 index c2edf9eae..000000000 --- a/src/Yavsc.Api.Test/Directory.Packages.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs b/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs deleted file mode 100644 index 97ed2e61c..000000000 --- a/src/Yavsc.Api.Test/FrontOfficeApiControllerTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Net; -using System.Net.Http.Headers; -using Microsoft.Extensions.DependencyInjection; -using Yavsc.Api.Test.Fixtures; -using Yavsc.Helpers; -using Yavsc.Models; -using Yavsc.Tests.Shared; - -namespace Yavsc.Api.Test; - -[Collection("Yavsc Api")] -public sealed class FrontOfficeApiControllerTests : IClassFixture -{ - private readonly ApiWebServerFixture _fixture; - - public FrontOfficeApiControllerTests(ApiWebServerFixture fixture) - { - _fixture = fixture; - } - - private HttpClient NewClient(string subject = "alice", string scope = "api") - { - var handler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (_, _, _, _) => true - }; - - var http = new HttpClient(handler) - { - BaseAddress = new Uri(_fixture.BaseAddress) - }; - http.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope)); - return http; - } - - [Fact] - public async Task Front_accept_query_updates_status_without_server_error() - { - WorkflowHelpers.ConfigureBillingService(); - _fixture.ResetAndSeedRdvQueryGraph(); - - long queryId; - using (var scope = _fixture.Services.CreateScope()) - { - var db = scope.ServiceProvider.GetRequiredService(); - queryId = db.RdvQueries.Select(q => q.Id).Single(); - } - - using var http = NewClient(); - var response = await http.PostAsync($"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", content: null, TestContext.Current.CancellationToken); - var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); - - Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); - - using var assertScope = _fixture.Services.CreateScope(); - var assertDb = assertScope.ServiceProvider.GetRequiredService(); - var updated = assertDb.RdvQueries.Single(q => q.Id == queryId); - - Assert.Equal(QueryStatus.Accepted, updated.Status); - } -} diff --git a/src/Yavsc.Api/Controllers/Business/BillingController.cs b/src/Yavsc.Api/Controllers/Business/BillingController.cs index ba099e7c7..aa195712d 100644 --- a/src/Yavsc.Api/Controllers/Business/BillingController.cs +++ b/src/Yavsc.Api/Controllers/Business/BillingController.cs @@ -157,6 +157,8 @@ namespace Yavsc.ApiControllers .Where(q => allowedActivityCodes.Contains(q.ActivityCode)) .Where(q => q.Status == QueryStatus.Inserted || q.Status == QueryStatus.Accepted + || q.Status == QueryStatus.ProAccepted + || q.Status == QueryStatus.ClientAccepted || q.Status == QueryStatus.InProgress) .Cast() .ToList() @@ -169,6 +171,8 @@ namespace Yavsc.ApiControllers .Where(q => allowedActivityCodes.Contains(q.ActivityCode)) .Where(q => q.Status == QueryStatus.Inserted || q.Status == QueryStatus.Accepted + || q.Status == QueryStatus.ProAccepted + || q.Status == QueryStatus.ClientAccepted || q.Status == QueryStatus.InProgress) .Cast() .ToList() @@ -181,6 +185,8 @@ namespace Yavsc.ApiControllers .Where(q => allowedActivityCodes.Contains(q.ActivityCode)) .Where(q => q.Status == QueryStatus.Inserted || q.Status == QueryStatus.Accepted + || q.Status == QueryStatus.ProAccepted + || q.Status == QueryStatus.ClientAccepted || q.Status == QueryStatus.InProgress) .Cast() .ToList() @@ -329,9 +335,9 @@ namespace Yavsc.ApiControllers return new ChallengeResult(); } - var filename = AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, id); + var filename = AbstractFileSystemHelpers.SignFileNameFormat("cli", billingCode, id); FileInfo fi = new FileInfo(Path.Combine(siteSettings.Bills, filename)); - if (!fi.Exists) return NotFound(new { Error = "Professional signature not found" }); + if (!fi.Exists) return NotFound(new { Error = "Client signature not found" }); return File(fi.OpenRead(), "application/x-pdf", filename); } @@ -401,79 +407,12 @@ namespace Yavsc.ApiControllers Strokes = body.Strokes, }; - // Disk write first: a disk failure shouldn't leave - // a Signature row pointing at a file that doesn't - // exist. The file helper throws on filesystem - // problems and propagates here. - FileReceivedInfo fi; - try - { - fi = await User.ReceiveEstimateSignatureAsync(id, type, payload, - siteSettings, token); - } - catch (Exception ex) - { - logger.LogError(ex, "estimate {Id}: signature file write failed", id); - return BadRequest(new { Error = "file write failed", Detail = ex.Message }); - } - - // Find-or-add: the (EstimateId, Type) pair is - // unique, so a second POST for the same side of the - // estimate replaces the previous signature. EF - // translates this into a single UPDATE when the - // row exists and an INSERT otherwise; the unique - // index in ApplicationDbContext is the - // database-level guarantee that the contract - // holds if two requests race. - var signature = await dbContext.Signatures - .FirstOrDefaultAsync(s => s.EstimateId == id && s.Type == type, token); - - if (signature is null) - { - signature = new Signature - { - EstimateId = id, - SignerId = userId, - Type = type, - }; - dbContext.Signatures.Add(signature); - } - else - { - // Roll the signer's quota back by the size of - // the file we're about to orphan: the old - // FilePath is no longer referenced once we - // overwrite FilePath below. - try - { - var orphan = new FileInfo(signature.FilePath); - if (orphan.Exists) - { - var signerForOrphan = await dbContext.Users - .FirstOrDefaultAsync(u => u.Id == userId, token); - if (signerForOrphan is not null) - signerForOrphan.DiskUsage = - Math.Max(0, signerForOrphan.DiskUsage - orphan.Length); - } - } - catch { /* best effort — the file is being replaced anyway */ } - } - - signature.SignerId = userId; - signature.CoordinateMax = payload.CoordinateMax; - signature.Strokes = payload.Strokes; - signature.CapturedAtUtc = payload.CapturedAtUtc; - signature.FilePath = Path.Combine(fi.DestDir, fi.FileName); - - // Bump the signer's quota. The Signature row's - // SignerId is the IdentityUser.Id (a string), so we - // look up by Id and not by username. - var signer = await dbContext.Users - .FirstOrDefaultAsync(u => u.Id == userId, token); - if (signer is not null) - { - signer.DiskUsage += new FileInfo(signature.FilePath).Length; - } + // The Signature row is staged by the shared persister; we + // SaveChanges here so the signature write is one + // transaction. The Strokes column is the source of truth — + // there is no on-disk copy to roll back on failure. + var signature = await EstimateSignaturePersister.StageAsync( + dbContext, id, type, userId, payload, token); try { @@ -482,10 +421,6 @@ namespace Yavsc.ApiControllers catch (Exception ex) { logger.LogError(ex, "estimate {Id}: signature db write failed", id); - // Best-effort rollback: remove the file we wrote - // so disk and db don't disagree. - try { System.IO.File.Delete(signature.FilePath); } - catch { /* swallow — the row will be re-orphaned, the user re-signs */ } return BadRequest(new { Error = "db write failed", Detail = ex.Message }); } diff --git a/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs b/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs index b72c6d7a2..92e89e28e 100644 --- a/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/EstimateApiController.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Yavsc.Models; using Yavsc.Models.Billing; +using Yavsc.Models.Blog; using Yavsc.Server.Helpers; namespace Yavsc.Controllers @@ -40,7 +41,7 @@ namespace Yavsc.Controllers else if (!UserIsAdminOrThis(ownerId)) // throw new Exception("Not authorized") ; // or just do nothing return new StatusCodeResult(StatusCodes.Status403Forbidden); - return Ok(_context.Estimates.Include(e => e.Bill).Where(e => e.OwnerId == ownerId)); + return Ok(_context.Estimates.Include(e => e.Bill).Include(e => e.Signatures).Where(e => e.OwnerId == ownerId)); } // GET: api/Estimate/5 [HttpGet("{id}", Name = "GetEstimate")] @@ -51,7 +52,7 @@ namespace Yavsc.Controllers return BadRequest(ModelState); } - Estimate estimate = _context.Estimates.Include(e => e.Bill).Single(m => m.Id == id); + Estimate estimate = _context.Estimates.Include(e => e.Bill).Include(e => e.Signatures).Single(m => m.Id == id); if (estimate == null) { @@ -201,23 +202,31 @@ namespace Yavsc.Controllers // GET: api/estimate/asclient // Ongoing estimates (not yet validated by the client) where the // current user is the client who initiated the request. + // An estimate only becomes visible to the client once the provider + // has actually established and submitted it (ProviderValidationDate); + // a freshly inserted request the provider hasn't worked on yet is + // not something the client can validate, so it's excluded. [HttpGet("asclient")] public IActionResult GetOngoingEstimatesAsClient() { var uid = User.GetUserId(); - return Ok(_context.Estimates.Include(e => e.Bill) - .Where(e => e.ClientId == uid && e.ClientValidationDate == default)); + return Ok(_context.Estimates.Include(e => e.Bill).Include(e => e.Signatures) + .Where(e => e.ClientId == uid + && e.ProviderValidationDate != default + && e.ClientValidationDate == default)); } // GET: api/estimate/asprovider - // Ongoing estimates (not yet validated by the client) established - // by the current user as the provider. + // Estimates awaiting the provider's signature, established by the + // current user as the provider. Once the provider validates + // (ProviderValidationDate stamped), the estimate leaves this list + // and appears in the client's "awaiting client signature" list. [HttpGet("asprovider")] public IActionResult GetOngoingEstimatesAsProvider() { var uid = User.GetUserId(); - return Ok(_context.Estimates.Include(e => e.Bill) - .Where(e => e.OwnerId == uid && e.ClientValidationDate == default)); + return Ok(_context.Estimates.Include(e => e.Bill).Include(e => e.Signatures) + .Where(e => e.OwnerId == uid && e.ProviderValidationDate == default)); } protected override void Dispose(bool disposing) @@ -229,6 +238,96 @@ namespace Yavsc.Controllers base.Dispose(disposing); } + // GET: api/v1/estimate/{id}/attachments + // + // Liste les fichiers de l'espace perso du fournisseur rattachés au + // devis, par référence (UploadedFile). Lecture ouverte aux deux + // parties (fournisseur/client) du devis et aux admin, comme + // GetEstimate : la contrepartie doit pouvoir voir et télécharger + // les pièces (le téléchargement des octets reste servi par le host + // Blogs, qui autorise via cette même table de jointure). + [HttpGet("{id}/attachments")] + public IActionResult GetAttachments(long id) + { + var estimate = _context.Estimates.FirstOrDefault(e => e.Id == id); + if (estimate == null) return NotFound(); + if (!UserIsAdminOrInThese(estimate.ClientId, estimate.OwnerId)) + return new StatusCodeResult(StatusCodes.Status403Forbidden); + + var attached = _context.EstimateAttachedFiles + .Where(a => a.EstimateId == id) + .Select(a => new + { + fileId = a.FileId, + path = a.File.Path, + size = a.File.Length, + contentType = a.File.ContentType, + ownerId = a.File.OwnerId + }) + .ToList(); + return Ok(attached); + } + + // POST: api/v1/estimate/{id}/attachments { fileId } + // + // Attache un fichier de l'espace perso du fournisseur au devis, par + // référence. Réservé au fournisseur (estimate.OwnerId == caller) : + // seul le fournisseur constitue le devis et y joint ses documents + // (dont les documents à faire signer par le client, per README). + // Le fichier doit appartenir au fournisseur (UploadedFile.OwnerId). + [HttpPost("{id}/attachments")] + public IActionResult AttachFile(long id, [FromBody] AttachFileRequest body) + { + if (!ModelState.IsValid) return BadRequest(ModelState); + var estimate = _context.Estimates.FirstOrDefault(e => e.Id == id); + if (estimate == null) return NotFound(); + + var uid = User.GetUserId(); + if (!User.IsInRole(Constants.AdminGroupName) && uid != estimate.OwnerId) + return new StatusCodeResult(StatusCodes.Status403Forbidden); + + if (body == null || body.FileId <= 0) + return BadRequest(new { error = "fileId required" }); + + var file = _context.UploadedFiles.FirstOrDefault(f => f.Id == body.FileId); + if (file == null) return BadRequest(new { error = "file not found" }); + if (file.OwnerId != uid) + return new StatusCodeResult(StatusCodes.Status403Forbidden); + + var existing = _context.EstimateAttachedFiles + .FirstOrDefault(a => a.EstimateId == id && a.FileId == body.FileId); + if (existing != null) return Ok(new { estimateId = id, fileId = body.FileId }); + + _context.EstimateAttachedFiles.Add(new EstimateAttachedFile + { + EstimateId = id, + FileId = body.FileId + }); + _context.SaveChanges(User.GetUserId()); + return Ok(new { estimateId = id, fileId = body.FileId }); + } + + // DELETE: api/v1/estimate/{id}/attachments/{fileId} + // + // Détache un fichier du devis. Réservé au fournisseur. + [HttpDelete("{id}/attachments/{fileId}")] + public IActionResult DetachFile(long id, long fileId) + { + var estimate = _context.Estimates.FirstOrDefault(e => e.Id == id); + if (estimate == null) return NotFound(); + + var uid = User.GetUserId(); + if (!User.IsInRole(Constants.AdminGroupName) && uid != estimate.OwnerId) + return new StatusCodeResult(StatusCodes.Status403Forbidden); + + var link = _context.EstimateAttachedFiles + .FirstOrDefault(a => a.EstimateId == id && a.FileId == fileId); + if (link == null) return NotFound(); + _context.EstimateAttachedFiles.Remove(link); + _context.SaveChanges(User.GetUserId()); + return Ok(new { estimateId = id, fileId }); + } + private bool EstimateExists(long id) { return _context.Estimates.Count(e => e.Id == id) > 0; diff --git a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs index 3adac5b03..e5b89c4a9 100644 --- a/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/FrontOfficeApiController.cs @@ -1,10 +1,18 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Yavsc.Abstract.Workflow; using Yavsc.Helpers; using Yavsc.Models; +using Yavsc.Models.Billing; +using Yavsc.Models.Blog; using Yavsc.Services; using Yavsc.Server.Helpers; using Yavsc.ViewModels.FrontOffice; +using Yavsc.ViewModels.Gen; namespace Yavsc.ApiControllers { @@ -13,13 +21,38 @@ namespace Yavsc.ApiControllers public class FrontOfficeApiController : Controller { ApplicationDbContext dbContext; + private readonly IBillingService billing; + private readonly ILogger logger; + private readonly SiteSettings siteSettings; + private readonly IRazorViewEngine viewEngine; - private IBillingService billing; - - public FrontOfficeApiController(ApplicationDbContext context, IBillingService billing = null) + // The view-rendering dependencies (siteSettings, viewEngine) are + // optional: only EstimateTex/EstimatePdf use them, and those run in + // the Api host where Program.cs wires AddControllersWithViews + + // SiteSettings. The accept/reject endpoints (and the Api integration + // test host, which composes with AddControllers alone) never touch + // them, so they must not be required for controller activation. + // + // .NET 10 does not register a single IViewEngine service (the view + // engine is registered as IRazorViewEngine and surfaced through + // MvcViewOptions.ViewEngines). IRazorViewEngine (RazorViewEngine) + // implements IViewEngine, so we inject that and hand it to + // TeXHelpers.RenderViewToString as an IViewEngine. RenderViewToString + // builds its ActionContext from the controller's own ControllerContext, + // so no IActionContextAccessor is needed (and that type is deprecated + // in .NET 10). + public FrontOfficeApiController( + ApplicationDbContext context, + ILoggerFactory loggerFactory, + IOptions siteSettings = null, + IRazorViewEngine viewEngine = null, + IBillingService billing = null) { dbContext = context; this.billing = billing ?? new BillingService(context); + logger = loggerFactory.CreateLogger(); + this.siteSettings = siteSettings?.Value; + this.viewEngine = viewEngine; } [HttpGet("profiles/{actCode}")] @@ -28,29 +61,402 @@ namespace Yavsc.ApiControllers return await dbContext.ListPerformersAsync(billing, actCode); } - [HttpPost("query/reject")] - public IActionResult RejectQuery(string billingCode, long queryId) + // GET: api/front/query/{queryId}/estimate.tex + // + // Renders the estimate linked to the query (Estimate.CommandId == + // queryId) as a LaTeX source document. The view is a generation + // template (Layout = "null"), not a web page: it is returned with + // a text/x-tex content type so the caller can save it as a .tex + // file. Either party (client or provider) or an admin may read it. + [HttpGet("query/{queryId}/estimate.tex")] + public async Task EstimateTex(long queryId, CancellationToken token) { - if (billingCode == null) return BadRequest("billingCode"); - if (queryId == 0) return BadRequest("queryId"); - var billing = BillingService.GetBillable(dbContext, billingCode, queryId); - if (billing == null) return BadRequest(); + if (viewEngine is null || siteSettings is null) + return Problem("TeX rendering is not configured on this host.", + statusCode: StatusCodes.Status503ServiceUnavailable); - billing.Status = QueryStatus.Rejected; - dbContext.SaveChanges(User.GetUserId()); - return Ok(); + var estimate = await LoadEstimateForRenderAsync(queryId, token); + if (estimate is null) return NotFound(new { Error = "no estimate linked to this query" }); + + var uid = User.GetUserId(); + if (!User.IsInRole(Constants.AdminGroupName) + && uid != estimate.ClientId && uid != estimate.OwnerId) + return Forbid(); + + PrepareEstimateViewBag(estimate); + Response.ContentType = "text/x-tex"; + return View("Estimate_tex", estimate); } - [HttpPost("query/accept")] - public IActionResult AcceptQuery(string billingCode, long queryId) + // GET: api/front/query/{queryId}/estimate.pdf + // + // Renders the same LaTeX template to a string, compiles it to PDF + // via texi2pdf (see SiteSettings.GenerateEstimatePdf), and streams + // the generated PDF bytes back. Requires /usr/bin/texi2pdf on the + // server host; on hosts without it the endpoint returns a problem. + [HttpGet("query/{queryId}/estimate.pdf")] + public async Task EstimatePdf(long queryId, CancellationToken token) { - if (billingCode == null) return BadRequest("billingCode"); - if (queryId == 0) return BadRequest("queryId"); - var billing = BillingService.GetBillable(dbContext, billingCode, queryId); - if (billing == null) return BadRequest(); - billing.Status = QueryStatus.Accepted; - dbContext.SaveChanges(User.GetUserId()); - return Ok(); + if (viewEngine is null || siteSettings is null) + { + string msg = "PDF rendering is not configured on this host."; + logger.LogError(msg); + return Problem(msg, + statusCode: StatusCodes.Status503ServiceUnavailable); + + } + + var estimate = await LoadEstimateForRenderAsync(queryId, token); + if (estimate is null) return NotFound(new { Error = "no estimate linked to this query" }); + + var uid = User.GetUserId(); + if (!User.IsInRole(Constants.AdminGroupName) + && uid != estimate.ClientId && uid != estimate.OwnerId) + return Forbid(); + + PrepareEstimateViewBag(estimate); + + var baseFileName = $"estimate-{estimate.Id}"; + // Resolve the bills dir to an absolute path once: appsettings + // configures it as a relative path ("bills"), and both the + // generation (GenerateEstimatePdf) and the read-back below must + // agree on the same location regardless of the host's cwd. + var billsDir = new System.IO.DirectoryInfo(siteSettings.Bills).FullName; + var model = new PdfGenerationViewModel + { + BaseFileName = baseFileName, + DestDir = billsDir, + Temp = siteSettings.TempDir, + }; + + try + { + // 1. Render the LaTeX template (Estimate_tex) to a TeX source + // string, using the estimate as the view model. + model.TeXSource = this.RenderViewToString(viewEngine, "Estimate_tex", estimate); + // 2. Render the generation view (Estimate_pdf). Its @{} block + // calls SiteSettings.GenerateEstimatePdf(Model), which + // compiles the TeX to PDF on disk as a side effect of the + // render. The HTML the view emits is not what we return — + // we stream the generated PDF bytes — but driving the + // generation through the view keeps the template as the + // output producer, per the original design. + if (model.GenerateEstimatePdf() is FileInfo fo && fo.Exists) + { + return File(fo.OpenRead(), "application/pdf"); + } + string msg = "TeX render failed: " + model.GenerationErrorMessage; + logger.LogError(msg); + return Problem(msg); + + } + catch (Exception ex) + { + logger.LogError(ex, "estimate {QueryId}: TeX/PDF render failed", queryId); + return Problem("TeX render failed: " + ex.Message); + } + } + + // Loads the estimate linked to a query with the navigation the + // Estimate_tex template reads (client + performer profile + bill + // + the captured signatures, drawn as tikz at the foot of the devis). + private Task LoadEstimateForRenderAsync(long queryId, CancellationToken token) + => dbContext.Estimates + .Where(e => e.CommandId == queryId) + .OrderByDescending(e => e.Id) + .Include(e => e.Bill) + .Include(e => e.Signatures) + .Include(e => e.Query).ThenInclude(q => q!.Client).ThenInclude(c => c!.PostalAddress) + .Include(e => e.Query).ThenInclude(q => q!.PerformerProfile).ThenInclude(p => p!.OrganizationAddress) + .Include(e => e.Query).ThenInclude(q => q!.PerformerProfile).ThenInclude(p => p!.Performer) + .FirstOrDefaultAsync(token); + + // Sets the ViewBag keys the Estimate_tex template reads. A devis + // is never a bill (AsBill=false) and is never acquitted. + private void PrepareEstimateViewBag(Estimate estimate) + { + ViewBag.AsBill = false; + ViewBag.Acquitted = false; + ViewBag.BillsDir = new System.IO.DirectoryInfo(siteSettings.Bills).FullName; + ViewBag.AvatarsDir = new System.IO.DirectoryInfo(siteSettings.Avatars).FullName; + } + + // POST: api/front/query/accept + // + // Either party of the query may accept: the provider + // (PerformerId) accepts the client's request, or the client + // (ClientId) accepts the provider's estimate. An optional + // signature (the PostIt wire format) may be sent along; it is + // persisted on the estimate linked to the query + // (Estimate.CommandId == queryId), and the matching validation + // date is stamped — ProviderValidationDate when the provider + // accepts, ClientValidationDate when the client accepts. + [HttpPost("query/accept")] + public async Task AcceptQuery( + string billingCode, + long queryId, + [FromBody] QueryAcceptanceRequest? body, + CancellationToken token) + { + var (query, role, error) = await ResolveAndAuthorizeAsync(billingCode, queryId, token); + if (error is not null) return error; + if (query is null) return BadRequest(new { Error = "query not found" }); + + Signature? signature = null; + // actingRole is the role the caller is signing / accepting + // as. It defaults to the role inferred from the caller's + // identity, but when a signature is submitted the caller + // declares the side (Pro/Client) and the server honors it + // only if the caller is that party. This disambiguates a + // user who is both parties (where inference would always + // pick the provider) and makes "write only for its author" + // explicit. + ActorRole actingRole = role; + if (HasSignature(body)) + { + // Admins bypass authorization but are neither party of + // the estimate, so they cannot sign. + if (role == ActorRole.Admin) + return BadRequest(new { Error = "admin cannot sign on behalf of a party" }); + + var estimate = await dbContext.Estimates + .Where(e => e.CommandId == queryId) + .OrderByDescending(e => e.Id) + .FirstOrDefaultAsync(token); + if (estimate is null) + return BadRequest(new { Error = "no estimate linked to this query" }); + + var uid = User.GetUserId(); + bool isProvider = query.PerformerId == uid; + bool isClient = query.ClientId == uid; + + // The caller declares which side they sign as. Null + // falls back to the role inferred from identity. + var declared = body!.SignatureType; + if (declared.HasValue) + { + if (declared.Value == SignatureType.Pro && !isProvider) + return Forbid(); + if (declared.Value == SignatureType.Client && !isClient) + return Forbid(); + actingRole = declared.Value == SignatureType.Pro + ? ActorRole.Provider + : ActorRole.Client; + } + + var type = actingRole == ActorRole.Provider + ? SignatureType.Pro + : SignatureType.Client; + + var payload = new SignaturePadPayload + { + CoordinateMax = body.CoordinateMax, + CapturedAtUtc = body.CapturedAtUtc ?? DateTime.UtcNow, + Strokes = body.Strokes!, + }; + + try + { + signature = await EstimateSignaturePersister.StageAsync( + dbContext, estimate.Id, type, uid, payload, token); + } + catch (Exception ex) + { + logger.LogError(ex, "query {QueryId}: signature stage failed", queryId); + return BadRequest(new { Error = "signature stage failed", Detail = ex.Message }); + } + + if (actingRole == ActorRole.Provider) + estimate.ProviderValidationDate = DateTime.UtcNow; + else + estimate.ClientValidationDate = DateTime.UtcNow; + } + + // The party-specific status records who accepted: the + // provider (ProAccepted) or the client (ClientAccepted). + // An admin accepting (neither party) falls back to the + // legacy generic Accepted. + query.Status = actingRole switch + { + ActorRole.Provider => QueryStatus.ProAccepted, + ActorRole.Client => QueryStatus.ClientAccepted, + _ => QueryStatus.Accepted, + }; + + try + { + await dbContext.SaveChangesAsync(User.GetUserId(), token); + } + catch (Exception ex) + { + logger.LogError(ex, "query {QueryId}: accept db write failed", queryId); + return BadRequest(new { Error = "db write failed", Detail = ex.Message }); + } + + return Ok(new + { + queryId, + status = query.Status.ToString(), + signature = signature is null + ? null + : new + { + id = signature.Id, + estimateId = signature.EstimateId, + type = signature.Type.ToString(), + }, + }); + } + + // POST: api/front/query/reject + // + // Either party may reject. A signature is not meaningful on a + // rejection, so one is refused with 400. + [HttpPost("query/reject")] + public async Task RejectQuery( + string billingCode, + long queryId, + [FromBody] QueryAcceptanceRequest? body, + CancellationToken token) + { + if (HasSignature(body)) + return BadRequest(new { Error = "signature not allowed on reject" }); + + var (query, role, error) = await ResolveAndAuthorizeAsync(billingCode, queryId, token); + if (error is not null) return error; + if (query is null) return BadRequest(new { Error = "query not found" }); + + query.Status = QueryStatus.Rejected; + await dbContext.SaveChangesAsync(User.GetUserId(), token); + + return Ok(new + { + queryId, + status = query.Status.ToString(), + }); + } + + // Loads the query and checks that the caller is a party to it + // (provider or client) or an admin. Returns the role the caller + // is acting in; Admin is only returned when the caller is + // neither party but is an admin. + private async Task<(IQuery? Query, ActorRole Role, IActionResult? Error)> + ResolveAndAuthorizeAsync(string billingCode, long queryId, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(billingCode)) + return (null, ActorRole.None, BadRequest("billingCode")); + if (queryId == 0) + return (null, ActorRole.None, BadRequest("queryId")); + + var query = await billing.GetBillAsync(billingCode, queryId); + if (query is null) + return (null, ActorRole.None, BadRequest()); + + var uid = User.GetUserId(); + bool isProvider = query.PerformerId == uid; + bool isClient = query.ClientId == uid; + + if (isProvider) return (query, ActorRole.Provider, null); + if (isClient) return (query, ActorRole.Client, null); + + if (User.IsInRole(Constants.AdminGroupName)) + return (query, ActorRole.Admin, null); + + return (null, ActorRole.None, Forbid()); + } + + private static bool HasSignature(QueryAcceptanceRequest? body) + => body is not null && body.Strokes is not null && body.Strokes.Length > 0; + + // GET: api/v1/front/query/{queryId}/attachments?billingCode=… + // + // Liste les fichiers de l'espace perso du client rattachés à la + // demande, par référence (UploadedFile). Lecture ouverte aux deux + // parties (client/fournisseur) de la demande et aux admin ; le + // téléchargement des octets est servi par le host Blogs. + [HttpGet("query/{queryId}/attachments")] + public async Task GetQueryAttachments( + string billingCode, long queryId, CancellationToken token) + { + var (query, role, error) = await ResolveAndAuthorizeAsync(billingCode, queryId, token); + if (error is not null) return error; + if (query is null) return BadRequest(new { Error = "query not found" }); + + var attached = await dbContext.QueryAttachedFiles + .Where(a => a.CommandId == queryId) + .Select(a => new + { + fileId = a.FileId, + path = a.File.Path, + size = a.File.Length, + contentType = a.File.ContentType, + ownerId = a.File.OwnerId + }) + .ToListAsync(token); + return Ok(attached); + } + + // POST: api/v1/front/query/{queryId}/attachments?billingCode=… { fileId } + // + // Attache un fichier de l'espace perso du client à sa demande, par + // référence. Réservé au client (rôle Client) : c'est lui qui + // documente sa demande. Le fichier doit lui appartenir. + [HttpPost("query/{queryId}/attachments")] + public async Task AttachQueryFile( + string billingCode, long queryId, [FromBody] AttachFileRequest? body, CancellationToken token) + { + var (query, role, error) = await ResolveAndAuthorizeAsync(billingCode, queryId, token); + if (error is not null) return error; + if (query is null) return BadRequest(new { Error = "query not found" }); + + if (role != ActorRole.Client && !User.IsInRole(Constants.AdminGroupName)) + return new StatusCodeResult(StatusCodes.Status403Forbidden); + + if (body == null || body.FileId <= 0) + return BadRequest(new { error = "fileId required" }); + + var uid = User.GetUserId(); + var file = await dbContext.UploadedFiles + .FirstOrDefaultAsync(f => f.Id == body.FileId, token); + if (file == null) return BadRequest(new { error = "file not found" }); + if (file.OwnerId != uid && !User.IsInRole(Constants.AdminGroupName)) + return new StatusCodeResult(StatusCodes.Status403Forbidden); + + var existing = await dbContext.QueryAttachedFiles + .FirstOrDefaultAsync(a => a.CommandId == queryId && a.FileId == body.FileId, token); + if (existing != null) return Ok(new { queryId, fileId = body.FileId }); + + dbContext.QueryAttachedFiles.Add(new QueryAttachedFile + { + CommandId = queryId, + FileId = body.FileId + }); + await dbContext.SaveChangesAsync(User.GetUserId(), token); + return Ok(new { queryId, fileId = body.FileId }); + } + + // DELETE: api/v1/front/query/{queryId}/attachments/{fileId}?billingCode=… + // + // Détache un fichier de la demande. Réservé au client. + [HttpDelete("query/{queryId}/attachments/{fileId}")] + public async Task DetachQueryFile( + string billingCode, long queryId, long fileId, CancellationToken token) + { + var (query, role, error) = await ResolveAndAuthorizeAsync(billingCode, queryId, token); + if (error is not null) return error; + if (query is null) return BadRequest(new { Error = "query not found" }); + + if (role != ActorRole.Client && !User.IsInRole(Constants.AdminGroupName)) + return new StatusCodeResult(StatusCodes.Status403Forbidden); + + var link = await dbContext.QueryAttachedFiles + .FirstOrDefaultAsync(a => a.CommandId == queryId && a.FileId == fileId, token); + if (link == null) return NotFound(); + dbContext.QueryAttachedFiles.Remove(link); + await dbContext.SaveChangesAsync(User.GetUserId(), token); + return Ok(new { queryId, fileId }); } } + + internal enum ActorRole { None, Provider, Client, Admin } } diff --git a/src/Yavsc.Api/Helpers/TeXHelpers.cs b/src/Yavsc.Api/Helpers/TeXHelpers.cs new file mode 100644 index 000000000..c35806bda --- /dev/null +++ b/src/Yavsc.Api/Helpers/TeXHelpers.cs @@ -0,0 +1,325 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Yavsc.Models.Billing; + +namespace Yavsc.Helpers +{ + using Microsoft.AspNetCore.Mvc.Razor; + using ViewModels.Gen; + public class TeXString : HtmlString + { + + public TeXString(TeXString teXString): base(teXString.ToString()) + { + } + + public TeXString(string str) : base(str) + { + + + } + + public static TeXString operator+ (TeXString a, TeXString b) { + return new TeXString(a.ToString()+b.ToString()); + } + + } + public class Replacement + { + string target; + string replacement; + public Replacement(string target, string replacement) + { + this.target = target; + this.replacement = replacement; + } + public string Execute(string source) + { + return source?.Replace(target, replacement) ?? null; + } + } + public static class TeXHelpers + { + public static readonly Replacement[] SpecialCharsDefaultRendering = + { + new Replacement("<","\\textless"), + new Replacement(">","\\textgreater"), + new Replacement("©","\\copyright"), + new Replacement("®","\\textregistered"), + new Replacement("\\","\\textbackslash"), + new Replacement("™","\\texttrademark"), + new Replacement("¶","\\P"), + new Replacement("|","\\textbar"), + new Replacement("%","\\%"), + new Replacement("{","\\{"), + new Replacement("}","\\}"), + new Replacement("_","\\_"), + new Replacement("#","\\#"), + new Replacement("$","\\$"), + new Replacement("_","\\_"), + new Replacement("¿","\\textquestiondown"), + new Replacement("§","\\S"), + new Replacement("£","\\pounds"), + new Replacement("&","\\&"), + new Replacement("¡","\\textexclamdown"), + new Replacement("†","\\dag"), + new Replacement("–","\\textendash"), + new Replacement("°","\\textdegree") + }; + + public static TeXString ToTeX(this string source, string defaultValue="\\textit{néant}") + { + if (source==null) return new TeXString(defaultValue); + string result=source; + foreach (var r in SpecialCharsDefaultRendering) + { + result = r.Execute(result); + } + return new TeXString(result); + } + + public static TeXString ToTeXCell(this string source, string defaultValue="\\textit{néant}") + { + if (source==null) return new TeXString(defaultValue); + string result=source; + foreach (var r in SpecialCharsDefaultRendering) + { + result = r.Execute(result); + } + result = result.Replace("\n","\\tabularnewline "); + return new TeXString(result); + } + + + public static string NewLinesWith(this string target, string separator) + { + var items = target.Split(new char[] { '\n' }).Where( + s => !string.IsNullOrWhiteSpace(s)); + + return string.Join(separator, items); + } + + public static TeXString ToTeXLines(this string source, string defaultValue, string lineSeparator = "\n\\\\") + { + if (source == null) return new TeXString(defaultValue); + return new TeXString( source.ToTeX().ToString().NewLinesWith(lineSeparator) ); + } + + public static TeXString SplitAddressToTeX (this string source, string lineSeparator = "\n\\\\", string defaultValue = "\\textit{pas d'adresse postale}") + { + if (string.IsNullOrWhiteSpace(source)) return new TeXString(defaultValue); + var alines = source.Split(','); + var texlines = alines.Select(l=>l.ToTeX().ToString()); + return new TeXString(string.Join(lineSeparator,texlines)); + } + + /// + /// Renders a captured signature as TikZ \draw/\fill + /// paths, suitable for embedding in a tikzpicture at the + /// foot of a devis. The signature is the PostIt wire format — a + /// length-prefixed sequence of strokes, each stroke being + /// [k, x0, y0, x1, y1, …] with coordinates normalised to + /// [0, CoordinateMax] and y growing downward (screen + /// orientation). We denormalise to a × + /// canvas and flip y so the signature + /// appears upright on paper. A single-point stroke (a pen-down + /// tap) is rendered as a filled dot, mirroring the round-capped + /// Polyline the Avalonia pad draws for it. + /// + /// The persisted signature. Null/empty yields + /// , so the caller can gate the + /// tikzpicture on a non-empty result. + public static HtmlString SignatureToTikz( + this Signature sig, double widthCm, double heightCm) + { + if (sig is null || sig.Strokes is null || sig.Strokes.Length == 0) + return HtmlString.Empty; + + double max = sig.CoordinateMax > 0 ? sig.CoordinateMax : 10_000.0; + var strokes = sig.Strokes; + var sb = new StringBuilder(); + + int i = 0; + while (i < strokes.Length) + { + int k = strokes[i]; + if (k <= 0) break; + if (i + 1 + 2 * k > strokes.Length) break; + i++; // skip the length prefix + + // A single-point stroke: a tap. Draw a round dot so it + // shows instead of a zero-length, invisible \draw. + if (k == 1) + { + double px = strokes[i] / max * widthCm; + double py = heightCm - strokes[i + 1] / max * heightCm; + sb.Append(CultureInfo.InvariantCulture, + $"\\fill ({px:F3},{py:F3}) circle (0.8pt); "); + i += 2; + continue; + } + + sb.Append("\\draw[line cap=round, line join=round, line width=1.5pt] "); + for (int p = 0; p < k; p++) + { + double px = strokes[i + 2 * p] / max * widthCm; + double py = heightCm - strokes[i + 2 * p + 1] / max * heightCm; + if (p == 0) sb.Append(CultureInfo.InvariantCulture, + $"({px:F3},{py:F3})"); + else sb.Append(CultureInfo.InvariantCulture, + $" -- ({px:F3},{py:F3})"); + } + sb.Append("; "); + i += 2 * k; + } + + return new HtmlString(sb.ToString()); + } + + public static FileInfo GenerateEstimatePdf(this PdfGenerationViewModel Model) + { + string errorMsg = null; + // Resolve to an absolute path: SiteSettings.Bills may be a + // relative path (e.g. "bills"), and lualatex resolves + // -output-directory against its working directory — passing a + // relative path to both would nest them (/bills/bills) and + // the log/pdf write would fail. + var billdir = new DirectoryInfo(Model.DestDir).FullName; + string name = Model.BaseFileName; + FileInfo fo = new FileInfo(System.IO.Path.Combine(billdir, name + ".pdf")); + System.IO.Directory.CreateDirectory(billdir); + + // Compile the LaTeX source with the system lualatex, feeding it + // the TeX on standard input (no intermediate .tex file) and + // directing the resulting .pdf straight into the bills + // directory. lualatex is a file-based engine — it cannot emit + // the PDF on stdout — so the pdf lands at its final destination + // and we only clean the transient aux/log artifacts beside it. + // + // We pass "/dev/stdin" as the input filename so lualatex treats + // stdin as a regular input *file* rather than the terminal: with + // no filename argument it reads stdin in terminal mode, which + // emergency-stops after the first line of a multi-line document. + // /dev/stdin makes the full multi-line TeX compile cleanly. (The + // API host is Linux — the systemd service and lualatex dep are + // Linux-only too.) + // + // We capture stdout/stderr so a LaTeX error (missing .sty, + // missing \includegraphics target, …) is surfaced in + // GenerationErrorMessage instead of being buried in the .log + // we then delete. + int exitCode; + string stdout = null, stderr = null; + try + { + using (Process p = new Process()) + { + p.StartInfo = new ProcessStartInfo + { + UseShellExecute = false, + WorkingDirectory = billdir, + FileName = "lualatex", + Arguments = $"-interaction=nonstopmode -halt-on-error" + + $" -jobname=\"{name}\" -output-directory=\"{billdir}\"" + + " /dev/stdin", + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + p.Start(); + // Write the TeX source, then drain stdout/stderr. Read + // them after WaitForExit — lualatex's output is small + // (a few KB), well under the pipe buffer, so no deadlock. + using (var stdin = p.StandardInput) + { + stdin.Write(Model.TeXSource); + } + p.WaitForExit(); + exitCode = p.ExitCode; + stdout = p.StandardOutput.ReadToEnd(); + stderr = p.StandardError.ReadToEnd(); + } + } + catch (Exception ex) + { + errorMsg = "lualatex invocation failed: " + ex.Message; + exitCode = -1; + } + + if (exitCode != 0 && errorMsg == null) + { + // Surface the engine's own diagnostics (stderr carries the + // "! Emergency stop" / "! LaTeX Error: …" lines; stdout has + // the transcript banner) so the caller can see why it failed + // without having to find the (soon-deleted) .log file. + var diag = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr; + errorMsg = $"Pdf generation failed with exit code {exitCode}: {diag}"; + } + else if (!fo.Exists && errorMsg == null) + errorMsg = "Pdf generation produced no output"; + + // Remove the LaTeX build artifacts left beside the pdf so only + // .pdf persists in the bills directory. Keep the .log on + // failure so it can be inspected. + foreach (var ext in new[] { ".aux", ".log", ".out", ".fls", + ".fdb_latexmk", ".synctex.gz", ".toc" }) + { + if (ext == ".log" && !fo.Exists) continue; + var fileName = name + ext; + var f = new FileInfo(System.IO.Path.Combine(billdir, fileName)); + if (f.Exists) { try { f.Delete(); } catch { } } + } + + Model.Generated = fo.Exists; + Model.GenerationErrorMessage = new HtmlString(errorMsg); + return fo; + } + + public static string RenderViewToString( + this Controller controller, IRazorViewEngine engine, + string viewName, object model, bool isMainPage = true) + { + if (engine == null) + throw new InvalidOperationException("no engine"); + + // Build an ActionContext from the controller's own context — + // .NET 10 deprecated IActionContextAccessor, and the controller + // already holds the live request context on its ControllerContext. + var controllerContext = controller.ControllerContext; + var actionContext = new ActionContext( + controllerContext.HttpContext, + controllerContext.RouteData, + controllerContext.ActionDescriptor); + + ViewEngineResult viewResult = engine.FindView(actionContext, viewName, isMainPage); + if (!viewResult.Success) + throw new InvalidOperationException( + $"View '{viewName}' not found. Searched locations: " + + string.Join(", ", viewResult.SearchedLocations)); + + // Hand the model to the view through ViewData so the template's + // @model directive resolves it (the previous implementation never + // set ViewData.Model, leaving Model null inside the template). + controller.ViewData.Model = model; + + using (var sw = new StringWriter()) + { + var viewContext = new ViewContext( + actionContext, + viewResult.View, + controller.ViewData, + controller.TempData, + sw, + new HtmlHelperOptions()); + viewResult.View.RenderAsync(viewContext).GetAwaiter().GetResult(); + return sw.GetStringBuilder().ToString(); + } + } + } +} diff --git a/src/Yavsc.Api/Program.cs b/src/Yavsc.Api/Program.cs index be4ae6579..8daff34d9 100644 --- a/src/Yavsc.Api/Program.cs +++ b/src/Yavsc.Api/Program.cs @@ -55,7 +55,11 @@ internal class Program }); }) .AddYavscCors(builder.Configuration) - .AddControllers(); + .AddControllersWithViews(); + + // Binds the "Site" section (Bills, TempDir, Avatars, …) used by + // the document-generation flow and BillingController. + services.Configure(builder.Configuration.GetSection("Site")); // accepts any access token issued by identity server services.AddAuthentication("Bearer") diff --git a/src/Yavsc.Api/ViewModels/FrontOffice/QueryAcceptanceRequest.cs b/src/Yavsc.Api/ViewModels/FrontOffice/QueryAcceptanceRequest.cs new file mode 100644 index 000000000..41e4dd650 --- /dev/null +++ b/src/Yavsc.Api/ViewModels/FrontOffice/QueryAcceptanceRequest.cs @@ -0,0 +1,40 @@ +using Yavsc.Models.Billing; + +namespace Yavsc.ViewModels.FrontOffice +{ + /// + /// Body of POST /api/v1/front/query/accept and + /// .../reject. The field is optional: + /// when present (non-empty) on accept, it is the PostIt wire-format + /// signature captured with the acceptance. All fields are optional + /// so a caller may post {} to accept/reject without a + /// signature. + /// + public class QueryAcceptanceRequest + { + /// + /// Wire-format strokes. See + /// PostIt.Models.SignaturePadData. Null or empty means + /// no signature. + /// + public int[]? Strokes { get; set; } + + public int CoordinateMax { get; set; } = 10_000; + + public DateTime? CapturedAtUtc { get; set; } + + /// + /// Which signature is being submitted: + /// (the provider) or + /// . When + /// is present this declares the side the caller is signing as; + /// the server honors it only if the caller is that party + /// (query.PerformerId for Pro, query.ClientId + /// for Client). This disambiguates the case where one user is + /// both parties, and makes "write only for its author" + /// explicit. When null, the server falls back to the role + /// inferred from the caller's identity. + /// + public SignatureType? SignatureType { get; set; } + } +} diff --git a/src/Yavsc.Org/ViewModels/Gen/PdfGenerationViewModel.cs b/src/Yavsc.Api/ViewModels/Gen/PdfGenerationViewModel.cs similarity index 98% rename from src/Yavsc.Org/ViewModels/Gen/PdfGenerationViewModel.cs rename to src/Yavsc.Api/ViewModels/Gen/PdfGenerationViewModel.cs index fe934e3ac..841e8d5dd 100644 --- a/src/Yavsc.Org/ViewModels/Gen/PdfGenerationViewModel.cs +++ b/src/Yavsc.Api/ViewModels/Gen/PdfGenerationViewModel.cs @@ -5,7 +5,7 @@ namespace Yavsc.ViewModels.Gen { public class PdfGenerationViewModel { - + [Required] public string TeXSource { get; set; } [Required] diff --git a/src/Yavsc.Org/Views/Shared/Components/Bill/Bill_tex.cshtml b/src/Yavsc.Api/Views/Shared/Bill_tex.cshtml similarity index 99% rename from src/Yavsc.Org/Views/Shared/Components/Bill/Bill_tex.cshtml rename to src/Yavsc.Api/Views/Shared/Bill_tex.cshtml index 353d124e0..f0a3d2ad3 100644 --- a/src/Yavsc.Org/Views/Shared/Components/Bill/Bill_tex.cshtml +++ b/src/Yavsc.Api/Views/Shared/Bill_tex.cshtml @@ -3,7 +3,7 @@ @using System.Globalization @model IBillable @{ - Layout = "null"; + Layout = null; var pro = ViewBag.PerformerProfile; var from = ViewBag.Performer; var to = ViewBag.Client; diff --git a/src/Yavsc.Org/Views/Shared/Components/Estimate/Estimate_tex.cshtml b/src/Yavsc.Api/Views/Shared/Estimate_tex.cshtml similarity index 72% rename from src/Yavsc.Org/Views/Shared/Components/Estimate/Estimate_tex.cshtml rename to src/Yavsc.Api/Views/Shared/Estimate_tex.cshtml index 4c35d4bdd..bbe4326b4 100644 --- a/src/Yavsc.Org/Views/Shared/Components/Estimate/Estimate_tex.cshtml +++ b/src/Yavsc.Api/Views/Shared/Estimate_tex.cshtml @@ -3,7 +3,7 @@ @using System.Globalization @model Estimate @{ - Layout = "null"; + Layout = null; var pro = Model.Query.PerformerProfile; var from = Model.Query.PerformerProfile.Performer; var to = Model.Query.Client; @@ -11,7 +11,6 @@ var proaddr = Model.Query.PerformerProfile.OrganizationAddress.Address.SplitAddressToTeX(); var proaddrm = new HtmlString(Model.Query.PerformerProfile.OrganizationAddress.Address); var isestimate = !ViewBag.AsBill; - var prosign = new FileInfo($"{ViewBag.BillsDir}/estimate-prosign-{Model.Id}.png"); } \documentclass[french,11pt]{article} @@ -25,6 +24,7 @@ \usepackage{graphicx} \usepackage{fancyhdr} \usepackage{fp} +\usepackage{tikz} \def\TVA{20} % Taux de la TVA @@ -49,7 +49,7 @@ \global\let\TotalHT\TotalHT \global\let\TotalTVA\TotalTVA \global\let\TotalTTC\TotalTTC - \cr + \cr \hline \textbf{Total} & & & \TotalHT } @@ -77,12 +77,12 @@ % Infos Client \def\ClientNom{@TeXHelpers.ToTeX(to.UserName)} % Nom du client -\def\ClientAdresse{ +\def\ClientAdresse{ % Adresse du client @PostalAddress @if (PostalAddress!=null) {@PostalAddress} @if (!string.IsNullOrWhiteSpace(to.PhoneNumber)) {@TeXHelpers.ToTeX(to.PhoneNumber)\\ - } + } E-mail: @TeXHelpers.ToTeX(to.Email) } @@ -105,7 +105,7 @@ \renewcommand{\headrulewidth}{0pt} \cfoot{ @TeXHelpers.ToTeX(from.UserName) @if (proaddrm!=null) { - @proaddrm } \newline - \small{ E-mail: @TeXHelpers.ToTeX(from.Email) @if (!string.IsNullOrWhiteSpace(from.PhoneNumber)) { - Téléphone fixe: @TeXHelpers.ToTeX(from.PhoneNumber) } + \small{ E-mail: @TeXHelpers.ToTeX(from.Email) @if (!string.IsNullOrWhiteSpace(from.PhoneNumber)) { - Téléphone fixe: @TeXHelpers.ToTeX(from.PhoneNumber) } } } @@ -113,10 +113,10 @@ % Logo de la société @if (from.Avatar != null) { -\includegraphics[height=60pt]{@(ViewBag.AvatarsDir)/@(from.UserName).png} +\includegraphics[height=60pt, viewport=0 0 128 128]{@(ViewBag.AvatarsDir)/@(from.UserName).png} -} else { -%\includegraphics{logo.png} +} else { +%\includegraphics[height=60pt, viewport=0 0 128 128]{logo.png} } % Nom et adresse de la société @@ -170,12 +170,12 @@ \begin{center} \begin{tabular}{|c c c c|} @if (!string.IsNullOrWhiteSpace(bi.BankCode) && !string.IsNullOrWhiteSpace(bi.WicketCode) - && !string.IsNullOrWhiteSpace(bi.AccountNumber) ) { + && !string.IsNullOrWhiteSpace(bi.AccountNumber) ) { \hline \textbf{Code banque} & \textbf{Code guichet} & \textbf{N° de Compte} & \textbf{Clé RIB} \\ @bi.BankCode & @bi.WicketCode & @bi.AccountNumber & @bi.BankedKey \\ } - @if (!string.IsNullOrWhiteSpace(@bi.IBAN) && !string.IsNullOrWhiteSpace(@bi.BIC)) { + @if (!string.IsNullOrWhiteSpace(@bi.IBAN) && !string.IsNullOrWhiteSpace(@bi.BIC)) { \hline \textbf{IBAN N°} & \multicolumn{3}{|l|}{ @bi.IBAN } \\ \hline \textbf{Code BIC} & \multicolumn{3}{|l|}{ @bi.BIC } @@ -188,19 +188,46 @@ } -@if (!(Model.ProviderValidationDate==null)) { - -\begin{flushright} - @(new HtmlString(isestimate?"Devis validé":"Facture validée")) le @TeXHelpers.ToTeX(Model.ProviderValidationDate.ToString("dddd dd MMMM yyyy")) -\end{flushright} -@if (prosign.Exists) { - -\begin{center} - \hspace{263pt} - \includegraphics[height=60pt]{@(ViewBag.BillsDir)/estimate-prosign-@(Model.Id).png} -\end{center} - +@{ + // Captured signatures, if any. The Estimate.Signatures collection + // is loaded by the controller; SignaturePro/SignatureClient project + // the row by type. We draw them as vector TikZ at the foot of the + // devis (see TeXHelpers.SignatureToTikz), so there is no PNG to + // produce or \includegraphics to fail on. + var fr = CultureInfo.CreateSpecificCulture("fr-FR"); + var proSig = Model.SignaturePro; + var cliSig = Model.SignatureClient; } - + +\vspace{2em} +\noindent +\begin{minipage}[t]{0.45\textwidth} + \textbf{Signature du fournisseur}\\ +@if (Model.ProviderValidationDate != default(DateTime)) { +\small Validé le @TeXHelpers.ToTeX(Model.ProviderValidationDate.ToString("dddd dd MMMM yyyy", fr))\\ } +@if (proSig != null && proSig.Strokes != null && proSig.Strokes.Length > 0) { +\begin{tikzpicture} + \useasboundingbox (0,0) rectangle (5,2.5); + @proSig.SignatureToTikz(5, 2.5) +\end{tikzpicture} +} else { +\rule[-2.2cm]{5cm}{0.4pt} +} +\end{minipage}\hfill +\begin{minipage}[t]{0.45\textwidth} + \textbf{Signature du client}\\ +@if (Model.ClientValidationDate != default(DateTime)) { +\small Validé le @TeXHelpers.ToTeX(Model.ClientValidationDate.ToString("dddd dd MMMM yyyy", fr))\\ +} +@if (cliSig != null && cliSig.Strokes != null && cliSig.Strokes.Length > 0) { +\begin{tikzpicture} + \useasboundingbox (0,0) rectangle (5,2.5); + @cliSig.SignatureToTikz(5, 2.5) +\end{tikzpicture} +} else { +\rule[-2.2cm]{5cm}{0.4pt} +} +\end{minipage} + \end{document} diff --git a/src/Yavsc.Api/Views/_ViewImports.cshtml b/src/Yavsc.Api/Views/_ViewImports.cshtml new file mode 100644 index 000000000..788aed5ab --- /dev/null +++ b/src/Yavsc.Api/Views/_ViewImports.cshtml @@ -0,0 +1,12 @@ +@using Yavsc +@using Yavsc.Helpers +@using Yavsc.Billing +@using Yavsc.Models +@using Yavsc.Models.Billing +@using Yavsc.ViewModels.Gen +@using Microsoft.AspNetCore.Mvc +@using Microsoft.AspNetCore.Html + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +@inject Microsoft.Extensions.Options.IOptions SiteSettings \ No newline at end of file diff --git a/src/Yavsc.Api/Yavsc.Api.csproj b/src/Yavsc.Api/Yavsc.Api.csproj index 1a2accfc8..be545e5b0 100644 --- a/src/Yavsc.Api/Yavsc.Api.csproj +++ b/src/Yavsc.Api/Yavsc.Api.csproj @@ -7,8 +7,8 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 diff --git a/src/Yavsc.Blogs.Tests/Directory.Packages.props b/src/Yavsc.Blogs.Tests/Directory.Packages.props deleted file mode 100644 index eea5d34f9..000000000 --- a/src/Yavsc.Blogs.Tests/Directory.Packages.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs b/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs index b714af4e0..2ca4250c0 100644 --- a/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs +++ b/src/Yavsc.Blogs/Controllers/FileSystemApiController.cs @@ -8,10 +8,14 @@ namespace Yavsc.Blogs.Controllers { using Yavsc.Attributes.Validation; using Yavsc.Models; + using Yavsc.Models.Blog; + using Yavsc.Models.Billing; using Yavsc.Exceptions; using Yavsc.Server.Helpers; using Yavsc.Abstract.Helpers; using Yavsc.Server.Models.FileSystem; + using Yavsc.ViewModels.UserFiles; + using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; [Authorize,Route(APIPrefix + "/fs")] @@ -34,21 +38,66 @@ namespace Yavsc.Blogs.Controllers this.siteSettings = siteSettings.Value; } + // GET: api/v1/fs [HttpGet()] public IActionResult Get() { return GetDir(null); } + // GET: api/v1/fs/{subdir} + // Liste les fichiers du sous-répertoire personnel de l'utilisateur, + // en enrichissant chaque entrée de l'identifiant de métadonnées + // UploadedFile.Id quand une ligne existe (cf. POST ci-dessous). [HttpGet("{*subdir}")] public IActionResult GetDir([ValidRemoteUserFilePath] string subdir="") { if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState); - // _logger.LogInformation($"listing files from {User.Identity.Name}{subdir}"); - var files = FileSystemHelpers.GetUserFiles(siteSettings, User.GetUserId(), subdir); + var uid = User.GetUserId(); + // The personal-storage tree is keyed by the user's login + // (UserName): EnsureDestinationDirectory writes under + // {Blog}/{UserName}, the download endpoint and avatar paths + // use user.UserName too. GetUserId() returns the "sub" claim + // (a GUID in production), so passing uid here made the listing + // look under {Blog}/{sub-GUID} — an empty tree, hence the + // "Mes fichiers" page listing nothing. Resolve the login and + // list under it. OwnerId on UploadedFile stays "sub" (uid), + // so EnrichWithFileIds still queries by uid. + var user = dbContext.Users.SingleOrDefault(u => u.Id == uid); + if (user == null) return NotFound(); + var files = FileSystemHelpers.GetUserFiles(siteSettings, user.UserName, subdir); + EnrichWithFileIds(files, uid, subdir); return Ok(files); } + // Reporte l'UploadedFile.Id (quand il existe) sur chaque entrée + // retournée par la liste, pour que le client lourd puisse attacher + // le fichier à un devis/demande par référence. + void EnrichWithFileIds(UserDirectoryInfo dir, string ownerId, string subdir) + { + if (dir?.Files == null || dir.Files.Length == 0) return; + var relPaths = dir.Files + .Select(f => RelPath(subdir, f.Name)) + .ToHashSet(); + var byPath = dbContext.UploadedFiles + .Where(u => u.OwnerId == ownerId && relPaths.Contains(u.Path)) + .ToDictionary(u => u.Path, u => u.Id); + foreach (var f in dir.Files) + { + var rel = RelPath(subdir, f.Name); + if (byPath.TryGetValue(rel, out var id)) f.Id = id; + } + } + + // Chemin relatif au racine de l'espace perso de l'utilisateur, + // tel que stocké dans UploadedFile.Path (sans segment utilisateur). + static string RelPath(string subdir, string fileName) + { + // Normalize with '/' for cross-platform consistency in DB. + if (string.IsNullOrWhiteSpace(subdir)) return fileName; + return $"{subdir.TrimStart('/')}/{fileName}"; + } + [HttpPost("{*subdir}")] public IActionResult Post([ValidRemoteUserFilePath] string subdir="") { @@ -78,7 +127,38 @@ namespace Yavsc.Blogs.Controllers foreach (var f in Request.Form.Files) { var item = user.ReceiveUserFile(destDir, f); - dbContext.SaveChanges(User.GetUserId()); + if (!item.QuotaOffense) + { + // Persiste une ligne de métadonnées UploadedFile, + // propriétaire = l'utilisateur courant, chemin relatif + // à son espace perso. Upsert sur (OwnerId, Path) pour + // le re-téléversement (même fichier écrasé sur disque). + var rel = RelPath(subdir, item.FileName); + var existing = dbContext.UploadedFiles + .FirstOrDefault(u => u.OwnerId == uid && u.Path == rel); + if (existing == null) + { + var uploaded = new UploadedFile + { + OwnerId = uid, + Path = rel, + ContentType = f.ContentType, + Length = f.Length + }; + dbContext.UploadedFiles.Add(uploaded); + dbContext.SaveChanges(User.GetUserId()); + item.FileId = uploaded.Id; + } + else + { + existing.ContentType = f.ContentType; + existing.Length = f.Length; + dbContext.SaveChanges(User.GetUserId()); + item.FileId = existing.Id; + } + item.Length = f.Length; + item.ContentType = f.ContentType; + } received.Add(item); _logger.LogInformation($"Received '{item.FileName}'."); if (item.QuotaOffense) @@ -88,6 +168,62 @@ namespace Yavsc.Blogs.Controllers return Ok(received); } + // GET: api/v1/fs/file/{fileId} + // Télécharge une pièce jointe référencée par son UploadedFile.Id. + // Autorise : le propriétaire du fichier, ou toute partie (client / + // fournisseur) d'un devis ou d'une demande auquel ce fichier est + // rattaché via EstimateAttachedFile / QueryAttachedFile. Les octets + // sont lus depuis l'espace perso du propriétaire, sur le disque de + // ce host Blogs (le host API ne possède pas ces octets). + [HttpGet("file/{fileId:long}")] + public IActionResult GetFile(long fileId) + { + var file = dbContext.UploadedFiles.FirstOrDefault(u => u.Id == fileId); + if (file == null) return NotFound(new { error = "file not found" }); + + var uid = User.GetUserId(); + if (!CanReadFile(file, uid)) + return Forbid(); + + var ownerUserName = dbContext.Users + .Where(u => u.Id == file.OwnerId) + .Select(u => u.UserName) + .FirstOrDefault(); + if (string.IsNullOrEmpty(ownerUserName)) + return NotFound(new { error = "owner not found" }); + + var physicalPath = Path.Combine(siteSettings.Blog, ownerUserName, file.Path ?? ""); + var fi = new FileInfo(physicalPath); + if (!fi.Exists) return NotFound(new { error = "file not on disk" }); + + var contentType = string.IsNullOrWhiteSpace(file.ContentType) + ? "application/octet-stream" : file.ContentType; + var stream = fi.OpenRead(); + return File(stream, contentType, fi.Name); + } + + // Le propriétaire peut toujours lire son fichier ; sinon, il faut + // que le fichier soit rattaché à un devis ou une demande dont + // l'appelant est partie (client ou fournisseur). Les admin sont + // autorisés comme pour le reste du workflow. + bool CanReadFile(UploadedFile file, string uid) + { + if (User.IsInRole(Constants.AdminGroupName)) return true; + if (file.OwnerId == uid) return true; + + if (dbContext.EstimateAttachedFiles.Any(a => + a.FileId == file.Id && + (a.Estimate.ClientId == uid || a.Estimate.OwnerId == uid))) + return true; + + if (dbContext.QueryAttachedFiles.Any(a => + a.FileId == file.Id && + (a.Command.ClientId == uid || a.Command.PerformerId == uid))) + return true; + + return false; + } + [Route("/api/fsc/addquota/{uname}/{len}")] [Authorize("AdministratorOnly")] public IActionResult AddQuota(string uname, int len) diff --git a/src/Yavsc.Blogs/Yavsc.Blogs.csproj b/src/Yavsc.Blogs/Yavsc.Blogs.csproj index 28b5841c9..38941be79 100644 --- a/src/Yavsc.Blogs/Yavsc.Blogs.csproj +++ b/src/Yavsc.Blogs/Yavsc.Blogs.csproj @@ -8,8 +8,8 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 diff --git a/src/Yavsc.Blogs/appsettings-blogs.json b/src/Yavsc.Blogs/appsettings-blogs.json index 34134c937..5361bfc3b 100644 --- a/src/Yavsc.Blogs/appsettings-blogs.json +++ b/src/Yavsc.Blogs/appsettings-blogs.json @@ -26,10 +26,10 @@ "Kestrel": { "Endpoints": { "Http": { - "Url": "http://localhost:5002" + "Url": "http://blogs.pschneider.fr" }, "Https": { - "Url": "https://localhost:5003" + "Url": "https://blogs.pschneider.fr" } } } diff --git a/src/Yavsc.Org.Tests/Directory.Packages.props b/src/Yavsc.Org.Tests/Directory.Packages.props deleted file mode 100644 index af2ef948c..000000000 --- a/src/Yavsc.Org.Tests/Directory.Packages.props +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs b/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs deleted file mode 100644 index b7b4d3591..000000000 --- a/src/Yavsc.Org.Tests/EstimateSignatureFileHelperTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System.Security.Claims; -using System.Text.Json; -using Xunit; -using Yavsc.Models.Billing; -using Yavsc.Server.Helpers; -using Yavsc.Server.Models.FileSystem; - -namespace Yavsc.Org.Tests; - -/// -/// Tests for the static -/// helper. Scope is intentionally narrow: the file-naming format, -/// the strokes counter, and the on-disk write path. The controller -/// (authz, db persistence, signalR notification) is out of scope -/// for this commit and will get a dedicated integration test once -/// the Yavsc.Api test project is set up. -/// -public class EstimateSignatureFileHelperTests : IDisposable -{ - private readonly string _tempRoot; - private SiteSettings siteSettings; - - public EstimateSignatureFileHelperTests() - { - // UserFilesDirName is a process-wide static; we redirect - // it to a per-test temp dir so concurrent tests don't - // collide and the host filesystem is not littered. - _tempRoot = Path.Combine( - Path.GetTempPath(), - "yavsc-sig-tests-" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(_tempRoot); - this.siteSettings = new SiteSettings { Blog = _tempRoot }; - } - - public void Dispose() - { - try { Directory.Delete(_tempRoot, recursive: true); } - catch { /* best effort — the OS will clean Temp eventually */ } - } - - [Fact] - public void FileNameFormat_lowercases_type_and_includes_estimateId_and_ticks() - { - var name = EstimateSignatureFileHelper.FileNameFormat( - SignatureType.Pro, 42, 638_000_000_000_000_000L); - Assert.Equal("sign-pro-42-638000000000000000.json", name); - - var cli = EstimateSignatureFileHelper.FileNameFormat( - SignatureType.Client, 7, 1L); - Assert.Equal("sign-client-7-1.json", cli); - } - - [Theory] - [InlineData(new int[] { }, 0)] - [InlineData(new[] { 1, 100, 200 }, 1)] - [InlineData(new[] { 2, 1, 2, 3, 4 }, 1)] - [InlineData(new[] { 1, 1, 1, 2, 2, 3, 3 }, 2)] - [InlineData(new[] { 0, 1, 2, 3 }, 0)] // malformed k=0: short-circuit - public void ReceiveEstimateSignatureAsync_writes_a_v1_envelope(int[] strokes, int expectedStrokeCount) - { - // We don't read the count back from the helper (it's a - // private method), but the JSON envelope must reflect - // it; this verifies the public behaviour end-to-end. - _ = expectedStrokeCount; - // Arrange - var user = MakeUser("alice"); - var payload = new SignaturePadPayload - { - CoordinateMax = 10_000, - CapturedAtUtc = new DateTime(2026, 7, 4, 12, 0, 0, DateTimeKind.Utc), - Strokes = strokes, - }; - - // Act - var fi = Run(user, 123L, SignatureType.Pro, payload, this.siteSettings); - - // Assert: file exists, sits under the user's root, and - // parses as a yavsc.signature/v1 envelope. - var fullPath = Path.Combine(fi.DestDir, fi.FileName); - Assert.True(File.Exists(fullPath), $"missing: {fullPath}"); - - using var doc = JsonDocument.Parse(File.ReadAllText(fullPath)); - var root = doc.RootElement; - Assert.Equal("yavsc.signature/v1", root.GetProperty("format").GetString()); - Assert.Equal(10_000, root.GetProperty("coordinateMax").GetInt32()); - Assert.Equal(123L, root.GetProperty("estimateId").GetInt64()); - Assert.Equal("Pro", root.GetProperty("type").GetString()); - Assert.Equal("alice", root.GetProperty("signerName").GetString()); - Assert.Equal(expectedStrokeCount, root.GetProperty("strokeCount").GetInt32()); - } - - [Fact] - public async Task ReceiveEstimateSignatureAsync_rejects_null_payload() - { - var user = MakeUser("bob"); - // Capture TestContext.Current.CancellationToken outside the - // lambda so xUnit1051 sees a real CancellationToken argument - // (the lambda body runs on a different stack frame). - var ct = TestContext.Current.CancellationToken; - await Assert.ThrowsAsync(() => - EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( - user, 1L, SignatureType.Pro, payload: null!, - siteSettings, token: ct)); - } - - [Fact] - public async Task ReceiveEstimateSignatureAsync_rejects_non_positive_estimateId() - { - var user = MakeUser("bob"); - var payload = new SignaturePadPayload { Strokes = new[] { 1, 100, 100 } }; - var ct = TestContext.Current.CancellationToken; - await Assert.ThrowsAsync(() => - EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( - user, 0L, SignatureType.Pro, payload, this.siteSettings, token: ct)); - } - - // --- helpers ---------------------------------------------------- - - private static FileReceivedInfo Run( - ClaimsPrincipal user, long estimateId, SignatureType type, SignaturePadPayload payload, SiteSettings siteSettings) - { - // The helper is async; tests that don't care about the - // result can call it sync via .GetAwaiter().GetResult() - // because we know it never throws in the happy path. - return EstimateSignatureFileHelper - .ReceiveEstimateSignatureAsync(user, estimateId, type, payload, siteSettings, CancellationToken.None) - .GetAwaiter().GetResult(); - } - - private static ClaimsPrincipal MakeUser(string username) - { - return new ClaimsPrincipal(new ClaimsIdentity( - new[] { new Claim(ClaimTypes.Name, username) }, - authenticationType: "test")); - } -} diff --git a/src/Yavsc.Org.Tests/MarkdownTests.cs b/src/Yavsc.Org.Tests/MarkdownTests.cs deleted file mode 100644 index 1bd0db9c5..000000000 --- a/src/Yavsc.Org.Tests/MarkdownTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using DotMarkdown.Linq; -using Markdig; -using Xunit; -using static DotMarkdown.Linq.MFactory; - - -public class MarkdownTests -{ - [Fact] - public void TestMarkdownRendering() - { - MDocument document = Document( - Heading1("Markdown Sample"), - Heading2("Bullet List"), - BulletList( - "text", - Bold("bold text")), - HorizontalRule(), - Heading2("IndentedCodeBlock"), - IndentedCodeBlock("string s = null;")); - - Console.WriteLine(document.ToString()); - - } - - [Fact] - public void TestAnotherMarkdownRendering() - { - var result = Markdown.ToHtml("This is a text with some *emphasis*"); - - - Console.WriteLine(result); - } -} diff --git a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs deleted file mode 100644 index 61d774c57..000000000 --- a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using IdentityServer8.Stores; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Yavsc.Org.Tests.Smoke; - -/// -/// Smoke for the Account BC (login / registration / OIDC discovery). -/// Hits the in-memory test server through -/// : no sockets, no -/// self-signed certificates, no real DB (the -/// AddInMemoryCollection in WebServerFixture.SetupHost -/// points the EF context at the in-memory provider). -/// -/// GET /signin is the public login endpoint served by -/// Yavsc.Org/Controllers/Accounting/AccountController -/// (decorated [HttpGet(YavscConstants.SigninPath)] with -/// YavscConstants.SigninPath = "~/signin"). A 200 means the -/// entire pipeline (routing + Razor + IdentityServer + EF + DI) -/// is wired correctly end-to-end. -/// -public class AccountSmokeTests : IClassFixture -{ - private readonly TestWebApplicationFactory _factory; - - public AccountSmokeTests(TestWebApplicationFactory factory) - { - _factory = factory; - } - - - - [Fact] - public async Task ResourceStore_get_all_resources_does_not_throw() - { - using var scope = _factory.Services.CreateScope(); - var resourceStore = scope.ServiceProvider.GetRequiredService(); - - var exception = await Record.ExceptionAsync(resourceStore.GetAllResourcesAsync); - - Assert.Null(exception); - } -} diff --git a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs index 04f60aa1d..f8fa7f0d2 100644 --- a/src/Yavsc.Org/Controllers/Accounting/AccountController.cs +++ b/src/Yavsc.Org/Controllers/Accounting/AccountController.cs @@ -1061,6 +1061,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory, } return View(); } + [HttpPost, Authorize] public async Task Delete(UnregisterViewModel model) { diff --git a/src/Yavsc.Org/Controllers/Communicating/BlogspotController.cs b/src/Yavsc.Org/Controllers/Communicating/BlogspotController.cs index b55171879..31142cd60 100644 --- a/src/Yavsc.Org/Controllers/Communicating/BlogspotController.cs +++ b/src/Yavsc.Org/Controllers/Communicating/BlogspotController.cs @@ -90,73 +90,6 @@ namespace Yavsc.Org.Controllers ); } - // GET: Blog/Create - [Authorize()] - public IActionResult Create(string title) - { - var result = new BlogPostEditViewModel - (new Models.Blog.BlogPost - { - Title = title - }, true); - SetLangItems(); - return View(result); - } - - // POST: Blog/Create - [HttpPost, Authorize, ValidateAntiForgeryToken] - public IActionResult Create(Models.Blog.BlogPost blogInput) - { - if (ModelState.IsValid) - { - var uid = User.GetUserId(); - Models.Blog.BlogPost post = blogSpotService.Create(uid, blogInput); - blogSpotService.AttachFiles(Request.Form.Files, uid, post.Id); - return RedirectToAction("Index"); - } - return View("Edit", blogInput); - } - - [Authorize()] - // GET: Blog/Edit/5 - public async Task Edit(long? id) - { - if (id == null) - { - return NotFound(); - } - try - { - var blog = await blogSpotService.GetPostForEdition(User, id.Value); - if (blog == null) - { - return NotFound(); - } - - - SetLangItems(); - return View(blog); - - } - catch (AuthorizationFailureException) - { - return new ChallengeResult(); - } - } - - // POST: Blog/Edit/5 - [HttpPost] - [ValidateAntiForgeryToken, Authorize()] - public async Task Edit(BlogPostEditViewModel blogEdit) - { - if (ModelState.IsValid) - { - await blogSpotService.Modify(User, blogEdit); - ViewBag.StatusMessage = "Post modified"; - return RedirectToAction("Index"); - } - return View(blogEdit); - } // GET: Blog/Delete/5 [ActionName("Delete"), Authorize()] diff --git a/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs b/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs index 02dda32a1..569b5a6d7 100644 --- a/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs +++ b/src/Yavsc.Org/Controllers/Communicating/CommentsController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using Yavsc.Models; @@ -19,7 +20,7 @@ namespace Yavsc.Controllers public CommentsController(ApplicationDbContext context) { - _context = context; + _context = context; } [HttpGet("{id:long}", Name = "GetComment")] @@ -82,8 +83,17 @@ namespace Yavsc.Controllers return CreatedAtRoute("GetComment", new { id = comment.Id }, new { id = comment.Id, dateCreated = comment.DateCreated }); } + // --- Actions MVC scaffoldées (vues CRUD) --------------------- + // Ces actions ne font pas partie de la surface d'API documentée + // par Swagger. Elles sont exclues de l'ApiExplorer : sinon, comme + // le contrôleur porte [Route("~/api/v1/blogcomments")], leurs + // [HttpPost] sans template propre atterrissent sur la même route + // "POST api/v1/blogcomments" que l'action Post ci-dessus, et + // Swashbuckle lève une SwaggerGeneratorException (conflicting + // method/path) au démarrage de /swagger. // GET: Comments [HttpGet] + [ApiExplorerSettings(IgnoreApi = true)] public async Task Index() { var applicationDbContext = _context.Comment.Include(c => c.Post); @@ -91,6 +101,7 @@ namespace Yavsc.Controllers } // GET: Comments/Details/5 + [ApiExplorerSettings(IgnoreApi = true)] public async Task Details(long? id) { if (id == null) @@ -109,6 +120,7 @@ namespace Yavsc.Controllers // GET: Comments/Create (MVC form endpoint) [HttpGet("form")] + [ApiExplorerSettings(IgnoreApi = true)] public IActionResult Create() { ViewBag.ReceiverId = new SelectList(_context.BlogSpot, "Id", "Title"); @@ -118,6 +130,7 @@ namespace Yavsc.Controllers // POST: Comments/Create (MVC form endpoint) [HttpPost("form")] [ValidateAntiForgeryToken] + [ApiExplorerSettings(IgnoreApi = true)] public async Task Create(Comment comment) { comment.UserCreated = User.GetUserId(); @@ -126,7 +139,7 @@ namespace Yavsc.Controllers // remove the stale binding error so a valid authenticated POST // does not fall into the invalid branch. ModelState.Remove(nameof(Comment.AuthorId)); - + if (ModelState.IsValid) { _context.Comment.Add(comment); @@ -138,6 +151,7 @@ namespace Yavsc.Controllers } // GET: Comments/Edit/5 + [ApiExplorerSettings(IgnoreApi = true)] public async Task Edit(long? id) { if (id == null) @@ -157,6 +171,7 @@ namespace Yavsc.Controllers // POST: Comments/Edit/5 [HttpPost] [ValidateAntiForgeryToken] + [ApiExplorerSettings(IgnoreApi = true)] public async Task Edit(Comment comment) { if (ModelState.IsValid) @@ -171,6 +186,7 @@ namespace Yavsc.Controllers // GET: Comments/Delete/5 [ActionName("Delete")] + [ApiExplorerSettings(IgnoreApi = true)] public async Task Delete(long? id) { if (id == null) @@ -190,6 +206,7 @@ namespace Yavsc.Controllers // POST: Comments/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] + [ApiExplorerSettings(IgnoreApi = true)] public async Task DeleteConfirmed(long id) { Comment comment = await _context.Comment.SingleAsync(m => m.Id == id); diff --git a/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs b/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs index 549d80942..ac97193d0 100644 --- a/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs +++ b/src/Yavsc.Org/Controllers/Contracting/FrontOfficeController.cs @@ -8,7 +8,6 @@ namespace Yavsc.Controllers using Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; - using Microsoft.Extensions.Options; using Models; using ViewModels.FrontOffice; using Yavsc.Server.Helpers; @@ -21,22 +20,20 @@ namespace Yavsc.Controllers readonly ILogger _logger; readonly IStringLocalizer _SR; private readonly IBillingService _billing; - private readonly SiteSettings _siteSettings; public FrontOfficeController(ApplicationDbContext context, UserManager userManager, IBillingService billing, ILoggerFactory loggerFactory, - IStringLocalizer SR, - IOptions siteSettings) + IStringLocalizer SR) { _context = context; _userManager = userManager; _logger = loggerFactory.CreateLogger(); _SR = SR; _billing = billing; - _siteSettings = siteSettings.Value; } + public ActionResult Index() { var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); @@ -46,9 +43,9 @@ namespace Yavsc.Controllers { EstimateToProduceCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && c.Status == QueryStatus.Inserted && c.ValidationDate == null && !_context.Estimates.Any(e => e.CommandId == c.Id)).Count(), - EstimateToHonorAsProCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && c.Status == QueryStatus.Accepted + EstimateToHonorAsProCount = _context.RdvQueries.Where(c => c.PerformerId == uid && c.EventDate > now && (c.Status == QueryStatus.ProAccepted || c.Status == QueryStatus.Accepted) && c.ValidationDate == null && _context.Estimates.Any(e => e.CommandId == c.Id )).Count(), - EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && e.Query.Status == QueryStatus.Accepted).Count(), + EstimateToSignAsCliCount = _context.Estimates.Where(e => e.ClientId == uid && (e.Query.Status == QueryStatus.ProAccepted || e.Query.Status == QueryStatus.Accepted)).Count(), BillToSignAsCliCount = 0, NewPayementsCount = 0 @@ -80,61 +77,5 @@ namespace Yavsc.Controllers return View(result); } - - [Produces("text/x-tex"), Authorize, Route("estimate-{id}.tex")] - [HttpGet] - public ViewResult EstimateTex(long id) - { - var estimate = _context.Estimates.Include(x => x.Query) - .Include(x => x.Query.Client) - .Include(x => x.Query.PerformerProfile) - .Include(x => x.Query.PerformerProfile.OrganizationAddress) - .Include(x => x.Query.PerformerProfile.Performer) - .Include(e => e.Bill).FirstOrDefault(x => x.Id == id); - Response.ContentType = "text/x-tex"; - return View("Estimate.tex", estimate); - } - - [Authorize, Route("Estimate-{id}.pdf")] - [HttpGet] - public IActionResult EstimatePdf(long id) - { - ViewBag.TempDir = _siteSettings.TempDir; - ViewBag.BillsDir = _siteSettings.Bills; - var estimate = _context.Estimates.Include(x => x.Query) - .Include(x => x.Query.Client) - .Include(x => x.Query.PerformerProfile) - .Include(x => x.Query.PerformerProfile.OrganizationAddress) - .Include(x => x.Query.PerformerProfile.Performer) - .Include(e => e.Bill).FirstOrDefault(x => x.Id == id); - if (estimate == null) - throw new Exception("No data"); - return View("Estimate.pdf", estimate); - } - - [Authorize] - public IActionResult EstimateProValidation() - { - throw new NotImplementedException(); - - } - - [Authorize] - public IActionResult EstimateClientValidation() - { - throw new NotImplementedException(); - - } - [Authorize] - public IActionResult BillValidation() - { - throw new NotImplementedException(); - - } - [Authorize] - public IActionResult BillAcquitment() - { - throw new NotImplementedException(); - } } } diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 8c1db2a73..b5bb65cc5 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -215,6 +215,18 @@ public static class HostingExtensions services.AddScoped, UserClaimsPrincipalFactory>(); + // The default application cookie LoginPath is "/Account/Login", which + // has no controller here — anonymous access to a restricted page must + // redirect to the actual sign-in endpoint instead. The paths mirror + // the route attributes on AccountController (Constants.SigninPath etc., + // which carry a "~/" prefix that the cookie middleware does not want). + services.ConfigureApplicationCookie(options => + { + options.LoginPath = "/signin"; + options.LogoutPath = "/signout"; + options.AccessDeniedPath = "/Account/AccessDenied"; + }); + // Dev-only: Chromium rejects SameSite=None without Secure on http:// // (e.g. http://localhost:5000). The default Identity cookie policy // sets SameSite=None, which is invalid without Secure. Force Lax in @@ -356,6 +368,17 @@ public static class HostingExtensions }) .AddAspNetIdentity() + // Register the custom ProfileService so IdentityServer actually + // calls it instead of the default UserProfileService. Without + // this, the class is dead code: IS8 falls back to the default, + // which only emits claims listed in each API resource's + // UserClaims (RequestedClaimTypes) — "name" is never among + // them, so every access token lacks "name" and the Blogs/Api + // bearer hosts get GetUserName() == null (the MyFiles upload + // 500). Our ProfileService emits name/email/roles for any + // user grant whose client is allowed "profile", independent + // of the caller, so the access token carries the login. + .AddProfileService() .AddClientStore() .AddClientConfigurationValidator() .AddCorsPolicyService() diff --git a/src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs b/src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs index 96391990a..778cf5cfe 100644 --- a/src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs +++ b/src/Yavsc.Org/Helpers/AsciiDocTagHelper.cs @@ -17,8 +17,8 @@ namespace Yavsc.Helpers if (string.IsNullOrWhiteSpace(text)) return; - try + { if (context.AllAttributes.ContainsName("summary")) { diff --git a/src/Yavsc.Org/Helpers/TeXHelpers.cs b/src/Yavsc.Org/Helpers/TeXHelpers.cs deleted file mode 100644 index f7cc812fb..000000000 --- a/src/Yavsc.Org/Helpers/TeXHelpers.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewEngines; - -namespace Yavsc.Helpers -{ - using Microsoft.AspNetCore.Mvc.Infrastructure; - using ViewModels.Gen; - public class TeXString : HtmlString - { - - public TeXString(TeXString teXString): base(teXString.ToString()) - { - } - - public TeXString(string str) : base(str) - { - - - } - - public static TeXString operator+ (TeXString a, TeXString b) { - return new TeXString(a.ToString()+b.ToString()); - } - - } - public class Replacement - { - string target; - string replacement; - public Replacement(string target, string replacement) - { - this.target = target; - this.replacement = replacement; - } - public string Execute(string source) - { - return source?.Replace(target, replacement) ?? null; - } - } - public static class TeXHelpers - { - public static readonly Replacement[] SpecialCharsDefaultRendering = - { - new Replacement("<","\\textless"), - new Replacement(">","\\textgreater"), - new Replacement("©","\\copyright"), - new Replacement("®","\\textregistered"), - new Replacement("\\","\\textbackslash"), - new Replacement("™","\\texttrademark"), - new Replacement("¶","\\P"), - new Replacement("|","\\textbar"), - new Replacement("%","\\%"), - new Replacement("{","\\{"), - new Replacement("}","\\}"), - new Replacement("_","\\_"), - new Replacement("#","\\#"), - new Replacement("$","\\$"), - new Replacement("_","\\_"), - new Replacement("¿","\\textquestiondown"), - new Replacement("§","\\S"), - new Replacement("£","\\pounds"), - new Replacement("&","\\&"), - new Replacement("¡","\\textexclamdown"), - new Replacement("†","\\dag"), - new Replacement("–","\\textendash"), - new Replacement("°","\\textdegree") - }; - - public static TeXString ToTeX(this string source, string defaultValue="\\textit{néant}") - { - if (source==null) return new TeXString(defaultValue); - string result=source; - foreach (var r in SpecialCharsDefaultRendering) - { - result = r.Execute(result); - } - return new TeXString(result); - } - - public static TeXString ToTeXCell(this string source, string defaultValue="\\textit{néant}") - { - if (source==null) return new TeXString(defaultValue); - string result=source; - foreach (var r in SpecialCharsDefaultRendering) - { - result = r.Execute(result); - } - result = result.Replace("\n","\\tabularnewline "); - return new TeXString(result); - } - - - public static string NewLinesWith(this string target, string separator) - { - var items = target.Split(new char[] { '\n' }).Where( - s => !string.IsNullOrWhiteSpace(s)); - - return string.Join(separator, items); - } - - public static TeXString ToTeXLines(this string source, string defaultValue, string lineSeparator = "\n\\\\") - { - if (source == null) return new TeXString(defaultValue); - return new TeXString( source.ToTeX().ToString().NewLinesWith(lineSeparator) ); - } - - public static TeXString SplitAddressToTeX (this string source, string lineSeparator = "\n\\\\", string defaultValue = "\\textit{pas d'adresse postale}") - { - if (string.IsNullOrWhiteSpace(source)) return new TeXString(defaultValue); - var alines = source.Split(','); - var texlines = alines.Select(l=>l.ToTeX().ToString()); - return new TeXString(string.Join(lineSeparator,texlines)); - } - - public static bool GenerateEstimatePdf(this SiteSettings settings, PdfGenerationViewModel Model) - { - string errorMsg = null; - var billdir = Model.DestDir; - var tempdir = settings.TempDir; - string name = Model.BaseFileName; - string fullname = new FileInfo( - System.IO.Path.Combine(tempdir, name)).FullName; - string ofullname = new FileInfo( - System.IO.Path.Combine(billdir, name)).FullName; - - FileInfo fi = new FileInfo(fullname + ".tex"); - FileInfo fo = new FileInfo(ofullname + ".pdf"); - using (StreamWriter sw = new StreamWriter(fi.FullName)) - { - sw.Write(Model.TeXSource); - } - if (!fi.Exists) - { - errorMsg = "Source write failed"; - } - else - { - using (Process p = new Process()) - { - p.StartInfo.WorkingDirectory = tempdir; - p.StartInfo = new ProcessStartInfo - { - UseShellExecute = false, - WorkingDirectory = tempdir, - FileName = "/usr/bin/texi2pdf", - Arguments = $"--batch --build-dir=. -o {fo.FullName} {fi.FullName}" - }; - p.Start(); - p.WaitForExit(); - if (p.ExitCode != 0) - { - errorMsg = $"Pdf generation failed with exit code: {p.ExitCode}"; - } - else - { - fi.Delete(); - var di = new DirectoryInfo(Path.Combine(tempdir,$"{Model.BaseFileName}.t2d")); - di.Delete(true); - - } - } - } - Model.Generated = fo.Exists; - Model.GenerationErrorMessage = new HtmlString(errorMsg); - return fo.Exists; - } - - public static string RenderViewToString( - this Controller controller, IViewEngine engine, - IActionContextAccessor contextAccessor, - string viewName, object model, bool isMainPage = true) - { - using (var sw = new StringWriter()) - { - if (engine == null) - throw new InvalidOperationException("no engine"); - - // try to find the specified view - controller.TryValidateModel(model); - ViewEngineResult viewResult = engine.FindView(contextAccessor.ActionContext, viewName, isMainPage); - - // create the associated context - ViewContext viewContext = new ViewContext(); - viewContext.ActionDescriptor = contextAccessor.ActionContext.ActionDescriptor; - viewContext.HttpContext = contextAccessor.ActionContext.HttpContext; - viewContext.TempData = controller.TempData; - viewContext.View = viewResult.View; - viewContext.Writer = sw; - // write the render view with the given context to the stringwriter - viewResult.View.RenderAsync(viewContext).Wait(); - return sw.GetStringBuilder().ToString(); - } - } - } -} diff --git a/src/Yavsc.Org/Migrations/20260919195608_blogPostId.Designer.cs b/src/Yavsc.Org/Migrations/20260919195608_blogPostId.Designer.cs new file mode 100644 index 000000000..903838f1d --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260919195608_blogPostId.Designer.cs @@ -0,0 +1,4714 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Yavsc.Models; + +#nullable disable + +namespace Yavsc.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260919195608_blogPostId")] + partial class blogPostId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenType") + .HasColumnType("integer"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("boolean"); + + b.Property("AllowOfflineAccess") + .HasColumnType("boolean"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("boolean"); + + b.Property("AllowRememberConsent") + .HasColumnType("boolean"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("boolean"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("boolean"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("integer"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("BackChannelLogoutUri") + .HasColumnType("text"); + + b.Property("ClientClaimsPrefix") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ClientName") + .HasColumnType("text"); + + b.Property("ClientUri") + .HasColumnType("text"); + + b.Property("ConsentLifetime") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("integer"); + + b.Property("EnableLocalLogin") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutUri") + .HasColumnType("text"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("integer"); + + b.Property("IncludeJwtId") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUri") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("PairWiseSubjectSalt") + .HasColumnType("text"); + + b.Property("ProtocolType") + .HasColumnType("text"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("integer"); + + b.Property("RefreshTokenUsage") + .HasColumnType("integer"); + + b.Property("RequireClientSecret") + .HasColumnType("boolean"); + + b.Property("RequireConsent") + .HasColumnType("boolean"); + + b.Property("RequirePkce") + .HasColumnType("boolean"); + + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.Property("UserCodeType") + .HasColumnType("text"); + + b.Property("UserSsoLifetime") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Origin") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("GrantType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("PostLogoutRedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("RedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("text"); + + b.Property("DeviceCode") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.HasKey("UserCode", "DeviceCode"); + + b.ToTable("DeviceFlowCodes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("PersistedGrants"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Yavsc.Abstract.Identity.ClientProviderInfo", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("BillingAddressId") + .HasColumnType("bigint"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("ClientProviderInfo"); + }); + + modelBuilder.Entity("Yavsc.Abstract.Models.Messaging.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Target") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("body") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("click_action") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("color") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("icon") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("exclam"); + + b.Property("sound") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("tag") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TargetId"); + + b.ToTable("Ban"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("BlackListed"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("BlogPostId") + .HasColumnType("bigint"); + + b.HasKey("CircleId", "BlogPostId"); + + b.HasIndex("BlogPostId"); + + b.ToTable("CircleAuthorizationToBlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Access") + .HasColumnType("smallint"); + + b.HasKey("CircleId", "Path", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("CircleAuthorizationToFile"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ContactCredits") + .HasColumnType("bigint"); + + b.Property("Credits") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.ToTable("BankStatus"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AllowMonthlyEmail") + .HasColumnType("boolean"); + + b.Property("Avatar") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("/images/Users/icon_user.png"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DedicatedGoogleCalendar") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiskQuota") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(524288000L); + + b.Property("DiskUsage") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxFileSize") + .HasColumnType("bigint"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddressId") + .HasColumnType("bigint"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PostalAddressId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Impact") + .HasColumnType("numeric"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BalanceId"); + + b.ToTable("BalanceImpact"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("BIC") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankedKey") + .HasColumnType("integer"); + + b.Property("IBAN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WicketCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("BankIdentity"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("Currency") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("EstimateTemplateId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UnitaryCost") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("EstimateTemplateId"); + + b.ToTable("CommandLine"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachedFilesString") + .IsRequired() + .HasColumnType("text"); + + b.Property("AttachedGraphicsString") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProviderValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("CommandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Estimates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EstimateTemplates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.ExceptionSIREN", b => + { + b.Property("SIREN") + .HasColumnType("text"); + + b.HasKey("SIREN"); + + b.ToTable("ExceptionsSIREN"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("NominativeServiceCommands"); + + b.HasDiscriminator("Discriminator").HasValue("NominativeServiceCommand"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinateMax") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10000); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignerId") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Strokes") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("EstimateId", "Type") + .IsUnique(); + + b.ToTable("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "PostId"); + + b.HasIndex("PostId"); + + b.ToTable("BlogAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasMaxLength(56224) + .HasColumnType("character varying(56224)"); + + b.Property("AuthorId") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Photo") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("BlogSpot"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("BlogTag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ReceiverId"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UploadedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("PostId"); + + b.ToTable("blogSpotPublications"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("OwnerId"); + + b.ToTable("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Reccurence") + .HasColumnType("integer"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleOwnerId"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("ScheduledEvent"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.Property("ConnectionId") + .HasColumnType("text"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("ConnectionId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("ChatConnection"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LatestJoinPart") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("ChatRoom"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.Property("ChannelName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("ChannelName", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ChatRoomAccess"); + }); + + modelBuilder.Entity("Yavsc.Models.Cratie.Option", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("CodeScrutin") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code", "CodeScrutin"); + + b.ToTable("Option"); + }); + + modelBuilder.Entity("Yavsc.Models.Drawing.Color", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Blue") + .HasColumnType("smallint"); + + b.Property("Green") + .HasColumnType("smallint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Red") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ActionDistance") + .HasColumnType("integer"); + + b.Property("CarePrice") + .HasColumnType("numeric"); + + b.Property("FlatFeeDiscount") + .HasColumnType("numeric"); + + b.Property("HalfBalayagePrice") + .HasColumnType("numeric"); + + b.Property("HalfBrushingPrice") + .HasColumnType("numeric"); + + b.Property("HalfColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfDefrisPrice") + .HasColumnType("numeric"); + + b.Property("HalfFoldingPrice") + .HasColumnType("numeric"); + + b.Property("HalfMechPrice") + .HasColumnType("numeric"); + + b.Property("HalfMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfPermanentPrice") + .HasColumnType("numeric"); + + b.Property("KidCutPrice") + .HasColumnType("numeric"); + + b.Property("LongBalayagePrice") + .HasColumnType("numeric"); + + b.Property("LongBrushingPrice") + .HasColumnType("numeric"); + + b.Property("LongColorPrice") + .HasColumnType("numeric"); + + b.Property("LongDefrisPrice") + .HasColumnType("numeric"); + + b.Property("LongFoldingPrice") + .HasColumnType("numeric"); + + b.Property("LongMechPrice") + .HasColumnType("numeric"); + + b.Property("LongMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("LongPermanentPrice") + .HasColumnType("numeric"); + + b.Property("ManBrushPrice") + .HasColumnType("numeric"); + + b.Property("ManCutPrice") + .HasColumnType("numeric"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.Property("ShampooPrice") + .HasColumnType("numeric"); + + b.Property("ShortBalayagePrice") + .HasColumnType("numeric"); + + b.Property("ShortBrushingPrice") + .HasColumnType("numeric"); + + b.Property("ShortColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortDefrisPrice") + .HasColumnType("numeric"); + + b.Property("ShortFoldingPrice") + .HasColumnType("numeric"); + + b.Property("ShortMechPrice") + .HasColumnType("numeric"); + + b.Property("ShortMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortPermanentPrice") + .HasColumnType("numeric"); + + b.Property("WomenHalfCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenLongCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenShortCutPrice") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.HasIndex("ScheduleOwnerId"); + + b.ToTable("BrusherProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Cares") + .HasColumnType("boolean"); + + b.Property("Cut") + .HasColumnType("boolean"); + + b.Property("Dressing") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Length") + .HasColumnType("integer"); + + b.Property("Shampoo") + .HasColumnType("boolean"); + + b.Property("Tech") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HairPrestation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("QueryId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PrestationId"); + + b.HasIndex("QueryId"); + + b.ToTable("HairPrestationCollectionItem"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Brand") + .HasColumnType("text"); + + b.Property("ColorId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ColorId"); + + b.ToTable("HairTaint"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.Property("TaintId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.HasKey("TaintId", "PrestationId"); + + b.HasIndex("PrestationId"); + + b.ToTable("HairTaintInstance"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Evolution.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Feature"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(10240) + .HasColumnType("character varying(10240)"); + + b.Property("FeatureId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("Bug"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeclarationDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("LOCALTIMESTAMP"); + + b.Property("DeviceOwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LatestActivityUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DeviceId"); + + b.HasIndex("DeviceOwnerId"); + + b.ToTable("DeviceDeclaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("MatchExcerpt") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PatternId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("PatternId"); + + b.ToTable("DeclarationFlag"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("ModeratorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ModerationLogs", t => + { + t.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"); + }); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.RegexAlertPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("RegexAlertPatterns"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DeclarantTokenId") + .HasColumnType("uuid"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Sentiment") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrustTokenId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("SubmittedAt"); + + b.HasIndex("TrustTokenId"); + + b.ToTable("TrustDeclarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TrustScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("TrustTokens"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Depth") + .HasColumnType("numeric"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("numeric"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.Property("Weight") + .HasColumnType("numeric"); + + b.Property("Width") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContextId") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("For") + .HasColumnType("smallint"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Sender") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Announce"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("DismissClicked"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Instrument"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("InstrumentId", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("InstrumentRating"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.Property("OwnerProfileId") + .HasColumnType("text"); + + b.Property("DjSettingsUserId") + .HasColumnType("text"); + + b.Property("MusicLoverSettingsUserId") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("TendencyId") + .HasColumnType("bigint"); + + b.HasKey("OwnerProfileId"); + + b.HasIndex("DjSettingsUserId"); + + b.HasIndex("MusicLoverSettingsUserId"); + + b.HasIndex("TendencyId"); + + b.ToTable("MusicalPreference"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalTendency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SoundCloudId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("DjSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("InstrumentId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Instrumentation"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("MusicLoverSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Property("CreationToken") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderReference") + .HasColumnType("text"); + + b.Property("PaypalPayerId") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("CreationToken"); + + b.HasIndex("ExecutorId"); + + b.ToTable("PayPalPayment"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Circle"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.Property("MemberId") + .HasColumnType("text"); + + b.Property("CircleId") + .HasColumnType("bigint"); + + b.HasKey("MemberId", "CircleId"); + + b.HasIndex("CircleId"); + + b.ToTable("CircleMembers"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("OwnerId", "UserId"); + + b.HasIndex("AddressId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.Property("HRef") + .HasColumnType("text"); + + b.Property("Method") + .HasColumnType("text"); + + b.Property("BrusherProfileUserId") + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("PayPalPaymentCreationToken") + .HasColumnType("text"); + + b.Property("Rel") + .HasColumnType("text"); + + b.HasKey("HRef", "Method"); + + b.HasIndex("BrusherProfileUserId"); + + b.HasIndex("PayPalPaymentCreationToken"); + + b.ToTable("HyperLink"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.PostalAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Street1") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SiteSkills"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DifferedFileName") + .HasColumnType("text"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Pitch") + .HasColumnType("text"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("LiveFlow"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Hidden") + .HasColumnType("boolean"); + + b.Property("Moderated") + .HasColumnType("boolean"); + + b.Property("ModeratorGroupName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCode") + .HasColumnType("text"); + + b.Property("Photo") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SettingsClassName") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ParentCode"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FormationSettingsUserId") + .HasColumnType("text"); + + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("WorkingForId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FormationSettingsUserId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("WorkingForId"); + + b.ToTable("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionName") + .HasColumnType("text"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.ToTable("CommandForm"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Country", b => + { + b.Property("Code") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Code"); + + b.ToTable("Countries"); + + b.HasData( + new + { + Code = "fr", + DisplayName = "France" + }, + new + { + Code = "en", + DisplayName = "England" + }, + new + { + Code = "pt", + DisplayName = "Portugal" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DomaineActiviteCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Nom") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("DomaineActiviteCode", "Langue", "Nom") + .IsUnique(); + + b.ToTable("DictionnaireMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryCode") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RegularExpression") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CountryCode"); + + b.ToTable("PerformerCodeInputValidations"); + + b.HasData( + new + { + Id = 1L, + CountryCode = "fr", + ErrorMessage = "Le code FR doit contenir entre 9 et 14 chiffres.", + RegularExpression = "^[0-9]{9,14}$" + }, + new + { + Id = 2L, + CountryCode = "en", + ErrorMessage = "Le code EN doit contenir entre 8 et 14 caracteres alphanumeriques.", + RegularExpression = "^[A-Za-z0-9]{8,14}$" + }, + new + { + Id = 3L, + CountryCode = "pt", + ErrorMessage = "Le code PT doit contenir exactement 9 chiffres.", + RegularExpression = "^[0-9]{9}$" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("AcceptNotifications") + .HasColumnType("boolean"); + + b.Property("AcceptPublicContact") + .HasColumnType("boolean"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ExerciseCountryCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxDailyCost") + .HasColumnType("integer"); + + b.Property("MinDailyCost") + .HasColumnType("integer"); + + b.Property("OrganizationAddressId") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SIREN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseGeoLocalizationToReduceDistanceWithClients") + .HasColumnType("boolean"); + + b.Property("WebSite") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("PerformerId"); + + b.HasIndex("OrganizationAddressId"); + + b.ToTable("Performers"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("FormationSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateSoumission") + .HasColumnType("timestamp with time zone"); + + b.Property("DateValidation") + .HasColumnType("timestamp with time zone"); + + b.Property("Definition") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DictionnaireMetierId") + .HasColumnType("bigint"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Mot") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProposeParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("StatutValidation") + .HasColumnType("integer"); + + b.Property("ValideParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("ProposeParId"); + + b.HasIndex("ValideParId"); + + b.HasIndex("DictionnaireMetierId", "Langue", "Mot") + .IsUnique(); + + b.ToTable("TermeMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.Property("DoesCode") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("DoesCode", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserActivities"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.Calendar.Period", b => + { + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Start", "End"); + + b.ToTable("Period"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.EMailing.MailingTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(65536) + .HasColumnType("character varying(65536)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyToAddress") + .HasColumnType("text"); + + b.Property("ToSend") + .HasColumnType("integer"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MailingTemplate"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectBuildConfiguration"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Branch") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("GitRepositoryReference"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("AdditionalInfo") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("SelectedProfileUserId") + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.HasIndex("PrestationId"); + + b.HasIndex("SelectedProfileUserId"); + + b.HasDiscriminator().HasValue("HairCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("HairMultiCutQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("HairMultiCutQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("HairMultiCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("LocationType") + .HasColumnType("integer"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("RdvQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("RdvQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("RdvQuery"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("GitId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasIndex("GitId"); + + b.HasDiscriminator().HasValue("Project"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "TargetUser") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("BlackList") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Target") + .WithMany("ACL") + .HasForeignKey("BlogPostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithOne("AccountBalance") + .HasForeignKey("Yavsc.Models.AccountBalance", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "PostalAddress") + .WithMany() + .HasForeignKey("PostalAddressId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.HasOne("Yavsc.Models.AccountBalance", "Balance") + .WithMany() + .HasForeignKey("BalanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Balance"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("BankInfo") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", null) + .WithMany("Bill") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.EstimateTemplate", null) + .WithMany("Bill") + .HasForeignKey("EstimateTemplateId"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Query") + .WithMany() + .HasForeignKey("CommandId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Owner"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany("Signatures") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("Signer"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("Posts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("BlogComments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.Comment", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Parent"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", null) + .WithMany("Events") + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Server.Models.Calendar.Period", "Period") + .WithMany() + .HasForeignKey("PeriodStart", "PeriodEnd"); + + b.Navigation("Period"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Connections") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Rooms") + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.HasOne("Yavsc.Models.Chat.ChatRoom", "Room") + .WithMany("Moderation") + .HasForeignKey("ChannelName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("RoomAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "BaseProfile") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseProfile"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairMultiCutQuery", "Query") + .WithMany("Prestations") + .HasForeignKey("QueryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.HasOne("Yavsc.Models.Drawing.Color", "Color") + .WithMany() + .HasForeignKey("ColorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany("Taints") + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairTaint", "Taint") + .WithMany() + .HasForeignKey("TaintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Taint"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.HasOne("Yavsc.Models.IT.Evolution.Feature", "False") + .WithMany() + .HasForeignKey("FeatureId"); + + b.Navigation("False"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "DeviceOwner") + .WithMany("DeviceDeclaration") + .HasForeignKey("DeviceOwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceOwner"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany("Flags") + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Kyc.RegexAlertPattern", "Pattern") + .WithMany() + .HasForeignKey("PatternId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + + b.Navigation("Pattern"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany() + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustToken", "Subject") + .WithMany("Declarations") + .HasForeignKey("TrustTokenId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Subject"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Services") + .HasForeignKey("ContextId"); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.HasOne("Yavsc.Abstract.Models.Messaging.Notification", "Notified") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notified"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Instrument") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Profile") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instrument"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.HasOne("Yavsc.Models.Musical.Profiles.DjSettings", null) + .WithMany("SoundColor") + .HasForeignKey("DjSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null) + .WithMany("SoundColor") + .HasForeignKey("MusicLoverSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency") + .WithMany() + .HasForeignKey("TendencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Tool") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tool"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Executor") + .WithMany() + .HasForeignKey("ExecutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Executor"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Circles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Circle") + .WithMany("Members") + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Member") + .WithMany("Membership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Circle"); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.HasOne("Yavsc.Models.Relationship.PostalAddress", "PostalAddress") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Book") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", null) + .WithMany("Links") + .HasForeignKey("BrusherProfileUserId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", null) + .WithMany("Links") + .HasForeignKey("PayPalPaymentCreationToken"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentCode"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.HasOne("Yavsc.Models.Workflow.Profiles.FormationSettings", null) + .WithMany("CoWorking") + .HasForeignKey("FormationSettingsUserId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Performer") + .WithMany() + .HasForeignKey("PerformerId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "WorkingFor") + .WithMany() + .HasForeignKey("WorkingForId"); + + b.Navigation("Performer"); + + b.Navigation("WorkingFor"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Forms") + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "DomaineActivite") + .WithMany() + .HasForeignKey("DomaineActiviteCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DomaineActivite"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.HasOne("Yavsc.Models.Workflow.Country", "Country") + .WithMany() + .HasForeignKey("CountryCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "OrganizationAddress") + .WithMany() + .HasForeignKey("OrganizationAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Performer") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationAddress"); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.DictionnaireMetier", "DictionnaireMetier") + .WithMany("Termes") + .HasForeignKey("DictionnaireMetierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "ProposePar") + .WithMany() + .HasForeignKey("ProposeParId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "ValidePar") + .WithMany() + .HasForeignKey("ValideParId"); + + b.Navigation("DictionnaireMetier"); + + b.Navigation("ProposePar"); + + b.Navigation("ValidePar"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Does") + .WithMany() + .HasForeignKey("DoesCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany("Activity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Does"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.HasOne("Yavsc.Server.Models.IT.Project", "TargetProject") + .WithMany("Configurations") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetProject"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", "SelectedProfile") + .WithMany() + .HasForeignKey("SelectedProfileUserId"); + + b.Navigation("Location"); + + b.Navigation("Prestation"); + + b.Navigation("SelectedProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasOne("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", "Repository") + .WithMany() + .HasForeignKey("GitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Navigation("AccountBalance"); + + b.Navigation("BankInfo"); + + b.Navigation("BlackList"); + + b.Navigation("BlogComments"); + + b.Navigation("Book"); + + b.Navigation("Circles"); + + b.Navigation("Connections"); + + b.Navigation("DeviceDeclaration"); + + b.Navigation("Membership"); + + b.Navigation("Posts"); + + b.Navigation("RoomAccess"); + + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Navigation("Bill"); + + b.Navigation("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Navigation("Bill"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Navigation("ACL"); + + b.Navigation("Comments"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Navigation("Moderation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Navigation("Taints"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Navigation("Declarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Navigation("Children"); + + b.Navigation("Forms"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Navigation("Termes"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Navigation("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Navigation("Prestations"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Navigation("Configurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260919195608_blogPostId.cs b/src/Yavsc.Org/Migrations/20260919195608_blogPostId.cs new file mode 100644 index 000000000..624571209 --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260919195608_blogPostId.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class blogPostId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_blogSpotPublications_BlogSpot_BlogpostId", + table: "blogSpotPublications"); + + migrationBuilder.RenameColumn( + name: "BlogpostId", + table: "blogSpotPublications", + newName: "PostId"); + + migrationBuilder.AddForeignKey( + name: "FK_blogSpotPublications_BlogSpot_PostId", + table: "blogSpotPublications", + column: "PostId", + principalTable: "BlogSpot", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_blogSpotPublications_BlogSpot_PostId", + table: "blogSpotPublications"); + + migrationBuilder.RenameColumn( + name: "PostId", + table: "blogSpotPublications", + newName: "BlogpostId"); + + migrationBuilder.AddForeignKey( + name: "FK_blogSpotPublications_BlogSpot_BlogpostId", + table: "blogSpotPublications", + column: "BlogpostId", + principalTable: "BlogSpot", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.Designer.cs b/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.Designer.cs new file mode 100644 index 000000000..04b9fdc68 --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.Designer.cs @@ -0,0 +1,4713 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Yavsc.Models; + +#nullable disable + +namespace Yavsc.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260921145454_DropSignatureFilePath")] + partial class DropSignatureFilePath + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenType") + .HasColumnType("integer"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("boolean"); + + b.Property("AllowOfflineAccess") + .HasColumnType("boolean"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("boolean"); + + b.Property("AllowRememberConsent") + .HasColumnType("boolean"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("boolean"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("boolean"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("integer"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("BackChannelLogoutUri") + .HasColumnType("text"); + + b.Property("ClientClaimsPrefix") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ClientName") + .HasColumnType("text"); + + b.Property("ClientUri") + .HasColumnType("text"); + + b.Property("ConsentLifetime") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("integer"); + + b.Property("EnableLocalLogin") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutUri") + .HasColumnType("text"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("integer"); + + b.Property("IncludeJwtId") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUri") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("PairWiseSubjectSalt") + .HasColumnType("text"); + + b.Property("ProtocolType") + .HasColumnType("text"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("integer"); + + b.Property("RefreshTokenUsage") + .HasColumnType("integer"); + + b.Property("RequireClientSecret") + .HasColumnType("boolean"); + + b.Property("RequireConsent") + .HasColumnType("boolean"); + + b.Property("RequirePkce") + .HasColumnType("boolean"); + + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.Property("UserCodeType") + .HasColumnType("text"); + + b.Property("UserSsoLifetime") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Origin") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("GrantType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("PostLogoutRedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("RedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("text"); + + b.Property("DeviceCode") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.HasKey("UserCode", "DeviceCode"); + + b.ToTable("DeviceFlowCodes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("PersistedGrants"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Yavsc.Abstract.Identity.ClientProviderInfo", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("BillingAddressId") + .HasColumnType("bigint"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("ClientProviderInfo"); + }); + + modelBuilder.Entity("Yavsc.Abstract.Models.Messaging.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Target") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("body") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("click_action") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("color") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("icon") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("exclam"); + + b.Property("sound") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("tag") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TargetId"); + + b.ToTable("Ban"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("BlackListed"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("BlogPostId") + .HasColumnType("bigint"); + + b.HasKey("CircleId", "BlogPostId"); + + b.HasIndex("BlogPostId"); + + b.ToTable("CircleAuthorizationToBlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Access") + .HasColumnType("smallint"); + + b.HasKey("CircleId", "Path", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("CircleAuthorizationToFile"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ContactCredits") + .HasColumnType("bigint"); + + b.Property("Credits") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.ToTable("BankStatus"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AllowMonthlyEmail") + .HasColumnType("boolean"); + + b.Property("Avatar") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("/images/Users/icon_user.png"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DedicatedGoogleCalendar") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiskQuota") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(524288000L); + + b.Property("DiskUsage") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxFileSize") + .HasColumnType("bigint"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddressId") + .HasColumnType("bigint"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PostalAddressId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Impact") + .HasColumnType("numeric"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BalanceId"); + + b.ToTable("BalanceImpact"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("BIC") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankedKey") + .HasColumnType("integer"); + + b.Property("IBAN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WicketCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("BankIdentity"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("Currency") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("EstimateTemplateId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UnitaryCost") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("EstimateTemplateId"); + + b.ToTable("CommandLine"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachedFilesString") + .IsRequired() + .HasColumnType("text"); + + b.Property("AttachedGraphicsString") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProviderValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("CommandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Estimates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EstimateTemplates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.ExceptionSIREN", b => + { + b.Property("SIREN") + .HasColumnType("text"); + + b.HasKey("SIREN"); + + b.ToTable("ExceptionsSIREN"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("NominativeServiceCommands"); + + b.HasDiscriminator("Discriminator").HasValue("NominativeServiceCommand"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinateMax") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10000); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("SignerId") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Strokes") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("EstimateId", "Type") + .IsUnique(); + + b.ToTable("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "PostId"); + + b.HasIndex("PostId"); + + b.ToTable("BlogAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasMaxLength(56224) + .HasColumnType("character varying(56224)"); + + b.Property("AuthorId") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Photo") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("BlogSpot"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("BlogTag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ReceiverId"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UploadedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("PostId"); + + b.ToTable("blogSpotPublications"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("OwnerId"); + + b.ToTable("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Reccurence") + .HasColumnType("integer"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleOwnerId"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("ScheduledEvent"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.Property("ConnectionId") + .HasColumnType("text"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("ConnectionId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("ChatConnection"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LatestJoinPart") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("ChatRoom"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.Property("ChannelName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("ChannelName", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ChatRoomAccess"); + }); + + modelBuilder.Entity("Yavsc.Models.Cratie.Option", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("CodeScrutin") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code", "CodeScrutin"); + + b.ToTable("Option"); + }); + + modelBuilder.Entity("Yavsc.Models.Drawing.Color", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Blue") + .HasColumnType("smallint"); + + b.Property("Green") + .HasColumnType("smallint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Red") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ActionDistance") + .HasColumnType("integer"); + + b.Property("CarePrice") + .HasColumnType("numeric"); + + b.Property("FlatFeeDiscount") + .HasColumnType("numeric"); + + b.Property("HalfBalayagePrice") + .HasColumnType("numeric"); + + b.Property("HalfBrushingPrice") + .HasColumnType("numeric"); + + b.Property("HalfColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfDefrisPrice") + .HasColumnType("numeric"); + + b.Property("HalfFoldingPrice") + .HasColumnType("numeric"); + + b.Property("HalfMechPrice") + .HasColumnType("numeric"); + + b.Property("HalfMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfPermanentPrice") + .HasColumnType("numeric"); + + b.Property("KidCutPrice") + .HasColumnType("numeric"); + + b.Property("LongBalayagePrice") + .HasColumnType("numeric"); + + b.Property("LongBrushingPrice") + .HasColumnType("numeric"); + + b.Property("LongColorPrice") + .HasColumnType("numeric"); + + b.Property("LongDefrisPrice") + .HasColumnType("numeric"); + + b.Property("LongFoldingPrice") + .HasColumnType("numeric"); + + b.Property("LongMechPrice") + .HasColumnType("numeric"); + + b.Property("LongMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("LongPermanentPrice") + .HasColumnType("numeric"); + + b.Property("ManBrushPrice") + .HasColumnType("numeric"); + + b.Property("ManCutPrice") + .HasColumnType("numeric"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.Property("ShampooPrice") + .HasColumnType("numeric"); + + b.Property("ShortBalayagePrice") + .HasColumnType("numeric"); + + b.Property("ShortBrushingPrice") + .HasColumnType("numeric"); + + b.Property("ShortColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortDefrisPrice") + .HasColumnType("numeric"); + + b.Property("ShortFoldingPrice") + .HasColumnType("numeric"); + + b.Property("ShortMechPrice") + .HasColumnType("numeric"); + + b.Property("ShortMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortPermanentPrice") + .HasColumnType("numeric"); + + b.Property("WomenHalfCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenLongCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenShortCutPrice") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.HasIndex("ScheduleOwnerId"); + + b.ToTable("BrusherProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Cares") + .HasColumnType("boolean"); + + b.Property("Cut") + .HasColumnType("boolean"); + + b.Property("Dressing") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Length") + .HasColumnType("integer"); + + b.Property("Shampoo") + .HasColumnType("boolean"); + + b.Property("Tech") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HairPrestation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("QueryId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PrestationId"); + + b.HasIndex("QueryId"); + + b.ToTable("HairPrestationCollectionItem"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Brand") + .HasColumnType("text"); + + b.Property("ColorId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ColorId"); + + b.ToTable("HairTaint"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.Property("TaintId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.HasKey("TaintId", "PrestationId"); + + b.HasIndex("PrestationId"); + + b.ToTable("HairTaintInstance"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Evolution.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Feature"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(10240) + .HasColumnType("character varying(10240)"); + + b.Property("FeatureId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("Bug"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeclarationDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("LOCALTIMESTAMP"); + + b.Property("DeviceOwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LatestActivityUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DeviceId"); + + b.HasIndex("DeviceOwnerId"); + + b.ToTable("DeviceDeclaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("MatchExcerpt") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PatternId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("PatternId"); + + b.ToTable("DeclarationFlag"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("ModeratorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ModerationLogs", t => + { + t.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"); + }); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.RegexAlertPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("RegexAlertPatterns"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DeclarantTokenId") + .HasColumnType("uuid"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Sentiment") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrustTokenId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("SubmittedAt"); + + b.HasIndex("TrustTokenId"); + + b.ToTable("TrustDeclarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TrustScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("TrustTokens"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Depth") + .HasColumnType("numeric"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("numeric"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.Property("Weight") + .HasColumnType("numeric"); + + b.Property("Width") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContextId") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("For") + .HasColumnType("smallint"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Sender") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Announce"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("DismissClicked"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Instrument"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("InstrumentId", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("InstrumentRating"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.Property("OwnerProfileId") + .HasColumnType("text"); + + b.Property("DjSettingsUserId") + .HasColumnType("text"); + + b.Property("MusicLoverSettingsUserId") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("TendencyId") + .HasColumnType("bigint"); + + b.HasKey("OwnerProfileId"); + + b.HasIndex("DjSettingsUserId"); + + b.HasIndex("MusicLoverSettingsUserId"); + + b.HasIndex("TendencyId"); + + b.ToTable("MusicalPreference"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalTendency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SoundCloudId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("DjSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("InstrumentId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Instrumentation"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("MusicLoverSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Property("CreationToken") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderReference") + .HasColumnType("text"); + + b.Property("PaypalPayerId") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("CreationToken"); + + b.HasIndex("ExecutorId"); + + b.ToTable("PayPalPayment"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Circle"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.Property("MemberId") + .HasColumnType("text"); + + b.Property("CircleId") + .HasColumnType("bigint"); + + b.HasKey("MemberId", "CircleId"); + + b.HasIndex("CircleId"); + + b.ToTable("CircleMembers"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("OwnerId", "UserId"); + + b.HasIndex("AddressId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.Property("HRef") + .HasColumnType("text"); + + b.Property("Method") + .HasColumnType("text"); + + b.Property("BrusherProfileUserId") + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("PayPalPaymentCreationToken") + .HasColumnType("text"); + + b.Property("Rel") + .HasColumnType("text"); + + b.HasKey("HRef", "Method"); + + b.HasIndex("BrusherProfileUserId"); + + b.HasIndex("PayPalPaymentCreationToken"); + + b.ToTable("HyperLink"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.PostalAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Street1") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SiteSkills"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DifferedFileName") + .HasColumnType("text"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Pitch") + .HasColumnType("text"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("LiveFlow"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Hidden") + .HasColumnType("boolean"); + + b.Property("Moderated") + .HasColumnType("boolean"); + + b.Property("ModeratorGroupName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCode") + .HasColumnType("text"); + + b.Property("Photo") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SettingsClassName") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ParentCode"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FormationSettingsUserId") + .HasColumnType("text"); + + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("WorkingForId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FormationSettingsUserId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("WorkingForId"); + + b.ToTable("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionName") + .HasColumnType("text"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.ToTable("CommandForm"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Country", b => + { + b.Property("Code") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Code"); + + b.ToTable("Countries"); + + b.HasData( + new + { + Code = "fr", + DisplayName = "France" + }, + new + { + Code = "en", + DisplayName = "England" + }, + new + { + Code = "pt", + DisplayName = "Portugal" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DomaineActiviteCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Nom") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("DomaineActiviteCode", "Langue", "Nom") + .IsUnique(); + + b.ToTable("DictionnaireMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryCode") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RegularExpression") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CountryCode"); + + b.ToTable("PerformerCodeInputValidations"); + + b.HasData( + new + { + Id = 1L, + CountryCode = "fr", + ErrorMessage = "Le code FR doit contenir entre 9 et 14 chiffres.", + RegularExpression = "^[0-9]{9,14}$" + }, + new + { + Id = 2L, + CountryCode = "en", + ErrorMessage = "Le code EN doit contenir entre 8 et 14 caracteres alphanumeriques.", + RegularExpression = "^[A-Za-z0-9]{8,14}$" + }, + new + { + Id = 3L, + CountryCode = "pt", + ErrorMessage = "Le code PT doit contenir exactement 9 chiffres.", + RegularExpression = "^[0-9]{9}$" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("AcceptNotifications") + .HasColumnType("boolean"); + + b.Property("AcceptPublicContact") + .HasColumnType("boolean"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ExerciseCountryCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxDailyCost") + .HasColumnType("integer"); + + b.Property("MinDailyCost") + .HasColumnType("integer"); + + b.Property("OrganizationAddressId") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SIREN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseGeoLocalizationToReduceDistanceWithClients") + .HasColumnType("boolean"); + + b.Property("WebSite") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("PerformerId"); + + b.HasIndex("OrganizationAddressId"); + + b.ToTable("Performers"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("FormationSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateSoumission") + .HasColumnType("timestamp with time zone"); + + b.Property("DateValidation") + .HasColumnType("timestamp with time zone"); + + b.Property("Definition") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DictionnaireMetierId") + .HasColumnType("bigint"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Mot") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProposeParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("StatutValidation") + .HasColumnType("integer"); + + b.Property("ValideParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("ProposeParId"); + + b.HasIndex("ValideParId"); + + b.HasIndex("DictionnaireMetierId", "Langue", "Mot") + .IsUnique(); + + b.ToTable("TermeMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.Property("DoesCode") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("DoesCode", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserActivities"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.Calendar.Period", b => + { + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Start", "End"); + + b.ToTable("Period"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.EMailing.MailingTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(65536) + .HasColumnType("character varying(65536)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyToAddress") + .HasColumnType("text"); + + b.Property("ToSend") + .HasColumnType("integer"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MailingTemplate"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectBuildConfiguration"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Branch") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("GitRepositoryReference"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("AdditionalInfo") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("SelectedProfileUserId") + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.HasIndex("PrestationId"); + + b.HasIndex("SelectedProfileUserId"); + + b.HasDiscriminator().HasValue("HairCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("HairMultiCutQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("HairMultiCutQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("HairMultiCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("LocationType") + .HasColumnType("integer"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("RdvQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("RdvQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("RdvQuery"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("GitId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasIndex("GitId"); + + b.HasDiscriminator().HasValue("Project"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "TargetUser") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("BlackList") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Target") + .WithMany("ACL") + .HasForeignKey("BlogPostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithOne("AccountBalance") + .HasForeignKey("Yavsc.Models.AccountBalance", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "PostalAddress") + .WithMany() + .HasForeignKey("PostalAddressId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.HasOne("Yavsc.Models.AccountBalance", "Balance") + .WithMany() + .HasForeignKey("BalanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Balance"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("BankInfo") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", null) + .WithMany("Bill") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.EstimateTemplate", null) + .WithMany("Bill") + .HasForeignKey("EstimateTemplateId"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Query") + .WithMany() + .HasForeignKey("CommandId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Owner"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany("Signatures") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("Signer"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("Posts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("BlogComments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.Comment", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Parent"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithOne("Publication") + .HasForeignKey("Yavsc.Models.BlogSpotPublication", "PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", null) + .WithMany("Events") + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Server.Models.Calendar.Period", "Period") + .WithMany() + .HasForeignKey("PeriodStart", "PeriodEnd"); + + b.Navigation("Period"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Connections") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Rooms") + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.HasOne("Yavsc.Models.Chat.ChatRoom", "Room") + .WithMany("Moderation") + .HasForeignKey("ChannelName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("RoomAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "BaseProfile") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseProfile"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairMultiCutQuery", "Query") + .WithMany("Prestations") + .HasForeignKey("QueryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.HasOne("Yavsc.Models.Drawing.Color", "Color") + .WithMany() + .HasForeignKey("ColorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany("Taints") + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairTaint", "Taint") + .WithMany() + .HasForeignKey("TaintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Taint"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.HasOne("Yavsc.Models.IT.Evolution.Feature", "False") + .WithMany() + .HasForeignKey("FeatureId"); + + b.Navigation("False"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "DeviceOwner") + .WithMany("DeviceDeclaration") + .HasForeignKey("DeviceOwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceOwner"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany("Flags") + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Kyc.RegexAlertPattern", "Pattern") + .WithMany() + .HasForeignKey("PatternId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + + b.Navigation("Pattern"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany() + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustToken", "Subject") + .WithMany("Declarations") + .HasForeignKey("TrustTokenId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Subject"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Services") + .HasForeignKey("ContextId"); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.HasOne("Yavsc.Abstract.Models.Messaging.Notification", "Notified") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notified"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Instrument") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Profile") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instrument"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.HasOne("Yavsc.Models.Musical.Profiles.DjSettings", null) + .WithMany("SoundColor") + .HasForeignKey("DjSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null) + .WithMany("SoundColor") + .HasForeignKey("MusicLoverSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency") + .WithMany() + .HasForeignKey("TendencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Tool") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tool"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Executor") + .WithMany() + .HasForeignKey("ExecutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Executor"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Circles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Circle") + .WithMany("Members") + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Member") + .WithMany("Membership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Circle"); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.HasOne("Yavsc.Models.Relationship.PostalAddress", "PostalAddress") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Book") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", null) + .WithMany("Links") + .HasForeignKey("BrusherProfileUserId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", null) + .WithMany("Links") + .HasForeignKey("PayPalPaymentCreationToken"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentCode"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.HasOne("Yavsc.Models.Workflow.Profiles.FormationSettings", null) + .WithMany("CoWorking") + .HasForeignKey("FormationSettingsUserId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Performer") + .WithMany() + .HasForeignKey("PerformerId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "WorkingFor") + .WithMany() + .HasForeignKey("WorkingForId"); + + b.Navigation("Performer"); + + b.Navigation("WorkingFor"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Forms") + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "DomaineActivite") + .WithMany() + .HasForeignKey("DomaineActiviteCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DomaineActivite"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.HasOne("Yavsc.Models.Workflow.Country", "Country") + .WithMany() + .HasForeignKey("CountryCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "OrganizationAddress") + .WithMany() + .HasForeignKey("OrganizationAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Performer") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationAddress"); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.DictionnaireMetier", "DictionnaireMetier") + .WithMany("Termes") + .HasForeignKey("DictionnaireMetierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "ProposePar") + .WithMany() + .HasForeignKey("ProposeParId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "ValidePar") + .WithMany() + .HasForeignKey("ValideParId"); + + b.Navigation("DictionnaireMetier"); + + b.Navigation("ProposePar"); + + b.Navigation("ValidePar"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Does") + .WithMany() + .HasForeignKey("DoesCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany("Activity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Does"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.HasOne("Yavsc.Server.Models.IT.Project", "TargetProject") + .WithMany("Configurations") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetProject"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", "SelectedProfile") + .WithMany() + .HasForeignKey("SelectedProfileUserId"); + + b.Navigation("Location"); + + b.Navigation("Prestation"); + + b.Navigation("SelectedProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasOne("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", "Repository") + .WithMany() + .HasForeignKey("GitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Navigation("AccountBalance"); + + b.Navigation("BankInfo"); + + b.Navigation("BlackList"); + + b.Navigation("BlogComments"); + + b.Navigation("Book"); + + b.Navigation("Circles"); + + b.Navigation("Connections"); + + b.Navigation("DeviceDeclaration"); + + b.Navigation("Membership"); + + b.Navigation("Posts"); + + b.Navigation("RoomAccess"); + + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Navigation("Bill"); + + b.Navigation("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Navigation("Bill"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Navigation("ACL"); + + b.Navigation("Comments"); + + b.Navigation("Publication") + .IsRequired(); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Navigation("Moderation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Navigation("Taints"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Navigation("Declarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Navigation("Children"); + + b.Navigation("Forms"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Navigation("Termes"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Navigation("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Navigation("Prestations"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Navigation("Configurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.cs b/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.cs new file mode 100644 index 000000000..d29c153cb --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260921145454_DropSignatureFilePath.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class DropSignatureFilePath : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FilePath", + table: "Signatures"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FilePath", + table: "Signatures", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260924150433_UserFileAttachments.Designer.cs b/src/Yavsc.Org/Migrations/20260924150433_UserFileAttachments.Designer.cs new file mode 100644 index 000000000..a05e3b2c2 --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260924150433_UserFileAttachments.Designer.cs @@ -0,0 +1,4797 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Yavsc.Models; + +#nullable disable + +namespace Yavsc.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260924150433_UserFileAttachments")] + partial class UserFileAttachments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiResourceId") + .HasColumnType("integer"); + + b.Property("ApiResourceId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("ApiResourceId1"); + + b.ToTable("ApiResourceSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("ScopeId") + .HasColumnType("integer"); + + b.Property("ScopeId1") + .HasColumnType("integer"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScopeId"); + + b.HasIndex("ScopeId1"); + + b.ToTable("ApiScopeProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenLifetime") + .HasColumnType("integer"); + + b.Property("AccessTokenType") + .HasColumnType("integer"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("boolean"); + + b.Property("AllowOfflineAccess") + .HasColumnType("boolean"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("boolean"); + + b.Property("AllowRememberConsent") + .HasColumnType("boolean"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasColumnType("text"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("boolean"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("boolean"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("integer"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("BackChannelLogoutUri") + .HasColumnType("text"); + + b.Property("ClientClaimsPrefix") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ClientName") + .HasColumnType("text"); + + b.Property("ClientUri") + .HasColumnType("text"); + + b.Property("ConsentLifetime") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("integer"); + + b.Property("EnableLocalLogin") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("boolean"); + + b.Property("FrontChannelLogoutUri") + .HasColumnType("text"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("integer"); + + b.Property("IncludeJwtId") + .HasColumnType("boolean"); + + b.Property("LastAccessed") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoUri") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("PairWiseSubjectSalt") + .HasColumnType("text"); + + b.Property("ProtocolType") + .HasColumnType("text"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("integer"); + + b.Property("RefreshTokenUsage") + .HasColumnType("integer"); + + b.Property("RequireClientSecret") + .HasColumnType("boolean"); + + b.Property("RequireConsent") + .HasColumnType("boolean"); + + b.Property("RequirePkce") + .HasColumnType("boolean"); + + b.Property("RequireRequestObject") + .HasColumnType("boolean"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("integer"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.Property("UserCodeType") + .HasColumnType("text"); + + b.Property("UserSsoLifetime") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Origin") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("GrantType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Provider") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("PostLogoutRedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("RedirectUri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ClientId1") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("ClientId1"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("text"); + + b.Property("DeviceCode") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.HasKey("UserCode", "DeviceCode"); + + b.ToTable("DeviceFlowCodes"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("Emphasize") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NonEditable") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("boolean"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IdentityResourceId") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityResourceProperties"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("text"); + + b.Property("ClientId") + .HasColumnType("text"); + + b.Property("ConsumedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .HasColumnType("text"); + + b.Property("SubjectId") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("PersistedGrants"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Yavsc.Abstract.Identity.ClientProviderInfo", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("BillingAddressId") + .HasColumnType("bigint"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("ClientProviderInfo"); + }); + + modelBuilder.Entity("Yavsc.Abstract.Models.Messaging.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Target") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("body") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("click_action") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("color") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("icon") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("exclam"); + + b.Property("sound") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("tag") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.HasKey("Id"); + + b.ToTable("Notification"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("TargetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TargetId"); + + b.ToTable("Ban"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("UserId"); + + b.ToTable("BlackListed"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("BlogPostId") + .HasColumnType("bigint"); + + b.HasKey("CircleId", "BlogPostId"); + + b.HasIndex("BlogPostId"); + + b.ToTable("CircleAuthorizationToBlogPost"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.Property("CircleId") + .HasColumnType("bigint"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Access") + .HasColumnType("smallint"); + + b.HasKey("CircleId", "Path", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("CircleAuthorizationToFile"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ContactCredits") + .HasColumnType("bigint"); + + b.Property("Credits") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.ToTable("BankStatus"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("AllowMonthlyEmail") + .HasColumnType("boolean"); + + b.Property("Avatar") + .ValueGeneratedOnAdd() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasDefaultValue("/images/Users/icon_user.png"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DedicatedGoogleCalendar") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("DiskQuota") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(524288000L); + + b.Property("DiskUsage") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxFileSize") + .HasColumnType("bigint"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddressId") + .HasColumnType("bigint"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PostalAddressId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Impact") + .HasColumnType("numeric"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BalanceId"); + + b.ToTable("BalanceImpact"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("BIC") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("BankedKey") + .HasColumnType("integer"); + + b.Property("IBAN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WicketCode") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("BankIdentity"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("Currency") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("EstimateTemplateId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UnitaryCost") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("EstimateTemplateId"); + + b.ToTable("CommandLine"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachedFilesString") + .IsRequired() + .HasColumnType("text"); + + b.Property("AttachedGraphicsString") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProviderValidationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("CommandId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Estimates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "EstimateId"); + + b.HasIndex("EstimateId"); + + b.ToTable("EstimateAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EstimateTemplates"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.ExceptionSIREN", b => + { + b.Property("SIREN") + .HasColumnType("text"); + + b.HasKey("SIREN"); + + b.ToTable("ExceptionsSIREN"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Consent") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("PaymentId") + .HasColumnType("text"); + + b.Property("PerformerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provisional") + .HasColumnType("numeric"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("ValidationDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.HasIndex("ClientId"); + + b.HasIndex("PaymentId"); + + b.HasIndex("PerformerId"); + + b.ToTable("NominativeServiceCommands"); + + b.HasDiscriminator("Discriminator").HasValue("NominativeServiceCommand"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.QueryAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "CommandId"); + + b.HasIndex("CommandId"); + + b.ToTable("QueryAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapturedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("CoordinateMax") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10000); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.Property("SignerId") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection("Strokes") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("EstimateId", "Type") + .IsUnique(); + + b.ToTable("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "PostId"); + + b.HasIndex("PostId"); + + b.ToTable("BlogAttachedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .HasMaxLength(56224) + .HasColumnType("character varying(56224)"); + + b.Property("AuthorId") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Photo") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("BlogSpot"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.Property("TagId") + .HasColumnType("bigint"); + + b.HasKey("PostId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("BlogTag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Article") + .IsRequired() + .HasColumnType("text"); + + b.Property("AuthorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentId") + .HasColumnType("bigint"); + + b.Property("ReceiverId") + .HasColumnType("bigint"); + + b.Property("UserCreated") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserModified") + .IsRequired() + .HasColumnType("text"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("ParentId"); + + b.HasIndex("ReceiverId"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Length") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("UploadedFiles"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.Property("PostId") + .HasColumnType("bigint"); + + b.HasKey("PostId"); + + b.ToTable("blogSpotPublications"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("OwnerId"); + + b.ToTable("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Reccurence") + .HasColumnType("integer"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScheduleOwnerId"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("ScheduledEvent"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.Property("ConnectionId") + .HasColumnType("text"); + + b.Property("ApplicationUserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("ConnectionId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("ChatConnection"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LatestJoinPart") + .HasColumnType("timestamp with time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("ChatRoom"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.Property("ChannelName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("ChannelName", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ChatRoomAccess"); + }); + + modelBuilder.Entity("Yavsc.Models.Cratie.Option", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("CodeScrutin") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code", "CodeScrutin"); + + b.ToTable("Option"); + }); + + modelBuilder.Entity("Yavsc.Models.Drawing.Color", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Blue") + .HasColumnType("smallint"); + + b.Property("Green") + .HasColumnType("smallint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Red") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.ToTable("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("ActionDistance") + .HasColumnType("integer"); + + b.Property("CarePrice") + .HasColumnType("numeric"); + + b.Property("FlatFeeDiscount") + .HasColumnType("numeric"); + + b.Property("HalfBalayagePrice") + .HasColumnType("numeric"); + + b.Property("HalfBrushingPrice") + .HasColumnType("numeric"); + + b.Property("HalfColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfDefrisPrice") + .HasColumnType("numeric"); + + b.Property("HalfFoldingPrice") + .HasColumnType("numeric"); + + b.Property("HalfMechPrice") + .HasColumnType("numeric"); + + b.Property("HalfMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("HalfPermanentPrice") + .HasColumnType("numeric"); + + b.Property("KidCutPrice") + .HasColumnType("numeric"); + + b.Property("LongBalayagePrice") + .HasColumnType("numeric"); + + b.Property("LongBrushingPrice") + .HasColumnType("numeric"); + + b.Property("LongColorPrice") + .HasColumnType("numeric"); + + b.Property("LongDefrisPrice") + .HasColumnType("numeric"); + + b.Property("LongFoldingPrice") + .HasColumnType("numeric"); + + b.Property("LongMechPrice") + .HasColumnType("numeric"); + + b.Property("LongMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("LongPermanentPrice") + .HasColumnType("numeric"); + + b.Property("ManBrushPrice") + .HasColumnType("numeric"); + + b.Property("ManCutPrice") + .HasColumnType("numeric"); + + b.Property("ScheduleOwnerId") + .HasColumnType("text"); + + b.Property("ShampooPrice") + .HasColumnType("numeric"); + + b.Property("ShortBalayagePrice") + .HasColumnType("numeric"); + + b.Property("ShortBrushingPrice") + .HasColumnType("numeric"); + + b.Property("ShortColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortDefrisPrice") + .HasColumnType("numeric"); + + b.Property("ShortFoldingPrice") + .HasColumnType("numeric"); + + b.Property("ShortMechPrice") + .HasColumnType("numeric"); + + b.Property("ShortMultiColorPrice") + .HasColumnType("numeric"); + + b.Property("ShortPermanentPrice") + .HasColumnType("numeric"); + + b.Property("WomenHalfCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenLongCutPrice") + .HasColumnType("numeric"); + + b.Property("WomenShortCutPrice") + .HasColumnType("numeric"); + + b.HasKey("UserId"); + + b.HasIndex("ScheduleOwnerId"); + + b.ToTable("BrusherProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Cares") + .HasColumnType("boolean"); + + b.Property("Cut") + .HasColumnType("boolean"); + + b.Property("Dressing") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Length") + .HasColumnType("integer"); + + b.Property("Shampoo") + .HasColumnType("boolean"); + + b.Property("Tech") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("HairPrestation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("QueryId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("PrestationId"); + + b.HasIndex("QueryId"); + + b.ToTable("HairPrestationCollectionItem"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Brand") + .HasColumnType("text"); + + b.Property("ColorId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ColorId"); + + b.ToTable("HairTaint"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.Property("TaintId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.HasKey("TaintId", "PrestationId"); + + b.HasIndex("PrestationId"); + + b.ToTable("HairTaintInstance"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Evolution.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ShortName") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Feature"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasMaxLength(10240) + .HasColumnType("character varying(10240)"); + + b.Property("FeatureId") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("Bug"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeclarationDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("LOCALTIMESTAMP"); + + b.Property("DeviceOwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LatestActivityUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Model") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DeviceId"); + + b.HasIndex("DeviceOwnerId"); + + b.ToTable("DeviceDeclaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("MatchExcerpt") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PatternId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("PatternId"); + + b.ToTable("DeclarationFlag"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("DeclarationId") + .HasColumnType("bigint"); + + b.Property("ModeratorId") + .HasColumnType("text"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeclarationId"); + + b.HasIndex("ModeratorId"); + + b.HasIndex("Timestamp"); + + b.ToTable("ModerationLogs", t => + { + t.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"); + }); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.RegexAlertPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Pattern") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.ToTable("RegexAlertPatterns"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DeclarantTokenId") + .HasColumnType("uuid"); + + b.Property("ScoreDelta") + .HasColumnType("integer"); + + b.Property("Sentiment") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrustTokenId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("SubmittedAt"); + + b.HasIndex("TrustTokenId"); + + b.ToTable("TrustDeclarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenSource") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TrustScore") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("TrustTokens"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Depth") + .HasColumnType("numeric"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("numeric"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.Property("Weight") + .HasColumnType("numeric"); + + b.Property("Width") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContextId") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ContextId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("For") + .HasColumnType("smallint"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Sender") + .HasColumnType("text"); + + b.Property("Topic") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Announce"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "NotificationId"); + + b.HasIndex("NotificationId"); + + b.ToTable("DismissClicked"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Instrument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Instrument"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("InstrumentId", "OwnerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("InstrumentRating"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.Property("OwnerProfileId") + .HasColumnType("text"); + + b.Property("DjSettingsUserId") + .HasColumnType("text"); + + b.Property("MusicLoverSettingsUserId") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("TendencyId") + .HasColumnType("bigint"); + + b.HasKey("OwnerProfileId"); + + b.HasIndex("DjSettingsUserId"); + + b.HasIndex("MusicLoverSettingsUserId"); + + b.HasIndex("TendencyId"); + + b.ToTable("MusicalPreference"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalTendency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("SoundCloudId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("DjSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.Property("InstrumentId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("InstrumentId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("Instrumentation"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("MusicLoverSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Property("CreationToken") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderReference") + .HasColumnType("text"); + + b.Property("PaypalPayerId") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("CreationToken"); + + b.HasIndex("ExecutorId"); + + b.ToTable("PayPalPayment"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Circle"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.Property("MemberId") + .HasColumnType("text"); + + b.Property("CircleId") + .HasColumnType("bigint"); + + b.HasKey("MemberId", "CircleId"); + + b.HasIndex("CircleId"); + + b.ToTable("CircleMembers"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("AddressId") + .HasColumnType("bigint"); + + b.Property("ApplicationUserId") + .HasColumnType("text"); + + b.Property("EMail") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("OwnerId", "UserId"); + + b.HasIndex("AddressId"); + + b.HasIndex("ApplicationUserId"); + + b.ToTable("Contact"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.Property("HRef") + .HasColumnType("text"); + + b.Property("Method") + .HasColumnType("text"); + + b.Property("BrusherProfileUserId") + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("PayPalPaymentCreationToken") + .HasColumnType("text"); + + b.Property("Rel") + .HasColumnType("text"); + + b.HasKey("HRef", "Method"); + + b.HasIndex("BrusherProfileUserId"); + + b.HasIndex("PayPalPaymentCreationToken"); + + b.ToTable("HyperLink"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Latitude") + .HasColumnType("double precision"); + + b.Property("Longitude") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.PostalAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("text"); + + b.Property("Street1") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SiteSkills"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DifferedFileName") + .HasColumnType("text"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Pitch") + .HasColumnType("text"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("LiveFlow"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Property("Code") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Hidden") + .HasColumnType("boolean"); + + b.Property("Moderated") + .HasColumnType("boolean"); + + b.Property("ModeratorGroupName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentCode") + .HasColumnType("text"); + + b.Property("Photo") + .HasColumnType("text"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SettingsClassName") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Code"); + + b.HasIndex("ParentCode"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FormationSettingsUserId") + .HasColumnType("text"); + + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("WorkingForId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FormationSettingsUserId"); + + b.HasIndex("PerformerId"); + + b.HasIndex("WorkingForId"); + + b.ToTable("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionName") + .HasColumnType("text"); + + b.Property("ActivityCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityCode"); + + b.ToTable("CommandForm"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Country", b => + { + b.Property("Code") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Code"); + + b.ToTable("Countries"); + + b.HasData( + new + { + Code = "fr", + DisplayName = "France" + }, + new + { + Code = "en", + DisplayName = "England" + }, + new + { + Code = "pt", + DisplayName = "Portugal" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DomaineActiviteCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Nom") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("DomaineActiviteCode", "Langue", "Nom") + .IsUnique(); + + b.ToTable("DictionnaireMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryCode") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("ErrorMessage") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RegularExpression") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CountryCode"); + + b.ToTable("PerformerCodeInputValidations"); + + b.HasData( + new + { + Id = 1L, + CountryCode = "fr", + ErrorMessage = "Le code FR doit contenir entre 9 et 14 chiffres.", + RegularExpression = "^[0-9]{9,14}$" + }, + new + { + Id = 2L, + CountryCode = "en", + ErrorMessage = "Le code EN doit contenir entre 8 et 14 caracteres alphanumeriques.", + RegularExpression = "^[A-Za-z0-9]{8,14}$" + }, + new + { + Id = 3L, + CountryCode = "pt", + ErrorMessage = "Le code PT doit contenir exactement 9 chiffres.", + RegularExpression = "^[0-9]{9}$" + }); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Property("PerformerId") + .HasColumnType("text"); + + b.Property("AcceptNotifications") + .HasColumnType("boolean"); + + b.Property("AcceptPublicContact") + .HasColumnType("boolean"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ExerciseCountryCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxDailyCost") + .HasColumnType("integer"); + + b.Property("MinDailyCost") + .HasColumnType("integer"); + + b.Property("OrganizationAddressId") + .HasColumnType("bigint"); + + b.Property("Rate") + .HasColumnType("integer"); + + b.Property("SIREN") + .IsRequired() + .HasColumnType("text"); + + b.Property("UseGeoLocalizationToReduceDistanceWithClients") + .HasColumnType("boolean"); + + b.Property("WebSite") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("PerformerId"); + + b.HasIndex("OrganizationAddressId"); + + b.ToTable("Performers"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("FormationSettings"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateSoumission") + .HasColumnType("timestamp with time zone"); + + b.Property("DateValidation") + .HasColumnType("timestamp with time zone"); + + b.Property("Definition") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DictionnaireMetierId") + .HasColumnType("bigint"); + + b.Property("Langue") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Mot") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProposeParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("StatutValidation") + .HasColumnType("integer"); + + b.Property("ValideParId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("ProposeParId"); + + b.HasIndex("ValideParId"); + + b.HasIndex("DictionnaireMetierId", "Langue", "Mot") + .IsUnique(); + + b.ToTable("TermeMetier"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.Property("DoesCode") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("DoesCode", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserActivities"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.Calendar.Period", b => + { + b.Property("Start") + .HasColumnType("timestamp with time zone"); + + b.Property("End") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Start", "End"); + + b.ToTable("Period"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.EMailing.MailingTemplate", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .HasMaxLength(65536) + .HasColumnType("character varying(65536)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplyToAddress") + .HasColumnType("text"); + + b.Property("ToSend") + .HasColumnType("integer"); + + b.Property("Topic") + .HasColumnType("text"); + + b.Property("UserCreated") + .HasColumnType("text"); + + b.Property("UserModified") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("MailingTemplate"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProjectBuildConfiguration"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Branch") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("GitRepositoryReference"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("AdditionalInfo") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("PrestationId") + .HasColumnType("bigint"); + + b.Property("SelectedProfileUserId") + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.HasIndex("PrestationId"); + + b.HasIndex("SelectedProfileUserId"); + + b.HasDiscriminator().HasValue("HairCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("HairMultiCutQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("HairMultiCutQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("HairMultiCutQuery"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("EventDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LocationId") + .HasColumnType("bigint"); + + b.Property("LocationType") + .HasColumnType("integer"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.HasIndex("LocationId"); + + b.ToTable("NominativeServiceCommands", t => + { + t.Property("EventDate") + .HasColumnName("RdvQuery_EventDate"); + + t.Property("LocationId") + .HasColumnName("RdvQuery_LocationId"); + }); + + b.HasDiscriminator().HasValue("RdvQuery"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasBaseType("Yavsc.Models.Billing.NominativeServiceCommand"); + + b.Property("GitId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasIndex("GitId"); + + b.HasDiscriminator().HasValue("Project"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResourceSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany() + .HasForeignKey("ApiResourceId1"); + + b.Navigation("ApiResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScopeProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.ApiScope", "Scope") + .WithMany() + .HasForeignKey("ScopeId1"); + + b.Navigation("Scope"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("IdentityServer8.EntityFramework.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId1"); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceClaim", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResourceProperty", b => + { + b.HasOne("IdentityServer8.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("IdentityResource"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Yavsc.Models.Access.Ban", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "TargetUser") + .WithMany() + .HasForeignKey("TargetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.BlackListed", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("BlackList") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToBlogPost", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Target") + .WithMany("ACL") + .HasForeignKey("BlogPostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Target"); + }); + + modelBuilder.Entity("Yavsc.Models.Access.CircleAuthorizationToFile", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Allowed") + .WithMany() + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Allowed"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.AccountBalance", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithOne("AccountBalance") + .HasForeignKey("Yavsc.Models.AccountBalance", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "PostalAddress") + .WithMany() + .HasForeignKey("PostalAddressId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.BalanceImpact", b => + { + b.HasOne("Yavsc.Models.AccountBalance", "Balance") + .WithMany() + .HasForeignKey("BalanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Balance"); + }); + + modelBuilder.Entity("Yavsc.Models.Bank.BankIdentity", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("BankInfo") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.CommandLine", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", null) + .WithMany("Bill") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.EstimateTemplate", null) + .WithMany("Bill") + .HasForeignKey("EstimateTemplateId"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Query") + .WithMany() + .HasForeignKey("CommandId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Owner"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateAttachedFile", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany() + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany() + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", "Regularization") + .WithMany() + .HasForeignKey("PaymentId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "PerformerProfile") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Context"); + + b.Navigation("PerformerProfile"); + + b.Navigation("Regularization"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.QueryAttachedFile", b => + { + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Command") + .WithMany() + .HasForeignKey("CommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Command"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany("Signatures") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("Signer"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogAttachedFile", b => + { + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany() + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("Posts") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogTag", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Tags") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Relationship.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Author") + .WithMany("BlogComments") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.Comment", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithMany("Comments") + .HasForeignKey("ReceiverId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Parent"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => + { + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithOne("Publication") + .HasForeignKey("Yavsc.Models.BlogSpotPublication", "PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.ScheduledEvent", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", null) + .WithMany("Events") + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Server.Models.Calendar.Period", "Period") + .WithMany() + .HasForeignKey("PeriodStart", "PeriodEnd"); + + b.Navigation("Period"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatConnection", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Connections") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany("Rooms") + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoomAccess", b => + { + b.HasOne("Yavsc.Models.Chat.ChatRoom", "Room") + .WithMany("Moderation") + .HasForeignKey("ChannelName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany("RoomAccess") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.HasOne("Yavsc.Models.Calendar.Schedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleOwnerId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "BaseProfile") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseProfile"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestationCollectionItem", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairMultiCutQuery", "Query") + .WithMany("Prestations") + .HasForeignKey("QueryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Query"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaint", b => + { + b.HasOne("Yavsc.Models.Drawing.Color", "Color") + .WithMany() + .HasForeignKey("ColorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Color"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairTaintInstance", b => + { + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany("Taints") + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.HairTaint", "Taint") + .WithMany() + .HasForeignKey("TaintId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prestation"); + + b.Navigation("Taint"); + }); + + modelBuilder.Entity("Yavsc.Models.IT.Fixing.Bug", b => + { + b.HasOne("Yavsc.Models.IT.Evolution.Feature", "False") + .WithMany() + .HasForeignKey("FeatureId"); + + b.Navigation("False"); + }); + + modelBuilder.Entity("Yavsc.Models.Identity.DeviceDeclaration", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "DeviceOwner") + .WithMany("DeviceDeclaration") + .HasForeignKey("DeviceOwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceOwner"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.DeclarationFlag", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany("Flags") + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Kyc.RegexAlertPattern", "Pattern") + .WithMany() + .HasForeignKey("PatternId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + + b.Navigation("Pattern"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.ModerationLog", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustDeclaration", "Declaration") + .WithMany() + .HasForeignKey("DeclarationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Declaration"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.HasOne("Yavsc.Models.Kyc.TrustToken", "Subject") + .WithMany("Declarations") + .HasForeignKey("TrustTokenId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Subject"); + }); + + modelBuilder.Entity("Yavsc.Models.Market.Service", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Services") + .HasForeignKey("ContextId"); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.Announce", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Messaging.DismissClicked", b => + { + b.HasOne("Yavsc.Abstract.Models.Messaging.Notification", "Notified") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notified"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.InstrumentRating", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Instrument") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Profile") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instrument"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.MusicalPreference", b => + { + b.HasOne("Yavsc.Models.Musical.Profiles.DjSettings", null) + .WithMany("SoundColor") + .HasForeignKey("DjSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.Profiles.MusicLoverSettings", null) + .WithMany("SoundColor") + .HasForeignKey("MusicLoverSettingsUserId"); + + b.HasOne("Yavsc.Models.Musical.MusicalTendency", "MusicalTendency") + .WithMany() + .HasForeignKey("TendencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicalTendency"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.Instrumentation", b => + { + b.HasOne("Yavsc.Models.Musical.Instrument", "Tool") + .WithMany() + .HasForeignKey("InstrumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tool"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Executor") + .WithMany() + .HasForeignKey("ExecutorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Executor"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Circles") + .HasForeignKey("ApplicationUserId"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.CircleMember", b => + { + b.HasOne("Yavsc.Models.Relationship.Circle", "Circle") + .WithMany("Members") + .HasForeignKey("CircleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Member") + .WithMany("Membership") + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Circle"); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Contact", b => + { + b.HasOne("Yavsc.Models.Relationship.PostalAddress", "PostalAddress") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", null) + .WithMany("Book") + .HasForeignKey("ApplicationUserId"); + + b.Navigation("PostalAddress"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.HyperLink", b => + { + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", null) + .WithMany("Links") + .HasForeignKey("BrusherProfileUserId"); + + b.HasOne("Yavsc.Models.Payment.PayPalPayment", null) + .WithMany("Links") + .HasForeignKey("PayPalPaymentCreationToken"); + }); + + modelBuilder.Entity("Yavsc.Models.Streaming.LiveFlow", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Parent") + .WithMany("Children") + .HasForeignKey("ParentCode"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CoWorking", b => + { + b.HasOne("Yavsc.Models.Workflow.Profiles.FormationSettings", null) + .WithMany("CoWorking") + .HasForeignKey("FormationSettingsUserId"); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "Performer") + .WithMany() + .HasForeignKey("PerformerId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "WorkingFor") + .WithMany() + .HasForeignKey("WorkingForId"); + + b.Navigation("Performer"); + + b.Navigation("WorkingFor"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.CommandForm", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Context") + .WithMany("Forms") + .HasForeignKey("ActivityCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Context"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "DomaineActivite") + .WithMany() + .HasForeignKey("DomaineActiviteCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DomaineActivite"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerCodeInputValidation", b => + { + b.HasOne("Yavsc.Models.Workflow.Country", "Country") + .WithMany() + .HasForeignKey("CountryCode") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "OrganizationAddress") + .WithMany() + .HasForeignKey("OrganizationAddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "Performer") + .WithMany() + .HasForeignKey("PerformerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationAddress"); + + b.Navigation("Performer"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.TermeMetier", b => + { + b.HasOne("Yavsc.Models.Workflow.DictionnaireMetier", "DictionnaireMetier") + .WithMany("Termes") + .HasForeignKey("DictionnaireMetierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.ApplicationUser", "ProposePar") + .WithMany() + .HasForeignKey("ProposeParId"); + + b.HasOne("Yavsc.Models.ApplicationUser", "ValidePar") + .WithMany() + .HasForeignKey("ValideParId"); + + b.Navigation("DictionnaireMetier"); + + b.Navigation("ProposePar"); + + b.Navigation("ValidePar"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.UserActivity", b => + { + b.HasOne("Yavsc.Models.Workflow.Activity", "Does") + .WithMany() + .HasForeignKey("DoesCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Workflow.PerformerProfile", "User") + .WithMany("Activity") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Does"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.ProjectBuildConfiguration", b => + { + b.HasOne("Yavsc.Server.Models.IT.Project", "TargetProject") + .WithMany("Configurations") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TargetProject"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId"); + + b.HasOne("Yavsc.Models.Haircut.HairPrestation", "Prestation") + .WithMany() + .HasForeignKey("PrestationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Haircut.BrusherProfile", "SelectedProfile") + .WithMany() + .HasForeignKey("SelectedProfileUserId"); + + b.Navigation("Location"); + + b.Navigation("Prestation"); + + b.Navigation("SelectedProfile"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.RdvQuery", b => + { + b.HasOne("Yavsc.Models.Relationship.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.HasOne("Yavsc.Server.Models.IT.SourceCode.GitRepositoryReference", "Repository") + .WithMany() + .HasForeignKey("GitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Repository"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("IdentityServer8.EntityFramework.Entities.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Yavsc.Models.ApplicationUser", b => + { + b.Navigation("AccountBalance"); + + b.Navigation("BankInfo"); + + b.Navigation("BlackList"); + + b.Navigation("BlogComments"); + + b.Navigation("Book"); + + b.Navigation("Circles"); + + b.Navigation("Connections"); + + b.Navigation("DeviceDeclaration"); + + b.Navigation("Membership"); + + b.Navigation("Posts"); + + b.Navigation("RoomAccess"); + + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.Estimate", b => + { + b.Navigation("Bill"); + + b.Navigation("Signatures"); + }); + + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => + { + b.Navigation("Bill"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.BlogPost", b => + { + b.Navigation("ACL"); + + b.Navigation("Comments"); + + b.Navigation("Publication") + .IsRequired(); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Yavsc.Models.Blog.Comment", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Yavsc.Models.Chat.ChatRoom", b => + { + b.Navigation("Moderation"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.BrusherProfile", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairPrestation", b => + { + b.Navigation("Taints"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustDeclaration", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Yavsc.Models.Kyc.TrustToken", b => + { + b.Navigation("Declarations"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.DjSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Musical.Profiles.MusicLoverSettings", b => + { + b.Navigation("SoundColor"); + }); + + modelBuilder.Entity("Yavsc.Models.Payment.PayPalPayment", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Yavsc.Models.Relationship.Circle", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Activity", b => + { + b.Navigation("Children"); + + b.Navigation("Forms"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.DictionnaireMetier", b => + { + b.Navigation("Termes"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.PerformerProfile", b => + { + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Yavsc.Models.Workflow.Profiles.FormationSettings", b => + { + b.Navigation("CoWorking"); + }); + + modelBuilder.Entity("Yavsc.Models.Haircut.HairMultiCutQuery", b => + { + b.Navigation("Prestations"); + }); + + modelBuilder.Entity("Yavsc.Server.Models.IT.Project", b => + { + b.Navigation("Configurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Yavsc.Org/Migrations/20260924150433_UserFileAttachments.cs b/src/Yavsc.Org/Migrations/20260924150433_UserFileAttachments.cs new file mode 100644 index 000000000..593bf0b26 --- /dev/null +++ b/src/Yavsc.Org/Migrations/20260924150433_UserFileAttachments.cs @@ -0,0 +1,148 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yavsc.Migrations +{ + /// + public partial class UserFileAttachments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Path", + table: "UploadedFiles", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ContentType", + table: "UploadedFiles", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "OwnerId", + table: "UploadedFiles", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "EstimateAttachedFiles", + columns: table => new + { + FileId = table.Column(type: "bigint", nullable: false), + EstimateId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EstimateAttachedFiles", x => new { x.FileId, x.EstimateId }); + table.ForeignKey( + name: "FK_EstimateAttachedFiles_Estimates_EstimateId", + column: x => x.EstimateId, + principalTable: "Estimates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_EstimateAttachedFiles_UploadedFiles_FileId", + column: x => x.FileId, + principalTable: "UploadedFiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "QueryAttachedFiles", + columns: table => new + { + FileId = table.Column(type: "bigint", nullable: false), + CommandId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QueryAttachedFiles", x => new { x.FileId, x.CommandId }); + table.ForeignKey( + name: "FK_QueryAttachedFiles_NominativeServiceCommands_CommandId", + column: x => x.CommandId, + principalTable: "NominativeServiceCommands", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_QueryAttachedFiles_UploadedFiles_FileId", + column: x => x.FileId, + principalTable: "UploadedFiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UploadedFiles_OwnerId", + table: "UploadedFiles", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_EstimateAttachedFiles_EstimateId", + table: "EstimateAttachedFiles", + column: "EstimateId"); + + migrationBuilder.CreateIndex( + name: "IX_QueryAttachedFiles_CommandId", + table: "QueryAttachedFiles", + column: "CommandId"); + + migrationBuilder.AddForeignKey( + name: "FK_UploadedFiles_AspNetUsers_OwnerId", + table: "UploadedFiles", + column: "OwnerId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_UploadedFiles_AspNetUsers_OwnerId", + table: "UploadedFiles"); + + migrationBuilder.DropTable( + name: "EstimateAttachedFiles"); + + migrationBuilder.DropTable( + name: "QueryAttachedFiles"); + + migrationBuilder.DropIndex( + name: "IX_UploadedFiles_OwnerId", + table: "UploadedFiles"); + + migrationBuilder.DropColumn( + name: "OwnerId", + table: "UploadedFiles"); + + migrationBuilder.AlterColumn( + name: "Path", + table: "UploadedFiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "ContentType", + table: "UploadedFiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs index 84960c5bc..711408cd4 100644 --- a/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Yavsc.Org/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Yavsc.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("ProductVersion", "10.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -1394,6 +1394,21 @@ namespace Yavsc.Migrations b.ToTable("Estimates"); }); + modelBuilder.Entity("Yavsc.Models.Billing.EstimateAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("EstimateId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "EstimateId"); + + b.HasIndex("EstimateId"); + + b.ToTable("EstimateAttachedFiles"); + }); + modelBuilder.Entity("Yavsc.Models.Billing.EstimateTemplate", b => { b.Property("Id") @@ -1502,6 +1517,21 @@ namespace Yavsc.Migrations b.UseTphMappingStrategy(); }); + modelBuilder.Entity("Yavsc.Models.Billing.QueryAttachedFile", b => + { + b.Property("FileId") + .HasColumnType("bigint"); + + b.Property("CommandId") + .HasColumnType("bigint"); + + b.HasKey("FileId", "CommandId"); + + b.HasIndex("CommandId"); + + b.ToTable("QueryAttachedFiles"); + }); + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => { b.Property("Id") @@ -1521,10 +1551,6 @@ namespace Yavsc.Migrations b.Property("EstimateId") .HasColumnType("bigint"); - b.Property("FilePath") - .IsRequired() - .HasColumnType("text"); - b.Property("SignerId") .IsRequired() .HasColumnType("text"); @@ -1678,25 +1704,32 @@ namespace Yavsc.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("ContentType") + .IsRequired() .HasColumnType("text"); b.Property("Length") .HasColumnType("bigint"); + b.Property("OwnerId") + .HasColumnType("text"); + b.Property("Path") + .IsRequired() .HasColumnType("text"); b.HasKey("Id"); + b.HasIndex("OwnerId"); + b.ToTable("UploadedFiles"); }); modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => { - b.Property("BlogpostId") + b.Property("PostId") .HasColumnType("bigint"); - b.HasKey("BlogpostId"); + b.HasKey("PostId"); b.ToTable("blogSpotPublications"); }); @@ -3831,6 +3864,25 @@ namespace Yavsc.Migrations b.Navigation("Query"); }); + modelBuilder.Entity("Yavsc.Models.Billing.EstimateAttachedFile", b => + { + b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") + .WithMany() + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("File"); + }); + modelBuilder.Entity("Yavsc.Models.Billing.NominativeServiceCommand", b => { b.HasOne("Yavsc.Models.Workflow.Activity", "Context") @@ -3864,6 +3916,25 @@ namespace Yavsc.Migrations b.Navigation("Regularization"); }); + modelBuilder.Entity("Yavsc.Models.Billing.QueryAttachedFile", b => + { + b.HasOne("Yavsc.Models.Billing.NominativeServiceCommand", "Command") + .WithMany() + .HasForeignKey("CommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yavsc.Models.Blog.UploadedFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Command"); + + b.Navigation("File"); + }); + modelBuilder.Entity("Yavsc.Models.Billing.Signature", b => { b.HasOne("Yavsc.Models.Billing.Estimate", "Estimate") @@ -3956,15 +4027,24 @@ namespace Yavsc.Migrations b.Navigation("Post"); }); + modelBuilder.Entity("Yavsc.Models.Blog.UploadedFile", b => + { + b.HasOne("Yavsc.Models.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId"); + + b.Navigation("Owner"); + }); + modelBuilder.Entity("Yavsc.Models.BlogSpotPublication", b => { - b.HasOne("Yavsc.Models.Blog.BlogPost", "BlogPost") - .WithMany() - .HasForeignKey("BlogpostId") + b.HasOne("Yavsc.Models.Blog.BlogPost", "Post") + .WithOne("Publication") + .HasForeignKey("Yavsc.Models.BlogSpotPublication", "PostId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("BlogPost"); + b.Navigation("Post"); }); modelBuilder.Entity("Yavsc.Models.Calendar.Schedule", b => @@ -4614,6 +4694,9 @@ namespace Yavsc.Migrations b.Navigation("Comments"); + b.Navigation("Publication") + .IsRequired(); + b.Navigation("Tags"); }); diff --git a/src/Yavsc.Org/ViewComponents/BillViewComponent.cs b/src/Yavsc.Org/ViewComponents/BillViewComponent.cs deleted file mode 100644 index 0217ebcf2..000000000 --- a/src/Yavsc.Org/ViewComponents/BillViewComponent.cs +++ /dev/null @@ -1,108 +0,0 @@ - -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Localization; -using Yavsc.Billing; -using Yavsc.Helpers; -using Yavsc.Models; -using Yavsc.ViewModels; -using Yavsc.ViewModels.Gen; -using Yavsc.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace Yavsc.ViewComponents -{ - public class BillViewComponent : ViewComponent - { - readonly ApplicationDbContext dbContext; - readonly IBillingService billing; - readonly IStringLocalizer localizer; - private readonly SiteSettings siteSettings; - - public BillViewComponent(ApplicationDbContext dbContext, - IStringLocalizer localizer, - IBillingService billing, - IOptions siteSettings) - { - this.billing = billing; - this.dbContext = dbContext; - this.localizer = localizer; - this.siteSettings = siteSettings.Value; - } - - public async Task InvokeAsync( - string code, - IBillable billable, - OutputFormat format, - bool asBill, - SiteSettings settings - ) - { - var di = new DirectoryInfo(settings.Bills); - var dia = new DirectoryInfo(settings.Avatars); - ViewBag.BillsDir = di.FullName; - ViewBag.AvatarsDir = dia.FullName; - ViewBag.AsBill = asBill; // vrai pour une facture, sinon, c'est un devis - ViewBag.Acquitted = billable.GetIsAcquitted(); - - ViewBag.BillingCode = code; - var client = await dbContext.Users - .Include(u=>u.PostalAddress) - .SingleAsync(u=>u.Id == billable.ClientId); - ViewBag.Client = client; - var performer = await dbContext.Users - .Include(u=>u.BankInfo) - .Include(u=>u.PostalAddress) - .SingleAsync(u=>u.Id == billable.PerformerId); - ViewBag.Performer = performer; - string clientAddress = client.PostalAddress?.Address ?? null; - ViewBag.ClientAddress = clientAddress.SplitAddressToTeX(); - - - var profile = await dbContext.Performers - .Include(p=>p.OrganizationAddress) - .SingleAsync(p=>p.PerformerId == billable.PerformerId); - ViewBag.PerformerProfile = profile; - ViewBag.ActivityLabel = (await dbContext.Activities.SingleAsync(a => a.Code == billable.ActivityCode)).Name; - - var proAddr = profile.OrganizationAddress.Address; - ViewBag.PerformerOrganizationAddress = proAddr.SplitAddressToTeX() ; - ViewBag.FooterPerformerOrganizationAddress = proAddr.SplitAddressToTeX(", "); - - ViewBag.PerformerAddress = performer.PostalAddress?.Address.SplitAddressToTeX() ; - switch (format) { - case OutputFormat.LaTeX : - return this.View("Bill_tex", billable); - case OutputFormat.Pdf : - string tex = null; - var oldWriter = ViewComponentContext.ViewContext.Writer; - using (var writer = new StringWriter()) - { - this.ViewComponentContext.ViewContext.Writer = writer; - var resultTex = View("Bill_tex", billable); - await resultTex.ExecuteAsync(this.ViewComponentContext); - tex = writer.ToString(); - } - ViewComponentContext.ViewContext.Writer = oldWriter; - - var pdfGenerationViewModel = new PdfGenerationViewModel - { - Temp = siteSettings.TempDir, - TeXSource = tex, - DestDir = siteSettings.Bills, - BaseFileName = billable.GetFileBaseName(billing) - }; - if (settings.GenerateEstimatePdf(pdfGenerationViewModel)) { - return this.View(new { Generated = pdfGenerationViewModel.BaseFileName+".pdf" }); - } else { - return View(new { Error = pdfGenerationViewModel.GenerationErrorMessage } ); - } - } - ViewBag.BillFileInfo = billable.GetBillInfo(billing, siteSettings); - ViewBag.Settings = settings; - return View("Default",billable); - - } - - } -} diff --git a/src/Yavsc.Org/ViewComponents/CommentViewComponent.cs b/src/Yavsc.Org/ViewComponents/CommentViewComponent.cs index 6f85a953f..32b7a14b2 100644 --- a/src/Yavsc.Org/ViewComponents/CommentViewComponent.cs +++ b/src/Yavsc.Org/ViewComponents/CommentViewComponent.cs @@ -18,7 +18,10 @@ namespace Yavsc.ViewComponents } public async Task InvokeAsync(long id) { - var comment = await context.Comment.Include(c=>c.Children).FirstOrDefaultAsync(c => c.Id==id); + var comment = await context.Comment + .Include(c => c.Children) + .Include(c => c.Author) + .FirstOrDefaultAsync(c => c.Id==id); if (comment == null) throw new InvalidOperationException(); ViewBag.apictlr = "/api/v1/blogcomments"; diff --git a/src/Yavsc.Org/Views/Account/Delete.cshtml b/src/Yavsc.Org/Views/Account/Delete.cshtml new file mode 100644 index 000000000..7443d3d2a --- /dev/null +++ b/src/Yavsc.Org/Views/Account/Delete.cshtml @@ -0,0 +1,21 @@ +@model UnregisterViewModel +@{ + ViewBag.Title = "Supprimer mon compte"; +} + +

@ViewBag.Title

+ +
+
+

+ Cette action est immédiate et irréversible. Toutes les données + associées à votre compte seront supprimées, et vous serez déconnecté. +

+ +
+ @Html.Hidden("UserId") + + Annuler +
+
+
\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Blogspot/Create.cshtml b/src/Yavsc.Org/Views/Blogspot/Create.cshtml deleted file mode 100644 index 6831b0461..000000000 --- a/src/Yavsc.Org/Views/Blogspot/Create.cshtml +++ /dev/null @@ -1,88 +0,0 @@ -@model BlogPostEditViewModel - -@{ - ViewBag.Title = "Blog post edition"; -} - -@section header { - - - - - - @{ await Html.RenderPartialAsync("_FSScriptsPartial"); } - -} - -

Blog post

- - -
- - -

@Model.Title

- -
@Model.Article
- -
-
-
- -
- -
- -
- - - -
-
- -
- -
- - - -
-
- -
- -
- - - -
-
- -
- -
- @await Component.InvokeAsync("CirclesControl", Model) -
-
- -
- -
- -
-
- -
-
- -
-
-
-
-@await Component.InvokeAsync(name: "Directory",arguments: new {subdir="."}) -
- @{ await Html.RenderPartialAsync("_PostFilesPartial"); } -
- diff --git a/src/Yavsc.Org/Views/Blogspot/Details.cshtml b/src/Yavsc.Org/Views/Blogspot/Details.cshtml index dc0bea8a8..0b07b0d94 100644 --- a/src/Yavsc.Org/Views/Blogspot/Details.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Details.cshtml @@ -1,105 +1,235 @@ @model BlogPost @{ - ViewBag.Title=Model.Title; + ViewBag.Title = Model.Title; } -@section scripts { - - - +@section header { } +@section scripts { + +} + +
+
+ @if (!string.IsNullOrEmpty(Model.Photo)) + { + @Model.Title + } +
+

@Model.Title

+ +
+
+ @Model.Article
- -

@Model.Title

- - @Html.DisplayFor(m=>m.Author) - @Html.DisplayFor(m=>m.Article) -
- @if (Model.Comments!=null) { - foreach (var comment in Model.Comments.Where(c=>c.ParentId==null)) { + @if (Model.Comments != null) + { + foreach (var comment in Model.Comments.Where(c => c.ParentId == null)) + { @await Component.InvokeAsync("Comment", new { id = comment.Id }) } }
-@if (User.GetUserId()!=null) { -
-
- - - - - -
- -
- } - else { -

Vous devez être identifié pour commenter.

- } - - @if ((await AuthorizationService.AuthorizeAsync(User, Model, new EditPermission())).Succeeded) { - Edit - } - Back to List + @if (User.GetUserId() != null) + { +
+
+ + + + + +
+
+ } + else + { +

Vous devez être identifié pour commenter.

+ } + + Back to List +
\ No newline at end of file diff --git a/src/Yavsc.Org/Views/Blogspot/Edit.cshtml b/src/Yavsc.Org/Views/Blogspot/Edit.cshtml deleted file mode 100644 index 4645d45db..000000000 --- a/src/Yavsc.Org/Views/Blogspot/Edit.cshtml +++ /dev/null @@ -1,120 +0,0 @@ -@model BlogPostEditViewModel -@{ - ViewBag.Title = "Blog post edition"; -} - -@section header { - - - - - - - - @{ await Html.RenderPartialAsync("_FSScriptsPartial"); } - -} - -

Blog post

- - -
- - - - - -

@Model.Title

- -
-
- -
- @Html.HiddenFor(m=>m.Id) - -
- -
- - - -
-
- -
- -
- - - -
-
-
- -
- - - -
-
-
- -
- @await Component.InvokeAsync("CirclesControl", Model) -
-
- -
- -
- -
-
- -
-
- -
-
-
-
-@await Component.InvokeAsync("Directory","") - - -
- -
- @{ await Html.RenderPartialAsync("_PostFilesPartial"); } -
- diff --git a/src/Yavsc.Org/Views/Blogspot/Index.cshtml b/src/Yavsc.Org/Views/Blogspot/Index.cshtml index 1582041cf..9f29a9417 100644 --- a/src/Yavsc.Org/Views/Blogspot/Index.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Index.cshtml @@ -1,92 +1,155 @@ - @model IEnumerable @{ ViewBag.Title = "Blogs, l'index"; + + // Pagination reflects the Index(string id, int skip, int take) + // call profile. The service applies Skip/Take server-side, so + // the view only needs the current page params to build Prev/Next + // links — it does not receive a total count. + int currentSkip = Context.Request.Query.TryGetValue("skip", out var skipVal) && int.TryParse(skipVal, out var s) ? s : 0; + int currentTake = Context.Request.Query.TryGetValue("take", out var takeVal) && int.TryParse(takeVal, out var t) ? t : 25; + + var posts = Model.ToList(); + bool hasPrev = currentSkip > 0; + bool hasNext = posts.Count >= currentTake; + + int prevSkip = Math.Max(0, currentSkip - currentTake); + int nextSkip = currentSkip + currentTake; + + int maxSummaryLen = 256; } @section header { -} -@section scripts { - + .blog-index .post-meta { + font-size: .8rem; + color: var(--bs-secondary-color, #6c757d); + } + .blog-index .post-author img { + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + vertical-align: middle; + margin-right: .25rem; + } + .blog-index .actiongroup { + margin-top: .5rem; + display: flex; + gap: .5rem; + } + .blog-index .post-summary { + margin: .25rem 0; + } + .blog-index .post-summary > p:last-child { + margin-bottom: 0; + } + .blog-pager { + display: flex; + justify-content: space-between; + margin-top: 1.5rem; + } + }

@ViewBag.Title

@ViewBag.StatusMessage

-@if (User.IsSignedIn()) { -

- Create a new article -

+ +@if (!posts.Any()) +{ +

Néant

} - -
-@{ - int maxTextLen = 75; - foreach (var post in Model) { -
- - - -
-

@post.Title

-
-
- - @post.Article - @Html.DisplayFor(m => post.Author) - - posté le @post.DateCreated.ToString("dddd d MMM yyyy à H:mm") - @if ((post.DateModified - post.DateCreated).Minutes > 0){  - @:- Modifié le @post.DateModified.ToString("dddd d MMM yyyy à H:mm") - } - -
-
- @if ((await AuthorizationService.AuthorizeAsync(User, post, new ReadPermission())).Succeeded) +else +{ +
+ @foreach (var post in posts) + { + var canEdit = (await AuthorizationService.AuthorizeAsync(User, post, new EditPermission())).Succeeded; +
+ @if (!string.IsNullOrEmpty(post.Photo)) { - Details + + @post.Title + } - else - { - Details - } - @if ((await AuthorizationService.AuthorizeAsync(User, post, new EditPermission())).Succeeded) - { - Edit - - Delete - - } -
+
+

+ @post.Title +

+
+ + @post.Article + +
+ +
+ Details + @if (canEdit) + { + Delete + } +
+
+ + }
- } -} -
-@if(Model.Count()==0){

Néant

} + +} \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Blogspot/Title.cshtml b/src/Yavsc.Org/Views/Blogspot/Title.cshtml index 7bdf0468c..9421a5b01 100644 --- a/src/Yavsc.Org/Views/Blogspot/Title.cshtml +++ b/src/Yavsc.Org/Views/Blogspot/Title.cshtml @@ -3,28 +3,24 @@

@ViewBag.StatusMessage

-

- Poster au même titre -

- - + @foreach (var item in Model) { -
- @((item.Article?.Length > 256) ? item.Article.Substring(0, 256) + " ..." : item.Article) + @((item.Article?.Length > 256) ? item.Article.Substring(0, 256) + " ..." : item.Article) (@item.Author.UserName , posté le @item.DateCreated.ToString("dddd d MMM yyyy à H:mm") - @if ((item.DateModified - item.DateCreated).Minutes > 0){  + @if ((item.DateModified - item.DateCreated).Minutes > 0){ @:- Modifié le @item.DateModified.ToString("dddd d MMM yyyy à H:mm") }) +
    @if ((await AuthorizationService.AuthorizeAsync(User, item, new ReadPermission())).Succeeded) {
  • @@ -32,8 +28,7 @@
  • } @if ((await AuthorizationService.AuthorizeAsync(User, item, new EditPermission())).Succeeded) { -
  • Edit -
  • +
  • Delete
  • } diff --git a/src/Yavsc.Org/Views/HairCutCommand/CGV.cshtml b/src/Yavsc.Org/Views/HairCutCommand/CGV.cshtml index 1428ee674..08d01cf63 100644 --- a/src/Yavsc.Org/Views/HairCutCommand/CGV.cshtml +++ b/src/Yavsc.Org/Views/HairCutCommand/CGV.cshtml @@ -1,13 +1,13 @@ -@{ +@{ ViewBag.Title = "Conditions Générales de Vente"; }

    @ViewBag.Title

    - + Le paiement intervient à la commande, quelque soit la date d'intervention. -Vous pouvez annuler votre commande depuis la [liste de vos commande en cours](/HairCutCommand/), - -Vous pouvez demander le remboursement d'un paiement, dans le cadre d'une réclamation sur l'execution +Vous pouvez annuler votre commande depuis la [liste de vos commande en cours](/HairCutCommand/), + +Vous pouvez demander le remboursement d'un paiement, dans le cadre d'une réclamation sur l'execution de votre commande, en nous contactant via courrier éléctronique ou postal, [que vour retrouverez sur la page de contact](/Home/Contact), votre demande sera rapidement traitée. - + diff --git a/src/Yavsc.Org/Views/Home/About.pt.cshtml b/src/Yavsc.Org/Views/Home/About.pt.cshtml index 487a1ab36..a805999ce 100755 --- a/src/Yavsc.Org/Views/Home/About.pt.cshtml +++ b/src/Yavsc.Org/Views/Home/About.pt.cshtml @@ -5,7 +5,7 @@

    @SiteSettings.Value.Title - objetivo

    - + = O objetivo Esta aplicação é construída para conectar artistas @@ -87,51 +87,51 @@ que imediatamente desativa as publicações associadas às suas informações, e planeia a eliminação completa desta informação no prazo de quinze dias do aplicativo, a menos que seja um pedido contraditório. A operação é anulável até duas semanas após a sua programação. - + -Este é o meu site perso, uma configuração de _Yavsc_ (outro negócio muito pequeno). +Este é o meu site perso, uma configuração de _Yavsc_ (outro negócio muito pequeno). * [README](https://forgejo.pschneider.fr/notazof/yavsc/blob/vnext/README.md) * [licença: GNU GPL v3](https://forgejo.pschneider.fr/notazof/yavsc/blob/vnext/LICENSE) Outras instalações: - - + + * [Coiffure](http://coiffure.pschneider.fr) * [ZicMoove](http://linkmuse.pschneider.fr) * [Yavsc](http://yavsc.pschneider.fr) - + - + Yet Another Very Small Company ... * [README](https://forgejo.pschneider.fr/notazof/yavsc/blob/vnext/README.md) * [license: GNU FPL v3](https://forgejo.pschneider.fr/notazof/yavsc/blob/vnext/LICENSE) - + - -## Yet Another Very Small Company : + +## Yet Another Very Small Company : * [README](https://forgejo.pschneider.fr/notazof/yavsc/blob/vnext/README.md) * [license: GNU FPL v3](https://forgejo.pschneider.fr/notazof/yavsc/blob/vnext/LICENSE) En production: - + * [Lua](https://lua.pschneider.fr) * [Yavsc](https://yavsc.pschneider.fr) - + - + Você está no site da ordem em cabeleireiro em casa de Soraya Boudjouraf, um ás de cabeleireiro, que trabalha na região de Paris. @@ -139,11 +139,11 @@ Ao validar um formulário de pedido aqui, é para ela que você notifica sua sol Você pode [Deixe-lhe o seu número de telefone] (/ HairCutCommand / HairCut? ActivityCode = Brush & performerId = 1bd841ab-c305-4971-940d-7ddca818310c) e / ou detalhes sobre o seu pedido, -ela vai ligar de volta. +ela vai ligar de volta. - + ## Este é um site de desenvolvimento. Este recurso só diz respeito ao desenvolvimento do software que o implementa. @@ -156,7 +156,7 @@ A "pré-produção" exibe os seguintes sites: * [Yavsc](https://yavsc.pschneider.fr) * [Lua](https://lua.pschneider.fr) - +

    @Model

    diff --git a/src/Yavsc.Org/Views/Home/AboutAccess.cshtml b/src/Yavsc.Org/Views/Home/AboutAccess.cshtml index 05331032b..6aaec7ff9 100644 --- a/src/Yavsc.Org/Views/Home/AboutAccess.cshtml +++ b/src/Yavsc.Org/Views/Home/AboutAccess.cshtml @@ -3,13 +3,13 @@ }

    @ViewBag.Title

    - + # Blog et fichiers utilisateurs Par défaut, un envoi à destination du serveur est privé, visible seulement pour son possesseur (et l'administration). -L'utilisateur peu publier chacun de ces derniers à l'adresse : +L'utilisateur peu publier chacun de ces derniers à l'adresse : * de tout le monde, * ou d'une liste de cercles d'utilisateurs, @@ -17,8 +17,8 @@ L'utilisateur peu publier chacun de ces derniers à l'adresse : Quand il s'agit d'un fichier, son adresse permanente relative à la racine du serveur web est: `/UserFiles/(Nom Utilisateur)/(cheminVersLeFichier)` -Les restrictions d'accès à ce fichier sont celles associées à tous les fichiers du même dossier. -Il n'y a pas de regle assiciée à chaque fichier individuellement, +Les restrictions d'accès à ce fichier sont celles associées à tous les fichiers du même dossier. +Il n'y a pas de regle assiciée à chaque fichier individuellement, elles appartiennent au dossier qui les contient. Les billet de blog sont regroupés par utilisateur, à l'Url @@ -35,6 +35,6 @@ Et ils possèdent une addresse permanente de la forme : - + diff --git a/src/Yavsc.Org/Views/Home/AboutMarkdown.cshtml b/src/Yavsc.Org/Views/Home/AboutMarkdown.cshtml index f025c111e..f39aaf536 100644 --- a/src/Yavsc.Org/Views/Home/AboutMarkdown.cshtml +++ b/src/Yavsc.Org/Views/Home/AboutMarkdown.cshtml @@ -3,7 +3,7 @@ }

    @ViewBag.Title

    - + Quelques extensions à un Markdown de base : * les video et audio: `![video: Ep. 28 - La France en commun - Une invitation à écrire un nouveau projet d'émancipation humaine](/images/testVideo.mp4)` @@ -16,4 +16,4 @@ Quelques extensions à un Markdown de base : * le tag "Titre d'article": "#1_great_title" * le tag "User" = "@@(John Doe)" ou "@@jdoe" - + diff --git a/src/Yavsc.Org/Views/Home/CGV.cshtml b/src/Yavsc.Org/Views/Home/CGV.cshtml index 1428ee674..08d01cf63 100644 --- a/src/Yavsc.Org/Views/Home/CGV.cshtml +++ b/src/Yavsc.Org/Views/Home/CGV.cshtml @@ -1,13 +1,13 @@ -@{ +@{ ViewBag.Title = "Conditions Générales de Vente"; }

    @ViewBag.Title

    - + Le paiement intervient à la commande, quelque soit la date d'intervention. -Vous pouvez annuler votre commande depuis la [liste de vos commande en cours](/HairCutCommand/), - -Vous pouvez demander le remboursement d'un paiement, dans le cadre d'une réclamation sur l'execution +Vous pouvez annuler votre commande depuis la [liste de vos commande en cours](/HairCutCommand/), + +Vous pouvez demander le remboursement d'un paiement, dans le cadre d'une réclamation sur l'execution de votre commande, en nous contactant via courrier éléctronique ou postal, [que vour retrouverez sur la page de contact](/Home/Contact), votre demande sera rapidement traitée. - + diff --git a/src/Yavsc.Org/Views/Home/Index.cshtml b/src/Yavsc.Org/Views/Home/Index.cshtml index f867b2bfc..a18b0727b 100755 --- a/src/Yavsc.Org/Views/Home/Index.cshtml +++ b/src/Yavsc.Org/Views/Home/Index.cshtml @@ -2,8 +2,6 @@ @{ ViewBag.Title = @Localizer["Page d'accueil"]; - int i=0; - bool multipleact = Model.Count()>1; } diff --git a/src/Yavsc.Org/Views/Home/Privacy.cshtml b/src/Yavsc.Org/Views/Home/Privacy.cshtml index 34ae18037..a9a6e7cf4 100644 --- a/src/Yavsc.Org/Views/Home/Privacy.cshtml +++ b/src/Yavsc.Org/Views/Home/Privacy.cshtml @@ -2,15 +2,15 @@ ViewBag.Title = Localizer["Home"]; } - -= La confidentialité -:hide-uri-scheme: + +# La confidentialité -xref:Contact[Get Report] +À aucun moment, aucune adresse postale, +aucune adresse e-mail ni aucun numéro de téléphone +ne sont transmis à personne. Seul le système + et son link:Contact[possesseur] ont accès + à ces informations. -À aucun moment, aucune adresse postale, aucune adresse e-mail ni aucun numéro de téléphone -ne sont transmis à personne. Seul le système et son link:Contact[possesseur] ont accès à ces informations. - -De plus, le droit de retrait est permanent et sa mise en oeuvre link:/Account/Delete[immédiate]. - - \ No newline at end of file +De plus, le droit de retrait est permanent +et sa [mise en oeuvre est immédiate](/Account/Delete). + diff --git a/src/Yavsc.Org/Views/Home/Todo.cshtml b/src/Yavsc.Org/Views/Home/Todo.cshtml index 448251a48..c63500479 100755 --- a/src/Yavsc.Org/Views/Home/Todo.cshtml +++ b/src/Yavsc.Org/Views/Home/Todo.cshtml @@ -4,14 +4,14 @@

    @ViewBag.Title

    Linkmuse(trox) - + Les tags. La librairie, les lives. - + @{ ViewBag.Title = @Localizer["TODO"]; }

    @ViewBag.Title

    Faster, stronger, shorter - - + + diff --git a/src/Yavsc.Org/Views/Shared/Components/Bill/Bill_pdf.cshtml b/src/Yavsc.Org/Views/Shared/Components/Bill/Bill_pdf.cshtml deleted file mode 100644 index f49fad802..000000000 --- a/src/Yavsc.Org/Views/Shared/Components/Bill/Bill_pdf.cshtml +++ /dev/null @@ -1,21 +0,0 @@ -@using System.Reflection -@using System.IO -@using Microsoft.Extensions.WebEncoders -@using System.Diagnostics -@using System.Text -@using Yavsc.Helpers -@model Yavsc.ViewModels.Gen.PdfGenerationViewModel - -@{ - ViewBag.Settings.GenerateEstimatePdf(Model); -} -@if (Model.Generated) { -
    - @(Model.BaseFileName).pdf was generated -
    -} else { -
    - @Model.GenerationErrorMessage -

    Something went wrong ...

    -
    -} diff --git a/src/Yavsc.Org/Views/Shared/Components/Bill/Default.cshtml b/src/Yavsc.Org/Views/Shared/Components/Bill/Default.cshtml deleted file mode 100644 index f2d0e0163..000000000 --- a/src/Yavsc.Org/Views/Shared/Components/Bill/Default.cshtml +++ /dev/null @@ -1,33 +0,0 @@ - -@model IBillable -
    -@if (ViewBag.BillFileInfo.Exists) { -La facture au format Pdf -} else { - -
    -
    - - -
    -
    -
    -
    -} -
    diff --git a/src/Yavsc.Org/Views/Shared/Components/Comment/Default.cshtml b/src/Yavsc.Org/Views/Shared/Components/Comment/Default.cshtml index 6dfec029c..e83ea14c6 100644 --- a/src/Yavsc.Org/Views/Shared/Components/Comment/Default.cshtml +++ b/src/Yavsc.Org/Views/Shared/Components/Comment/Default.cshtml @@ -1,16 +1,30 @@ @model Comment -
    - @Model.Article - - @if (Model.Children!=null) { -
    - @foreach (var comment in Model.Children) - { - @await this.Component.InvokeAsync("Comment", new { id = comment.Id}); - } +
    +
    + @Model.Article
    -} -
    - + + — @(Model.Author?.UserName ?? "Anonyme"), + posté le @Model.DateCreated.ToString("dddd d MMM yyyy à H:mm") + + @if (User.GetUserId() != null) + { + + } +
    + @if (Model.Children != null) + { + foreach (var comment in Model.Children) + { + @await this.Component.InvokeAsync("Comment", new { id = comment.Id }) + } + } +
    +
    \ No newline at end of file diff --git a/src/Yavsc.Org/Views/Shared/Components/Estimate/Default.cshtml b/src/Yavsc.Org/Views/Shared/Components/Estimate/Default.cshtml deleted file mode 100644 index b9a1ab26d..000000000 --- a/src/Yavsc.Org/Views/Shared/Components/Estimate/Default.cshtml +++ /dev/null @@ -1,13 +0,0 @@ - -@model Estimate - diff --git a/src/Yavsc.Org/Views/Shared/Components/Estimate/Estimate_pdf.cshtml b/src/Yavsc.Org/Views/Shared/Components/Estimate/Estimate_pdf.cshtml deleted file mode 100644 index bef19b35a..000000000 --- a/src/Yavsc.Org/Views/Shared/Components/Estimate/Estimate_pdf.cshtml +++ /dev/null @@ -1,21 +0,0 @@ -@using System.Reflection -@using System.IO -@using Microsoft.Extensions.WebEncoders -@using System.Diagnostics -@using System.Text -@using Yavsc.Helpers -@model Yavsc.ViewModels.Gen.PdfGenerationViewModel - -@{ - SiteSettings.Value.GenerateEstimatePdf(Model); -} -@if (Model.Generated) { -
    - @(Model.BaseFileName).pdf was generated -
    -} else { -
    - @Model.GenerationErrorMessage -

    Something went wrong ...

    -
    -} diff --git a/src/Yavsc.Org/Views/Shared/_Layout.cshtml b/src/Yavsc.Org/Views/Shared/_Layout.cshtml index 9515ac2b4..653ffae0a 100644 --- a/src/Yavsc.Org/Views/Shared/_Layout.cshtml +++ b/src/Yavsc.Org/Views/Shared/_Layout.cshtml @@ -31,7 +31,7 @@

    @((n.click_action == null) ? Localizer["Fermer"] : Localizer[n.click_action]) - @n.body + @n.body
    } } diff --git a/src/Yavsc.Org/Views/Shared/_Nav.cshtml b/src/Yavsc.Org/Views/Shared/_Nav.cshtml index 385884fc0..4af820fc8 100644 --- a/src/Yavsc.Org/Views/Shared/_Nav.cshtml +++ b/src/Yavsc.Org/Views/Shared/_Nav.cshtml @@ -2,7 +2,7 @@ style="background-image: url('@SiteSettings.Value.Banner'); background-position: 0%; background-attachment: fixed;"> - + diff --git a/src/Yavsc.Org/Yavsc.Org.csproj b/src/Yavsc.Org/Yavsc.Org.csproj index 88fb3c7be..01c38378c 100644 --- a/src/Yavsc.Org/Yavsc.Org.csproj +++ b/src/Yavsc.Org/Yavsc.Org.csproj @@ -9,8 +9,8 @@ https://github.com/pazof/yavsc 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 @@ -39,8 +39,6 @@ - - diff --git a/src/Yavsc.Org/appsettings-org-template.json b/src/Yavsc.Org/appsettings-org-template.json deleted file mode 100644 index 85593635f..000000000 --- a/src/Yavsc.Org/appsettings-org-template.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Microsoft.AspNetCore.SignalR": "Debug", - "Microsoft.AspNetCore.Http.Connections": "Debug" - } - }, - "AllowedHosts": "*", - "ConnectionStrings": { - "YavscConnection": "*** via dotnet user-secrets ou variable d'environnement *** Server=[YOURSERVERNAME];Port=5432;Database=[YOURDBNAME];Username=[YOURDBUSERNAME];Password=[YOURDBPASSW];" - }, - "Site": { - "Title": "yavsc", - "Slogan": "Yavsc!", - "StyleSheet": "/css/default.css", - "Authority": "https://[Your domaine name]", - "ExternalUrl": "https://[Your domaine name]", - "CorsAllowedOrigins": [ - "https://your.api.url.com/api/v1", - "https://localhost:5003/api/v1", - "https://localhost:5005/api/v1" - ], - "Audience": [ "api", "blogs"], - "Banner": "/images/banner-dewang.jpg", - "Owner": { - "Name": "[Site owner's name]", - "EMail": "[Site owner's e-mail address]" - }, - "Admin": { - "Name": "[a name for a site admin]", - "EMail": "[an e-mail for a site admin]" - }, - "Avatars": "avatars", - "Quota": 200000000, - "Bills": "bills", - "Blog": "blog", - "TempDir": "temp", - "DataDir": "data" - }, - "Smtp": { - "Host": "[YOURSMTPHOST]", - "Port": 465, - "Username": "[YOURSMTPUSERNAME]", - "Password": "[YOURSMTPPASSWORD]" - }, - "DataProtection": { - "Keys": { - "Dir": "DataProtection" - }, - "RSAParamFile": "RSA-Params.json", - "ExpiresInHours": 168 - }, - "Authentication": { - "PayPal": { - "Mode": "[sandbox|production]", - "ClientId": "[Your PayPal REST API ClientId]", - "Secret": "[Your PayPal REST API Secret]" - }, - "Google": { - "ApiKey": "[Your ApiKey]", - "ClientId": "[Your ClientId]", - "ClientSecret": "[Your ClientSecret]", - "ServiceAccount": { - "project_id": "[Yours]", - "private_key_id": "[Yours]", - "private_key": "-----[Yours]---\n", - "client_email": "[Yours]@appspot.gserviceaccount.com", - "client_id": "[Yours]", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://accounts.google.com/o/oauth2/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www41.googleapis.com/robot/v1/[Yours].com" - } - }, - "Facebook": { - "ClientId": "[Your ClientId]", - "ClientSecret": "[Your ClientSecret]" - }, - "Twitter": { - "ClientId": "[Your ClientId]", - "ClientSecret": "[Your ClientSecret]" - } - }, - "Moderation": { - "AutoApproveThreshold": 0.7, - "AutoRejectThreshold": 0.9 - }, - "Kestrel": { - "Endpoints": { - "Http": { - "Url": "http://localhost:82" - }, - "Https": { - "Url": "https://localhost:83", - "Certificate": { - "Path": "/etc/letsencrypt/live/[Your domaine name]/cert.pem", - "KeyPath": "/etc/letsencrypt/live/[Your domaine name]/privkey.pem" - } - } - } - } -} diff --git a/src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.css b/src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.css index 84fb5efe4..466c6e28b 100644 --- a/src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.css +++ b/src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.css @@ -3924,7 +3924,7 @@ fieldset:disabled .btn { --bs-dropdown-box-shadow: var(--bs-box-shadow); --bs-dropdown-link-color: var(--bs-body-color); --bs-dropdown-link-hover-color: #fff; - --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.4); + --bs-dropdown-link-hover-bg: rgba(0, 0, 0, 0.4); --bs-dropdown-link-active-color: #fff; --bs-dropdown-link-active-bg: #e83283; --bs-dropdown-link-disabled-color: var(--bs-tertiary-color); @@ -5033,7 +5033,8 @@ fieldset:disabled .btn { background-color: var(--bs-card-bg); background-clip: border-box; border: var(--bs-card-border-width) solid var(--bs-card-border-color); - border-radius: var(--bs-card-border-radius) + border-radius: var(--bs-card-border-radius); + padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x); } .card>hr { diff --git a/src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.min.css b/src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.min.css index 390637ef1..32df6b5e2 100644 --- a/src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.min.css +++ b/src/Yavsc.Org/wwwroot/lib/bootstrap.quartz.min.css @@ -4,4 +4,4 @@ * Licensed under MIT * Based on Bootstrap*//*!* Bootstrap v5.3.8 (https://getbootstrap.com/) * Copyright 2011-2025 The Bootstrap Authors -* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)*/:root,[data-bs-theme=light]{--bs-blue:#3a8fd9;--bs-indigo:#6610f2;--bs-purple:#686dc3;--bs-pink:#e83283;--bs-red:#fc346f;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#41d7a7;--bs-teal:#528fb3;--bs-cyan:#39cbfb;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9e9e8;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#e83283;--bs-secondary:rgba(255, 255, 255, 0.4);--bs-success:#41d7a7;--bs-info:#39cbfb;--bs-warning:#ffc107;--bs-danger:#fd7e14;--bs-light:#e9e9e8;--bs-dark:#212529;--bs-primary-rgb:232, 50, 131;--bs-secondary-rgb:255, 255, 255;--bs-success-rgb:65, 215, 167;--bs-info-rgb:57, 203, 251;--bs-warning-rgb:255, 193, 7;--bs-danger-rgb:253, 126, 20;--bs-light-rgb:233, 233, 232;--bs-dark-rgb:33, 37, 41;--bs-primary-text-emphasis:#5d1434;--bs-secondary-text-emphasis:rgba(36, 36, 36, 0.76);--bs-success-text-emphasis:#1a5643;--bs-info-text-emphasis:#175164;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#653208;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#fad6e6;--bs-secondary-bg-subtle:rgba(255, 255, 255, 0.88);--bs-success-bg-subtle:#d9f7ed;--bs-info-bg-subtle:#d7f5fe;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#ffe5d0;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#f6adcd;--bs-secondary-border-subtle:rgba(255, 255, 255, 0.76);--bs-success-border-subtle:#b3efdc;--bs-info-border-subtle:#b0eafd;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#fecba1;--bs-light-border-subtle:#e9e9e8;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255, 255, 255;--bs-black-rgb:0, 0, 0;--bs-font-sans-serif:system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#fff;--bs-body-color-rgb:255, 255, 255;--bs-body-bg:#686dc3;--bs-body-bg-rgb:104, 109, 195;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0, 0, 0;--bs-secondary-color:rgba(255, 255, 255, 0.75);--bs-secondary-color-rgb:255, 255, 255;--bs-secondary-bg:#e9e9e8;--bs-secondary-bg-rgb:233, 233, 232;--bs-tertiary-color:rgba(255, 255, 255, 0.5);--bs-tertiary-color-rgb:255, 255, 255;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248, 249, 250;--bs-heading-color:inherit;--bs-link-color:#fff;--bs-link-color-rgb:255, 255, 255;--bs-link-decoration:underline;--bs-link-hover-color:#cccccc;--bs-link-hover-color-rgb:204, 204, 204;--bs-code-color:#e83283;--bs-highlight-color:#fff;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:rgba(255, 255, 255, 0.2);--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.5rem;--bs-border-radius-sm:0.6rem;--bs-border-radius-lg:0.7rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:1px 3px 24px -1px rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 1px 1px rgba(0, 0, 0, 0.1);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(232, 50, 131, 0.25);--bs-form-valid-color:#41d7a7;--bs-form-valid-border-color:#41d7a7;--bs-form-invalid-color:#fd7e14;--bs-form-invalid-border-color:#fd7e14}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222, 226, 230;--bs-body-bg:#212529;--bs-body-bg-rgb:33, 37, 41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255, 255, 255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222, 226, 230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52, 58, 64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222, 226, 230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43, 48, 53;--bs-primary-text-emphasis:#f184b5;--bs-secondary-text-emphasis:rgba(255, 255, 255, 0.64);--bs-success-text-emphasis:#8de7ca;--bs-info-text-emphasis:#88e0fd;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#feb272;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#2e0a1a;--bs-secondary-bg-subtle:rgba(15, 15, 15, 0.88);--bs-success-bg-subtle:#0d2b21;--bs-info-bg-subtle:#0b2932;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#331904;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#8b1e4f;--bs-secondary-border-subtle:rgba(70, 70, 70, 0.64);--bs-success-border-subtle:#278164;--bs-info-border-subtle:#227a97;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#984c0c;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#f184b5;--bs-link-hover-color:#f49dc4;--bs-link-color-rgb:241, 132, 181;--bs-link-hover-color-rgb:244, 157, 196;--bs-code-color:#f184b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#8de7ca;--bs-form-valid-border-color:#8de7ca;--bs-form-invalid-color:#fd85a9;--bs-form-invalid-border-color:#fd85a9}*,::after,::before{box-sizing:border-box}@media(prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:2rem 0;color:inherit;border:0;border-top:var(--bs-border-width)solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:1rem;font-weight:700;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media(min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media(min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media(min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media(min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.6rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;line-height:inherit;font-size:calc(1.275rem + .3vw)}@media(min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-weight:300;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media(min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:300;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media(min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:300;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media(min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:300;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media(min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:300;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media(min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:300;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@media(min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:2rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-2rem;margin-bottom:2rem;font-size:.875em;color:rgba(255,255,255,.7)}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width)solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:1rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media(min-width:576px){.container,.container-sm{max-width:540px}}@media(min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media(min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media(min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media(min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0}.row-cols-auto>*{flex:none;width:auto}.row-cols-1>*{flex:none;width:100%}.row-cols-2>*{flex:none;width:50%}.row-cols-3>*{flex:none;width:33.33333333%}.row-cols-4>*{flex:none;width:25%}.row-cols-5>*{flex:none;width:20%}.row-cols-6>*{flex:none;width:16.66666667%}.col-auto{flex:none;width:auto}.col-1{flex:none;width:8.33333333%}.col-2{flex:none;width:16.66666667%}.col-3{flex:none;width:25%}.col-4{flex:none;width:33.33333333%}.col-5{flex:none;width:41.66666667%}.col-6{flex:none;width:50%}.col-7{flex:none;width:58.33333333%}.col-8{flex:none;width:66.66666667%}.col-9{flex:none;width:75%}.col-10{flex:none;width:83.33333333%}.col-11{flex:none;width:91.66666667%}.col-12{flex:none;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.5rem}.g-1,.gy-1{--bs-gutter-y:0.5rem}.g-2,.gx-2{--bs-gutter-x:1rem}.g-2,.gy-2{--bs-gutter-y:1rem}.g-3,.gx-3{--bs-gutter-x:2rem}.g-3,.gy-3{--bs-gutter-y:2rem}.g-4,.gx-4{--bs-gutter-x:3rem}.g-4,.gy-4{--bs-gutter-y:3rem}.g-5,.gx-5{--bs-gutter-x:6rem}.g-5,.gy-5{--bs-gutter-y:6rem}@media(min-width:576px){.col-sm{flex:1 0}.row-cols-sm-auto>*{flex:none;width:auto}.row-cols-sm-1>*{flex:none;width:100%}.row-cols-sm-2>*{flex:none;width:50%}.row-cols-sm-3>*{flex:none;width:33.33333333%}.row-cols-sm-4>*{flex:none;width:25%}.row-cols-sm-5>*{flex:none;width:20%}.row-cols-sm-6>*{flex:none;width:16.66666667%}.col-sm-auto{flex:none;width:auto}.col-sm-1{flex:none;width:8.33333333%}.col-sm-2{flex:none;width:16.66666667%}.col-sm-3{flex:none;width:25%}.col-sm-4{flex:none;width:33.33333333%}.col-sm-5{flex:none;width:41.66666667%}.col-sm-6{flex:none;width:50%}.col-sm-7{flex:none;width:58.33333333%}.col-sm-8{flex:none;width:66.66666667%}.col-sm-9{flex:none;width:75%}.col-sm-10{flex:none;width:83.33333333%}.col-sm-11{flex:none;width:91.66666667%}.col-sm-12{flex:none;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.5rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.5rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:1rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:1rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:2rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:2rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:3rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:3rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:6rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:6rem}}@media(min-width:768px){.col-md{flex:1 0}.row-cols-md-auto>*{flex:none;width:auto}.row-cols-md-1>*{flex:none;width:100%}.row-cols-md-2>*{flex:none;width:50%}.row-cols-md-3>*{flex:none;width:33.33333333%}.row-cols-md-4>*{flex:none;width:25%}.row-cols-md-5>*{flex:none;width:20%}.row-cols-md-6>*{flex:none;width:16.66666667%}.col-md-auto{flex:none;width:auto}.col-md-1{flex:none;width:8.33333333%}.col-md-2{flex:none;width:16.66666667%}.col-md-3{flex:none;width:25%}.col-md-4{flex:none;width:33.33333333%}.col-md-5{flex:none;width:41.66666667%}.col-md-6{flex:none;width:50%}.col-md-7{flex:none;width:58.33333333%}.col-md-8{flex:none;width:66.66666667%}.col-md-9{flex:none;width:75%}.col-md-10{flex:none;width:83.33333333%}.col-md-11{flex:none;width:91.66666667%}.col-md-12{flex:none;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.5rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.5rem}.g-md-2,.gx-md-2{--bs-gutter-x:1rem}.g-md-2,.gy-md-2{--bs-gutter-y:1rem}.g-md-3,.gx-md-3{--bs-gutter-x:2rem}.g-md-3,.gy-md-3{--bs-gutter-y:2rem}.g-md-4,.gx-md-4{--bs-gutter-x:3rem}.g-md-4,.gy-md-4{--bs-gutter-y:3rem}.g-md-5,.gx-md-5{--bs-gutter-x:6rem}.g-md-5,.gy-md-5{--bs-gutter-y:6rem}}@media(min-width:992px){.col-lg{flex:1 0}.row-cols-lg-auto>*{flex:none;width:auto}.row-cols-lg-1>*{flex:none;width:100%}.row-cols-lg-2>*{flex:none;width:50%}.row-cols-lg-3>*{flex:none;width:33.33333333%}.row-cols-lg-4>*{flex:none;width:25%}.row-cols-lg-5>*{flex:none;width:20%}.row-cols-lg-6>*{flex:none;width:16.66666667%}.col-lg-auto{flex:none;width:auto}.col-lg-1{flex:none;width:8.33333333%}.col-lg-2{flex:none;width:16.66666667%}.col-lg-3{flex:none;width:25%}.col-lg-4{flex:none;width:33.33333333%}.col-lg-5{flex:none;width:41.66666667%}.col-lg-6{flex:none;width:50%}.col-lg-7{flex:none;width:58.33333333%}.col-lg-8{flex:none;width:66.66666667%}.col-lg-9{flex:none;width:75%}.col-lg-10{flex:none;width:83.33333333%}.col-lg-11{flex:none;width:91.66666667%}.col-lg-12{flex:none;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.5rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.5rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:1rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:1rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:2rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:2rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:3rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:3rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:6rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:6rem}}@media(min-width:1200px){.col-xl{flex:1 0}.row-cols-xl-auto>*{flex:none;width:auto}.row-cols-xl-1>*{flex:none;width:100%}.row-cols-xl-2>*{flex:none;width:50%}.row-cols-xl-3>*{flex:none;width:33.33333333%}.row-cols-xl-4>*{flex:none;width:25%}.row-cols-xl-5>*{flex:none;width:20%}.row-cols-xl-6>*{flex:none;width:16.66666667%}.col-xl-auto{flex:none;width:auto}.col-xl-1{flex:none;width:8.33333333%}.col-xl-2{flex:none;width:16.66666667%}.col-xl-3{flex:none;width:25%}.col-xl-4{flex:none;width:33.33333333%}.col-xl-5{flex:none;width:41.66666667%}.col-xl-6{flex:none;width:50%}.col-xl-7{flex:none;width:58.33333333%}.col-xl-8{flex:none;width:66.66666667%}.col-xl-9{flex:none;width:75%}.col-xl-10{flex:none;width:83.33333333%}.col-xl-11{flex:none;width:91.66666667%}.col-xl-12{flex:none;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.5rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.5rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:1rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:1rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:2rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:2rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:3rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:3rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:6rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:6rem}}@media(min-width:1400px){.col-xxl{flex:1 0}.row-cols-xxl-auto>*{flex:none;width:auto}.row-cols-xxl-1>*{flex:none;width:100%}.row-cols-xxl-2>*{flex:none;width:50%}.row-cols-xxl-3>*{flex:none;width:33.33333333%}.row-cols-xxl-4>*{flex:none;width:25%}.row-cols-xxl-5>*{flex:none;width:20%}.row-cols-xxl-6>*{flex:none;width:16.66666667%}.col-xxl-auto{flex:none;width:auto}.col-xxl-1{flex:none;width:8.33333333%}.col-xxl-2{flex:none;width:16.66666667%}.col-xxl-3{flex:none;width:25%}.col-xxl-4{flex:none;width:33.33333333%}.col-xxl-5{flex:none;width:41.66666667%}.col-xxl-6{flex:none;width:50%}.col-xxl-7{flex:none;width:58.33333333%}.col-xxl-8{flex:none;width:66.66666667%}.col-xxl-9{flex:none;width:75%}.col-xxl-10{flex:none;width:83.33333333%}.col-xxl-11{flex:none;width:91.66666667%}.col-xxl-12{flex:none;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.5rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.5rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:1rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:1rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:2rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:2rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:3rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:3rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:6rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:6rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:initial;--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:initial;--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:initial;--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:initial;--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:2rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2)solid}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width)0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#fff;--bs-table-bg:#e83283;--bs-table-border-color:#ed5b9c;--bs-table-striped-bg:#e93c89;--bs-table-striped-color:#fff;--bs-table-active-bg:#ea478f;--bs-table-active-color:#fff;--bs-table-hover-bg:#ea418c;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#fff;--bs-table-bg:rgba(255, 255, 255, 0.4);--bs-table-border-color:rgba(255, 255, 255, 0.52);--bs-table-striped-bg:rgba(255, 255, 255, 0.43);--bs-table-striped-color:#000;--bs-table-active-bg:rgba(255, 255, 255, 0.46);--bs-table-active-color:#000;--bs-table-hover-bg:rgba(255, 255, 255, 0.445);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#fff;--bs-table-bg:#41d7a7;--bs-table-border-color:#67dfb9;--bs-table-striped-bg:#4bd9ab;--bs-table-striped-color:#fff;--bs-table-active-bg:#54dbb0;--bs-table-active-color:#fff;--bs-table-hover-bg:#4fdaae;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#fff;--bs-table-bg:#39cbfb;--bs-table-border-color:#61d5fc;--bs-table-striped-bg:#43cefb;--bs-table-striped-color:#fff;--bs-table-active-bg:#4dd0fb;--bs-table-active-color:#fff;--bs-table-hover-bg:#48cffb;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#fff;--bs-table-bg:#ffc107;--bs-table-border-color:#ffcd39;--bs-table-striped-bg:#ffc413;--bs-table-striped-color:#fff;--bs-table-active-bg:#ffc720;--bs-table-active-color:#fff;--bs-table-hover-bg:#ffc61a;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#fff;--bs-table-bg:#fd7e14;--bs-table-border-color:#fd9843;--bs-table-striped-bg:#fd8420;--bs-table-striped-color:#fff;--bs-table-active-bg:#fd8b2c;--bs-table-active-color:#fff;--bs-table-hover-bg:#fd8826;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#e9e9e8;--bs-table-border-color:#bababa;--bs-table-striped-bg:#dddddc;--bs-table-striped-color:#000;--bs-table-active-bg:#d2d2d1;--bs-table-active-color:#fff;--bs-table-hover-bg:#d8d8d7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.75rem + 1px);padding-bottom:calc(.75rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.75rem 1.5rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:initial;background-clip:padding-box;border:1px solid rgba(255,255,255,.4);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:initial;border-color:rgba(255,255,255,.4);outline:0;box-shadow:none}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:rgba(255,255,255,.7);opacity:1}.form-control::placeholder{color:rgba(255,255,255,.7);opacity:1}.form-control:disabled{background-color:rgba(255,255,255,.1);opacity:1}.form-control::-webkit-file-upload-button{padding:.75rem 1.5rem;margin:-.75rem -1.5rem;-webkit-margin-end:1.5rem;margin-inline-end:1.5rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.75rem 1.5rem;margin:-.75rem -1.5rem;-webkit-margin-end:1.5rem;margin-inline-end:1.5rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.75rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:initial;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(1px * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + 1.5rem + calc(1px * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(1px * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2))}.form-control-color{width:3rem;height:calc(1.5em + 1.5rem + calc(1px * 2));padding:.75rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(1px * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(1px * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.75rem 4.5rem .75rem 1.5rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:initial;background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right 1.5rem center;background-size:16px 12px;border:1px solid rgba(255,255,255,.4);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:rgba(255,255,255,.4);outline:0;box-shadow:none}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:1.5rem;background-image:none}.form-select:disabled{color:rgba(255,255,255,.7);background-color:rgba(255,255,255,.1)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:rgba(255, 255, 255, 0.3);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:50%;background-size:contain;border:1px solid rgba(255,255,255,.2);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:rgba(255,255,255,.4);outline:0;box-shadow:0 0 0 .25rem rgba(232,50,131,.25)}.form-check-input:checked{background-color:#e83283;border-color:#e83283}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#e83283;border-color:#e83283;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:0;border-radius:2em;transition:background-position .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:100%;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:initial}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #686dc3,none}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #686dc3,none}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#e83283;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#f8c2da}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:rgba(0,0,0,.2);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#e83283;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#f8c2da}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:rgba(0,0,0,.2);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(1px * 2));min-height:calc(3.5rem + calc(1px * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem 1.5rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media(prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem 1.5rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:1.5rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{transform:scale(.85)translateY(-.5rem)translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85)translateY(-.5rem)translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85)translateY(-.5rem)translateX(.15rem)}.form-floating>textarea:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem .75rem;z-index:-1;height:1.5em;content:"";background-color:initial;border-radius:var(--bs-border-radius)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem .75rem;z-index:-1;height:1.5em;content:"";background-color:initial;border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:rgba(255,255,255,.1)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.75rem 1.5rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:initial;border:1px solid rgba(255,255,255,.4);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:6rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1 * 1px);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem 1rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + 1.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2341d7a7' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .375rem)center;background-size:calc(.75em + .75rem)calc(.75em + .75rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 1.5rem);background-position:top calc(.375em + .375rem)right calc(.375em + .375rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2341d7a7' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");padding-right:8.25rem;background-position:right 1.5rem center,center right 4.5rem;background-size:16px 12px,calc(.75em + .75rem)calc(.75em + .75rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + 1.5rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem 1rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + 1.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23fd7e14'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23fd7e14' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .375rem)center;background-size:calc(.75em + .75rem)calc(.75em + .75rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 1.5rem);background-position:top calc(.375em + .375rem)right calc(.375em + .375rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23fd7e14'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23fd7e14' stroke='none'/%3e%3c/svg%3e");padding-right:8.25rem;background-position:right 1.5rem center,center right 4.5rem;background-size:16px 12px,calc(.75em + .75rem)calc(.75em + .75rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + 1.5rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:1.5rem;--bs-btn-padding-y:0.75rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:1px 3px 24px -1px rgba(0, 0, 0, 0.15);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y)var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width)solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#e83283;--bs-btn-border-color:#e83283;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#c52b6f;--bs-btn-hover-border-color:#ba2869;--bs-btn-focus-shadow-rgb:235, 81, 150;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ba2869;--bs-btn-active-border-color:#ae2662;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#e83283;--bs-btn-disabled-border-color:#e83283}.btn-secondary{--bs-btn-color:#000;--bs-btn-bg:rgba(255, 255, 255, 0.4);--bs-btn-border-color:rgba(255, 255, 255, 0.4);--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgba(255, 255, 255, 0.49);--bs-btn-hover-border-color:rgba(255, 255, 255, 0.46);--bs-btn-focus-shadow-rgb:149, 149, 149;--bs-btn-active-color:#000;--bs-btn-active-bg:rgba(255, 255, 255, 0.52);--bs-btn-active-border-color:rgba(255, 255, 255, 0.46);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:rgba(255, 255, 255, 0.4);--bs-btn-disabled-border-color:rgba(255, 255, 255, 0.4)}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#41d7a7;--bs-btn-border-color:#41d7a7;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#37b78e;--bs-btn-hover-border-color:#34ac86;--bs-btn-focus-shadow-rgb:94, 221, 180;--bs-btn-active-color:#fff;--bs-btn-active-bg:#34ac86;--bs-btn-active-border-color:#31a17d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#41d7a7;--bs-btn-disabled-border-color:#41d7a7}.btn-info{--bs-btn-color:#fff;--bs-btn-bg:#39cbfb;--bs-btn-border-color:#39cbfb;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#30add5;--bs-btn-hover-border-color:#2ea2c9;--bs-btn-focus-shadow-rgb:87, 211, 252;--bs-btn-active-color:#fff;--bs-btn-active-bg:#2ea2c9;--bs-btn-active-border-color:#2b98bc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#39cbfb;--bs-btn-disabled-border-color:#39cbfb}.btn-warning{--bs-btn-color:#fff;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#d9a406;--bs-btn-hover-border-color:#cc9a06;--bs-btn-focus-shadow-rgb:255, 202, 44;--bs-btn-active-color:#fff;--bs-btn-active-bg:#cc9a06;--bs-btn-active-border-color:#bf9105;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#fd7e14;--bs-btn-border-color:#fd7e14;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#d76b11;--bs-btn-hover-border-color:#ca6510;--bs-btn-focus-shadow-rgb:253, 145, 55;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ca6510;--bs-btn-active-border-color:#be5f0f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#fd7e14;--bs-btn-disabled-border-color:#fd7e14}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#e9e9e8;--bs-btn-border-color:#e9e9e8;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#c6c6c5;--bs-btn-hover-border-color:#bababa;--bs-btn-focus-shadow-rgb:198, 198, 197;--bs-btn-active-color:#fff;--bs-btn-active-bg:#bababa;--bs-btn-active-border-color:#afafae;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#e9e9e8;--bs-btn-disabled-border-color:#e9e9e8}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66, 70, 73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#e83283;--bs-btn-border-color:#e83283;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#e83283;--bs-btn-hover-border-color:#e83283;--bs-btn-focus-shadow-rgb:232, 50, 131;--bs-btn-active-color:#fff;--bs-btn-active-bg:#e83283;--bs-btn-active-border-color:#e83283;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#e83283;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#e83283;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:rgba(255, 255, 255, 0.4);--bs-btn-border-color:rgba(255, 255, 255, 0.4);--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgba(255, 255, 255, 0.4);--bs-btn-hover-border-color:rgba(255, 255, 255, 0.4);--bs-btn-focus-shadow-rgb:255, 255, 255;--bs-btn-active-color:#000;--bs-btn-active-bg:rgba(255, 255, 255, 0.4);--bs-btn-active-border-color:rgba(255, 255, 255, 0.4);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:rgba(255, 255, 255, 0.4);--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:rgba(255, 255, 255, 0.4);--bs-gradient:none}.btn-outline-success{--bs-btn-color:#41d7a7;--bs-btn-border-color:#41d7a7;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#41d7a7;--bs-btn-hover-border-color:#41d7a7;--bs-btn-focus-shadow-rgb:65, 215, 167;--bs-btn-active-color:#fff;--bs-btn-active-bg:#41d7a7;--bs-btn-active-border-color:#41d7a7;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#41d7a7;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#41d7a7;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#39cbfb;--bs-btn-border-color:#39cbfb;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#39cbfb;--bs-btn-hover-border-color:#39cbfb;--bs-btn-focus-shadow-rgb:57, 203, 251;--bs-btn-active-color:#fff;--bs-btn-active-bg:#39cbfb;--bs-btn-active-border-color:#39cbfb;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#39cbfb;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#39cbfb;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255, 193, 7;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#fd7e14;--bs-btn-border-color:#fd7e14;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#fd7e14;--bs-btn-hover-border-color:#fd7e14;--bs-btn-focus-shadow-rgb:253, 126, 20;--bs-btn-active-color:#fff;--bs-btn-active-bg:#fd7e14;--bs-btn-active-border-color:#fd7e14;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fd7e14;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#fd7e14;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#e9e9e8;--bs-btn-border-color:#e9e9e8;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#e9e9e8;--bs-btn-hover-border-color:#e9e9e8;--bs-btn-focus-shadow-rgb:233, 233, 232;--bs-btn-active-color:#000;--bs-btn-active-bg:#e9e9e8;--bs-btn-active-border-color:#e9e9e8;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#e9e9e8;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#e9e9e8;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33, 37, 41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:217, 217, 217;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media(prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media(prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media(prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:rgba(255, 255, 255, 0.2);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:rgba(255, 255, 255, 0.2);--bs-dropdown-divider-margin-y:1rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:#fff;--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.4);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#e83283;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:2rem;--bs-dropdown-item-padding-y:0.5rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:2rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y)var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width)solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y)0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y)var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:initial;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:initial}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y)var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y)var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:rgba(255, 255, 255, 0.2);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:rgba(255, 255, 255, 0.2);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#e83283;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--bs-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(-1 * var(--bs-border-width))}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:rgba(255, 255, 255, 0.7);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y)var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media(prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(232,50,131,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:0;--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:0;--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:#343a40;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width)solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width)solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:50rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#e83283}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:#fff;gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width)solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:initial}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:initial}.nav-fill .nav-item,.nav-fill>.nav-link{flex:auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:1rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y)var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y)var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:initial;border:var(--bs-border-width)solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media(prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:50%;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media(min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1.75rem;--bs-card-spacer-x:2rem;--bs-card-title-spacer-y:1rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:rgba(255, 255, 255, 0.2);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.875rem;--bs-card-cap-padding-x:2rem;--bs-card-cap-bg:transparent;--bs-card-cap-color:#fff;--bs-card-height: ;--bs-card-color:#fff;--bs-card-bg:transparent;--bs-card-img-overlay-padding:2rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width)solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:auto;padding:var(--bs-card-spacer-y)var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y)var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width)solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius)var(--bs-card-inner-border-radius)0 0}.card-footer{padding:var(--bs-card-cap-padding-y)var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width)solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius)var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media(min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child)>.card-header,.card-group>.card:not(:last-child)>.card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-footer,.card-group>.card:not(:last-child)>.card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child)>.card-header,.card-group>.card:not(:first-child)>.card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-footer,.card-group>.card:not(:first-child)>.card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:rgba(255, 255, 255, 0.4);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%235d1434' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(232, 50, 131, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:#fff;--bs-accordion-active-bg:#e83283}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y)var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media(prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width))0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media(prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width)solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y)var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f184b5'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f184b5'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:#fff;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#fff;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y)var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider,"/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:rgba(255, 255, 255, 0.3);--bs-pagination-border-width:0;--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:#fff;--bs-pagination-hover-bg:rgba(255, 255, 255, 0.4);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:#fff;--bs-pagination-focus-bg:rgba(255, 255, 255, 0.4);--bs-pagination-focus-box-shadow:none;--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#e83283;--bs-pagination-active-border-color:#e83283;--bs-pagination-disabled-color:rgba(255, 255, 255, 0.7);--bs-pagination-disabled-bg:rgba(255, 255, 255, 0.3);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y)var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width)solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(-1 * 0)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y)var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:2rem;--bs-alert-padding-y:2rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y)var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:6rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:2.5rem 2rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:rgba(0, 0, 0, 0.2);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#e83283;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media(prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height)var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media(prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:#fff;--bs-list-group-bg:transparent;--bs-list-group-border-color:rgba(255, 255, 255, 0.2);--bs-list-group-border-width:0;--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:2rem;--bs-list-group-item-padding-y:1rem;--bs-list-group-action-color:#fff;--bs-list-group-action-hover-color:#fff;--bs-list-group-action-hover-bg:rgba(255, 255, 255, 0.4);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:rgba(255, 255, 255, 0.7);--bs-list-group-disabled-bg:transparent;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#e83283;--bs-list-group-active-border-color:#e83283;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section,".")". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y)var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width)solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media(min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#fff;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(232, 50, 131, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:1em;height:1em;padding:.25em;color:var(--bs-btn-close-color);background:var(--bs-btn-close-bg)50%/1em no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:.5rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}:root,[data-bs-theme=light]{--bs-btn-close-filter: }[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:#fff;--bs-toast-header-bg:transparent;--bs-toast-header-border-color:rgba(255, 255, 255, 0.2);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width)solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y)var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width)solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:2rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:2rem;--bs-modal-header-padding-y:2rem;--bs-modal-header-padding:2rem 2rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@media(prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width)solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width)solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5)calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width)solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media(min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media(min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media(min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:1rem;--bs-tooltip-padding-y:0.5rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.7;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height)calc(var(--bs-tooltip-arrow-width) * .5)0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5)var(--bs-tooltip-arrow-height)calc(var(--bs-tooltip-arrow-width) * .5)0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5)var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5)0 calc(var(--bs-tooltip-arrow-width) * .5)var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y)var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:2rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:transparent;--bs-popover-body-padding-x:2rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:#fff;--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width)solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height)calc(var(--bs-popover-arrow-width) * .5)0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5)var(--bs-popover-arrow-height)calc(var(--bs-popover-arrow-width) * .5)0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5)var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width)solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5)0 calc(var(--bs-popover-arrow-width) * .5)var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y)var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width)solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y)var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media(prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;filter:var(--bs-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:initial;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:var(--bs-carousel-indicator-active-bg);background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media(prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#fff;--bs-carousel-caption-color:#fff;--bs-carousel-control-icon-filter: }[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;flex-shrink:0;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed)linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width)solid;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media(prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:2rem;--bs-offcanvas-padding-y:2rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media(max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media(max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media(min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}@media(max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media(max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media(min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}@media(max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media(max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media(min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}@media(max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media(max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media(min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}@media(max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media(max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media(min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media(prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y)var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5)calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y)var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#000!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#fff!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#fff!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(186,40,105,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(186,40,105,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(186,40,105,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(255,255,255,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,255,255,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,255,255,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(52,172,134,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(52,172,134,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(52,172,134,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(46,162,201,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(46,162,201,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(46,162,201,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(204,154,6,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(204,154,6,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(204,154,6,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(202,101,16,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(202,101,16,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(202,101,16,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(237,237,237,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(237,237,237,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(237,237,237,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0)var(--bs-focus-ring-y,0)var(--bs-focus-ring-blur,0)var(--bs-focus-ring-width)var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,.5));text-underline-offset:.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media(prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media(min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media(min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media(min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media(min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media(min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.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}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!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}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-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}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-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}.justify-content-evenly{justify-content:space-evenly!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-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-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}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.5rem!important}.m-2{margin:1rem!important}.m-3{margin:2rem!important}.m-4{margin:3rem!important}.m-5{margin:6rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-2{margin-right:1rem!important;margin-left:1rem!important}.mx-3{margin-right:2rem!important;margin-left:2rem!important}.mx-4{margin-right:3rem!important;margin-left:3rem!important}.mx-5{margin-right:6rem!important;margin-left:6rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.5rem!important}.mt-2{margin-top:1rem!important}.mt-3{margin-top:2rem!important}.mt-4{margin-top:3rem!important}.mt-5{margin-top:6rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.5rem!important}.me-2{margin-right:1rem!important}.me-3{margin-right:2rem!important}.me-4{margin-right:3rem!important}.me-5{margin-right:6rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.5rem!important}.mb-2{margin-bottom:1rem!important}.mb-3{margin-bottom:2rem!important}.mb-4{margin-bottom:3rem!important}.mb-5{margin-bottom:6rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.5rem!important}.ms-2{margin-left:1rem!important}.ms-3{margin-left:2rem!important}.ms-4{margin-left:3rem!important}.ms-5{margin-left:6rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.5rem!important}.p-2{padding:1rem!important}.p-3{padding:2rem!important}.p-4{padding:3rem!important}.p-5{padding:6rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-2{padding-right:1rem!important;padding-left:1rem!important}.px-3{padding-right:2rem!important;padding-left:2rem!important}.px-4{padding-right:3rem!important;padding-left:3rem!important}.px-5{padding-right:6rem!important;padding-left:6rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.5rem!important}.pt-2{padding-top:1rem!important}.pt-3{padding-top:2rem!important}.pt-4{padding-top:3rem!important}.pt-5{padding-top:6rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.5rem!important}.pe-2{padding-right:1rem!important}.pe-3{padding-right:2rem!important}.pe-4{padding-right:3rem!important}.pe-5{padding-right:6rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.5rem!important}.pb-2{padding-bottom:1rem!important}.pb-3{padding-bottom:2rem!important}.pb-4{padding-bottom:3rem!important}.pb-5{padding-bottom:6rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.5rem!important}.ps-2{padding-left:1rem!important}.ps-3{padding-left:2rem!important}.ps-4{padding-left:3rem!important}.ps-5{padding-left:6rem!important}.gap-0{gap:0!important}.gap-1{gap:.5rem!important}.gap-2{gap:1rem!important}.gap-3{gap:2rem!important}.gap-4{gap:3rem!important}.gap-5{gap:6rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.5rem!important}.row-gap-2{row-gap:1rem!important}.row-gap-3{row-gap:2rem!important}.row-gap-4{row-gap:3rem!important}.row-gap-5{row-gap:6rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!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-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:.125em!important}.link-offset-1-hover:hover{text-underline-offset:.125em!important}.link-offset-2{text-underline-offset:.25em!important}.link-offset-2-hover:hover{text-underline-offset:.25em!important}.link-offset-3{text-underline-offset:.375em!important}.link-offset-3-hover:hover{text-underline-offset:.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:initial!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media(min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.5rem!important}.m-sm-2{margin:1rem!important}.m-sm-3{margin:2rem!important}.m-sm-4{margin:3rem!important}.m-sm-5{margin:6rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-2{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-3{margin-right:2rem!important;margin-left:2rem!important}.mx-sm-4{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-5{margin-right:6rem!important;margin-left:6rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-sm-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.5rem!important}.mt-sm-2{margin-top:1rem!important}.mt-sm-3{margin-top:2rem!important}.mt-sm-4{margin-top:3rem!important}.mt-sm-5{margin-top:6rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.5rem!important}.me-sm-2{margin-right:1rem!important}.me-sm-3{margin-right:2rem!important}.me-sm-4{margin-right:3rem!important}.me-sm-5{margin-right:6rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.5rem!important}.mb-sm-2{margin-bottom:1rem!important}.mb-sm-3{margin-bottom:2rem!important}.mb-sm-4{margin-bottom:3rem!important}.mb-sm-5{margin-bottom:6rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.5rem!important}.ms-sm-2{margin-left:1rem!important}.ms-sm-3{margin-left:2rem!important}.ms-sm-4{margin-left:3rem!important}.ms-sm-5{margin-left:6rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.5rem!important}.p-sm-2{padding:1rem!important}.p-sm-3{padding:2rem!important}.p-sm-4{padding:3rem!important}.p-sm-5{padding:6rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-2{padding-right:1rem!important;padding-left:1rem!important}.px-sm-3{padding-right:2rem!important;padding-left:2rem!important}.px-sm-4{padding-right:3rem!important;padding-left:3rem!important}.px-sm-5{padding-right:6rem!important;padding-left:6rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-sm-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-sm-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.5rem!important}.pt-sm-2{padding-top:1rem!important}.pt-sm-3{padding-top:2rem!important}.pt-sm-4{padding-top:3rem!important}.pt-sm-5{padding-top:6rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.5rem!important}.pe-sm-2{padding-right:1rem!important}.pe-sm-3{padding-right:2rem!important}.pe-sm-4{padding-right:3rem!important}.pe-sm-5{padding-right:6rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.5rem!important}.pb-sm-2{padding-bottom:1rem!important}.pb-sm-3{padding-bottom:2rem!important}.pb-sm-4{padding-bottom:3rem!important}.pb-sm-5{padding-bottom:6rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.5rem!important}.ps-sm-2{padding-left:1rem!important}.ps-sm-3{padding-left:2rem!important}.ps-sm-4{padding-left:3rem!important}.ps-sm-5{padding-left:6rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.5rem!important}.gap-sm-2{gap:1rem!important}.gap-sm-3{gap:2rem!important}.gap-sm-4{gap:3rem!important}.gap-sm-5{gap:6rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.5rem!important}.row-gap-sm-2{row-gap:1rem!important}.row-gap-sm-3{row-gap:2rem!important}.row-gap-sm-4{row-gap:3rem!important}.row-gap-sm-5{row-gap:6rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-sm-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-sm-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-sm-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media(min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.5rem!important}.m-md-2{margin:1rem!important}.m-md-3{margin:2rem!important}.m-md-4{margin:3rem!important}.m-md-5{margin:6rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-2{margin-right:1rem!important;margin-left:1rem!important}.mx-md-3{margin-right:2rem!important;margin-left:2rem!important}.mx-md-4{margin-right:3rem!important;margin-left:3rem!important}.mx-md-5{margin-right:6rem!important;margin-left:6rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-md-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.5rem!important}.mt-md-2{margin-top:1rem!important}.mt-md-3{margin-top:2rem!important}.mt-md-4{margin-top:3rem!important}.mt-md-5{margin-top:6rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.5rem!important}.me-md-2{margin-right:1rem!important}.me-md-3{margin-right:2rem!important}.me-md-4{margin-right:3rem!important}.me-md-5{margin-right:6rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.5rem!important}.mb-md-2{margin-bottom:1rem!important}.mb-md-3{margin-bottom:2rem!important}.mb-md-4{margin-bottom:3rem!important}.mb-md-5{margin-bottom:6rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.5rem!important}.ms-md-2{margin-left:1rem!important}.ms-md-3{margin-left:2rem!important}.ms-md-4{margin-left:3rem!important}.ms-md-5{margin-left:6rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.5rem!important}.p-md-2{padding:1rem!important}.p-md-3{padding:2rem!important}.p-md-4{padding:3rem!important}.p-md-5{padding:6rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-2{padding-right:1rem!important;padding-left:1rem!important}.px-md-3{padding-right:2rem!important;padding-left:2rem!important}.px-md-4{padding-right:3rem!important;padding-left:3rem!important}.px-md-5{padding-right:6rem!important;padding-left:6rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-md-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-md-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.5rem!important}.pt-md-2{padding-top:1rem!important}.pt-md-3{padding-top:2rem!important}.pt-md-4{padding-top:3rem!important}.pt-md-5{padding-top:6rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.5rem!important}.pe-md-2{padding-right:1rem!important}.pe-md-3{padding-right:2rem!important}.pe-md-4{padding-right:3rem!important}.pe-md-5{padding-right:6rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.5rem!important}.pb-md-2{padding-bottom:1rem!important}.pb-md-3{padding-bottom:2rem!important}.pb-md-4{padding-bottom:3rem!important}.pb-md-5{padding-bottom:6rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.5rem!important}.ps-md-2{padding-left:1rem!important}.ps-md-3{padding-left:2rem!important}.ps-md-4{padding-left:3rem!important}.ps-md-5{padding-left:6rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.5rem!important}.gap-md-2{gap:1rem!important}.gap-md-3{gap:2rem!important}.gap-md-4{gap:3rem!important}.gap-md-5{gap:6rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.5rem!important}.row-gap-md-2{row-gap:1rem!important}.row-gap-md-3{row-gap:2rem!important}.row-gap-md-4{row-gap:3rem!important}.row-gap-md-5{row-gap:6rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-md-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-md-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-md-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media(min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.5rem!important}.m-lg-2{margin:1rem!important}.m-lg-3{margin:2rem!important}.m-lg-4{margin:3rem!important}.m-lg-5{margin:6rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-2{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-3{margin-right:2rem!important;margin-left:2rem!important}.mx-lg-4{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-5{margin-right:6rem!important;margin-left:6rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-lg-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.5rem!important}.mt-lg-2{margin-top:1rem!important}.mt-lg-3{margin-top:2rem!important}.mt-lg-4{margin-top:3rem!important}.mt-lg-5{margin-top:6rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.5rem!important}.me-lg-2{margin-right:1rem!important}.me-lg-3{margin-right:2rem!important}.me-lg-4{margin-right:3rem!important}.me-lg-5{margin-right:6rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.5rem!important}.mb-lg-2{margin-bottom:1rem!important}.mb-lg-3{margin-bottom:2rem!important}.mb-lg-4{margin-bottom:3rem!important}.mb-lg-5{margin-bottom:6rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.5rem!important}.ms-lg-2{margin-left:1rem!important}.ms-lg-3{margin-left:2rem!important}.ms-lg-4{margin-left:3rem!important}.ms-lg-5{margin-left:6rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.5rem!important}.p-lg-2{padding:1rem!important}.p-lg-3{padding:2rem!important}.p-lg-4{padding:3rem!important}.p-lg-5{padding:6rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-2{padding-right:1rem!important;padding-left:1rem!important}.px-lg-3{padding-right:2rem!important;padding-left:2rem!important}.px-lg-4{padding-right:3rem!important;padding-left:3rem!important}.px-lg-5{padding-right:6rem!important;padding-left:6rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-lg-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-lg-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.5rem!important}.pt-lg-2{padding-top:1rem!important}.pt-lg-3{padding-top:2rem!important}.pt-lg-4{padding-top:3rem!important}.pt-lg-5{padding-top:6rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.5rem!important}.pe-lg-2{padding-right:1rem!important}.pe-lg-3{padding-right:2rem!important}.pe-lg-4{padding-right:3rem!important}.pe-lg-5{padding-right:6rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.5rem!important}.pb-lg-2{padding-bottom:1rem!important}.pb-lg-3{padding-bottom:2rem!important}.pb-lg-4{padding-bottom:3rem!important}.pb-lg-5{padding-bottom:6rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.5rem!important}.ps-lg-2{padding-left:1rem!important}.ps-lg-3{padding-left:2rem!important}.ps-lg-4{padding-left:3rem!important}.ps-lg-5{padding-left:6rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.5rem!important}.gap-lg-2{gap:1rem!important}.gap-lg-3{gap:2rem!important}.gap-lg-4{gap:3rem!important}.gap-lg-5{gap:6rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.5rem!important}.row-gap-lg-2{row-gap:1rem!important}.row-gap-lg-3{row-gap:2rem!important}.row-gap-lg-4{row-gap:3rem!important}.row-gap-lg-5{row-gap:6rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-lg-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-lg-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-lg-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media(min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.5rem!important}.m-xl-2{margin:1rem!important}.m-xl-3{margin:2rem!important}.m-xl-4{margin:3rem!important}.m-xl-5{margin:6rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-2{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-3{margin-right:2rem!important;margin-left:2rem!important}.mx-xl-4{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-5{margin-right:6rem!important;margin-left:6rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-xl-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.5rem!important}.mt-xl-2{margin-top:1rem!important}.mt-xl-3{margin-top:2rem!important}.mt-xl-4{margin-top:3rem!important}.mt-xl-5{margin-top:6rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.5rem!important}.me-xl-2{margin-right:1rem!important}.me-xl-3{margin-right:2rem!important}.me-xl-4{margin-right:3rem!important}.me-xl-5{margin-right:6rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.5rem!important}.mb-xl-2{margin-bottom:1rem!important}.mb-xl-3{margin-bottom:2rem!important}.mb-xl-4{margin-bottom:3rem!important}.mb-xl-5{margin-bottom:6rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.5rem!important}.ms-xl-2{margin-left:1rem!important}.ms-xl-3{margin-left:2rem!important}.ms-xl-4{margin-left:3rem!important}.ms-xl-5{margin-left:6rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.5rem!important}.p-xl-2{padding:1rem!important}.p-xl-3{padding:2rem!important}.p-xl-4{padding:3rem!important}.p-xl-5{padding:6rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-2{padding-right:1rem!important;padding-left:1rem!important}.px-xl-3{padding-right:2rem!important;padding-left:2rem!important}.px-xl-4{padding-right:3rem!important;padding-left:3rem!important}.px-xl-5{padding-right:6rem!important;padding-left:6rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-xl-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-xl-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.5rem!important}.pt-xl-2{padding-top:1rem!important}.pt-xl-3{padding-top:2rem!important}.pt-xl-4{padding-top:3rem!important}.pt-xl-5{padding-top:6rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.5rem!important}.pe-xl-2{padding-right:1rem!important}.pe-xl-3{padding-right:2rem!important}.pe-xl-4{padding-right:3rem!important}.pe-xl-5{padding-right:6rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.5rem!important}.pb-xl-2{padding-bottom:1rem!important}.pb-xl-3{padding-bottom:2rem!important}.pb-xl-4{padding-bottom:3rem!important}.pb-xl-5{padding-bottom:6rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.5rem!important}.ps-xl-2{padding-left:1rem!important}.ps-xl-3{padding-left:2rem!important}.ps-xl-4{padding-left:3rem!important}.ps-xl-5{padding-left:6rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.5rem!important}.gap-xl-2{gap:1rem!important}.gap-xl-3{gap:2rem!important}.gap-xl-4{gap:3rem!important}.gap-xl-5{gap:6rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.5rem!important}.row-gap-xl-2{row-gap:1rem!important}.row-gap-xl-3{row-gap:2rem!important}.row-gap-xl-4{row-gap:3rem!important}.row-gap-xl-5{row-gap:6rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-xl-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-xl-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-xl-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media(min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.5rem!important}.m-xxl-2{margin:1rem!important}.m-xxl-3{margin:2rem!important}.m-xxl-4{margin:3rem!important}.m-xxl-5{margin:6rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-2{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-3{margin-right:2rem!important;margin-left:2rem!important}.mx-xxl-4{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-5{margin-right:6rem!important;margin-left:6rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-xxl-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.5rem!important}.mt-xxl-2{margin-top:1rem!important}.mt-xxl-3{margin-top:2rem!important}.mt-xxl-4{margin-top:3rem!important}.mt-xxl-5{margin-top:6rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.5rem!important}.me-xxl-2{margin-right:1rem!important}.me-xxl-3{margin-right:2rem!important}.me-xxl-4{margin-right:3rem!important}.me-xxl-5{margin-right:6rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.5rem!important}.mb-xxl-2{margin-bottom:1rem!important}.mb-xxl-3{margin-bottom:2rem!important}.mb-xxl-4{margin-bottom:3rem!important}.mb-xxl-5{margin-bottom:6rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.5rem!important}.ms-xxl-2{margin-left:1rem!important}.ms-xxl-3{margin-left:2rem!important}.ms-xxl-4{margin-left:3rem!important}.ms-xxl-5{margin-left:6rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.5rem!important}.p-xxl-2{padding:1rem!important}.p-xxl-3{padding:2rem!important}.p-xxl-4{padding:3rem!important}.p-xxl-5{padding:6rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-2{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-3{padding-right:2rem!important;padding-left:2rem!important}.px-xxl-4{padding-right:3rem!important;padding-left:3rem!important}.px-xxl-5{padding-right:6rem!important;padding-left:6rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-xxl-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-xxl-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.5rem!important}.pt-xxl-2{padding-top:1rem!important}.pt-xxl-3{padding-top:2rem!important}.pt-xxl-4{padding-top:3rem!important}.pt-xxl-5{padding-top:6rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.5rem!important}.pe-xxl-2{padding-right:1rem!important}.pe-xxl-3{padding-right:2rem!important}.pe-xxl-4{padding-right:3rem!important}.pe-xxl-5{padding-right:6rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.5rem!important}.pb-xxl-2{padding-bottom:1rem!important}.pb-xxl-3{padding-bottom:2rem!important}.pb-xxl-4{padding-bottom:3rem!important}.pb-xxl-5{padding-bottom:6rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.5rem!important}.ps-xxl-2{padding-left:1rem!important}.ps-xxl-3{padding-left:2rem!important}.ps-xxl-4{padding-left:3rem!important}.ps-xxl-5{padding-left:6rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.5rem!important}.gap-xxl-2{gap:1rem!important}.gap-xxl-3{gap:2rem!important}.gap-xxl-4{gap:3rem!important}.gap-xxl-5{gap:6rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.5rem!important}.row-gap-xxl-2{row-gap:1rem!important}.row-gap-xxl-3{row-gap:2rem!important}.row-gap-xxl-4{row-gap:3rem!important}.row-gap-xxl-5{row-gap:6rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-xxl-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-xxl-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-xxl-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media(min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}body{background-image:linear-gradient(90deg,#21373f,#5e62b0,#700936)}[data-bs-theme=dark] body{color:#fff}.table-secondary{--bs-table-hover-color:$white}.btn-secondary{color:#fff;border:none}.btn-secondary:focus,.btn-secondary:hover{color:#fff}.btn-secondary.disabled{color:#fff}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:0}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:0}.bg-light .btn{background-color:rgba(0,0,0,.2)}.form-control::-webkit-file-upload-button{background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);border:none}.form-control::file-selector-button,.input-group-text{background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);border:none}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:rgba(255,255,255,.1)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:rgba(255,255,255,.1)}.input-group-text{box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1)}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:0;border-left:none}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-right:none}.form-select:not([multiple]){transition:border-color .15s ease-in-out}.form-select:not([multiple]) option{color:#000}.bg-light .form-control{color:#343a40;border-color:rgba(0,0,0,.2)}.bg-light .form-control::-moz-placeholder{color:rgba(0,0,0,.2)}.bg-light .form-control::placeholder{color:rgba(0,0,0,.2)}.dropdown-menu{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.dropdown-menu a{color:#fff}.dropdown-menu .text-muted{color:rgba(255,255,255,.7)!important}.nav-tabs .nav-link{transition:none}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{color:#fff;isolation:auto}.nav-tabs .nav-link,.nav-tabs .nav-link.disabled{background-color:rgba(0,0,0,.2)}.nav-tabs .nav-item:first-child>.nav-link{border-radius:.5rem 0 0 .5rem}.nav-tabs .nav-item:last-child>.nav-link{border-radius:0 .5rem .5rem 0}.nav-tabs .nav-item .nav-link.active,.nav-tabs .nav-item.show .nav-link{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,#fff,rgba(255,255,255,.9) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);border-radius:.5rem;transform:scale(1.1)}.nav-tabs .nav-item .nav-link.active a,.nav-tabs .nav-item.show .nav-link a{color:#fff}.nav-tabs .nav-item .nav-link.active .text-muted,.nav-tabs .nav-item.show .nav-link .text-muted{color:rgba(255,255,255,.7)!important}.nav-tabs .nav-item .nav-link.active:focus,.nav-tabs .nav-item .nav-link.active:hover,.nav-tabs .nav-item.show .nav-link:focus,.nav-tabs .nav-item.show .nav-link:hover{color:#343a40}.nav-tabs .dropdown-menu{border-radius:.5rem}.nav-pills .nav-link{transition:none}.nav-pills .nav-link:focus,.nav-pills .nav-link:hover{color:#fff;isolation:auto}.alert{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);position:relative;overflow:hidden;color:#fff}.alert a{color:#fff}.alert .text-muted{color:rgba(255,255,255,.7)!important}.alert-primary::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#e83283}.alert-secondary::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:rgba(255,255,255,.4)}.alert-success::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#41d7a7}.alert-info::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#39cbfb}.alert-warning::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#ffc107}.alert-danger::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#fd7e14}.alert-light::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#e9e9e8}.alert-dark::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#212529}.alert .alert-link{color:#fff}.badge.bg-secondary{--bs-bg-opacity:.4}.badge.bg-light{color:#343a40}.list-group{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.list-group a{color:#fff}.list-group .text-muted{color:rgba(255,255,255,.7)!important}.card{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);--bs-secondary-rgb:transparent}.card a{color:#fff}.card .text-muted{color:rgba(255,255,255,.7)!important}.card.bg-primary{background-image:none}.card.bg-success{background-image:none}.card.bg-info{background-image:none}.card.bg-warning{background-image:none}.card.bg-danger{background-image:none}.card.bg-light{background-image:none}.card.bg-dark{background-image:none}.card.bg-light .card-body,.card.bg-light .card-header{color:#343a40}.card.border-primary{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-primary a{color:#fff}.card.border-primary .text-muted{color:rgba(255,255,255,.7)!important}.card.border-primary .card-header{background-color:#e83283!important;border-bottom:none}.card.border-secondary{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-secondary a{color:#fff}.card.border-secondary .text-muted{color:rgba(255,255,255,.7)!important}.card.border-secondary .card-header{background-color:rgba(255,255,255,.4)!important;border-bottom:none}.card.border-success{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-success a{color:#fff}.card.border-success .text-muted{color:rgba(255,255,255,.7)!important}.card.border-success .card-header{background-color:#41d7a7!important;border-bottom:none}.card.border-info{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-info a{color:#fff}.card.border-info .text-muted{color:rgba(255,255,255,.7)!important}.card.border-info .card-header{background-color:#39cbfb!important;border-bottom:none}.card.border-warning{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-warning a{color:#fff}.card.border-warning .text-muted{color:rgba(255,255,255,.7)!important}.card.border-warning .card-header{background-color:#ffc107!important;border-bottom:none}.card.border-danger{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-danger a{color:#fff}.card.border-danger .text-muted{color:rgba(255,255,255,.7)!important}.card.border-danger .card-header{background-color:#fd7e14!important;border-bottom:none}.card.border-light{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-light a{color:#fff}.card.border-light .text-muted{color:rgba(255,255,255,.7)!important}.card.border-light .card-header{background-color:#e9e9e8!important;border-bottom:none;color:#343a40}.card.border-dark{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-dark a{color:#fff}.card.border-dark .text-muted{color:rgba(255,255,255,.7)!important}.card.border-dark .card-header{background-color:#212529!important;border-bottom:none}.card-header{font-weight:700}.accordion-item{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);box-shadow:none}.accordion-item a{color:#fff}.accordion-item .text-muted{color:rgba(255,255,255,.7)!important}.toast{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.toast a{color:#fff}.toast .text-muted{color:rgba(255,255,255,.7)!important}.popover{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.popover a{color:#fff}.popover .text-muted{color:rgba(255,255,255,.7)!important}.popover-header{border-bottom-color:rgba(255,255,255,.2)}.tooltip{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.modal-content{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.modal-content a{color:#fff}.modal-content .text-muted{color:rgba(255,255,255,.7)!important}.offcanvas{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.offcanvas a{color:#fff}.offcanvas .text-muted{color:rgba(255,255,255,.7)!important}/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file +* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)*/:root,[data-bs-theme=light]{--bs-blue:#3a8fd9;--bs-indigo:#6610f2;--bs-purple:#686dc3;--bs-pink:#e83283;--bs-red:#fc346f;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#41d7a7;--bs-teal:#528fb3;--bs-cyan:#39cbfb;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9e9e8;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#e83283;--bs-secondary:rgba(255, 255, 255, 0.4);--bs-success:#41d7a7;--bs-info:#39cbfb;--bs-warning:#ffc107;--bs-danger:#fd7e14;--bs-light:#e9e9e8;--bs-dark:#212529;--bs-primary-rgb:232, 50, 131;--bs-secondary-rgb:255, 255, 255;--bs-success-rgb:65, 215, 167;--bs-info-rgb:57, 203, 251;--bs-warning-rgb:255, 193, 7;--bs-danger-rgb:253, 126, 20;--bs-light-rgb:233, 233, 232;--bs-dark-rgb:33, 37, 41;--bs-primary-text-emphasis:#5d1434;--bs-secondary-text-emphasis:rgba(36, 36, 36, 0.76);--bs-success-text-emphasis:#1a5643;--bs-info-text-emphasis:#175164;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#653208;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#fad6e6;--bs-secondary-bg-subtle:rgba(255, 255, 255, 0.88);--bs-success-bg-subtle:#d9f7ed;--bs-info-bg-subtle:#d7f5fe;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#ffe5d0;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#f6adcd;--bs-secondary-border-subtle:rgba(255, 255, 255, 0.76);--bs-success-border-subtle:#b3efdc;--bs-info-border-subtle:#b0eafd;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#fecba1;--bs-light-border-subtle:#e9e9e8;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255, 255, 255;--bs-black-rgb:0, 0, 0;--bs-font-sans-serif:system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#fff;--bs-body-color-rgb:255, 255, 255;--bs-body-bg:#686dc3;--bs-body-bg-rgb:104, 109, 195;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0, 0, 0;--bs-secondary-color:rgba(255, 255, 255, 0.75);--bs-secondary-color-rgb:255, 255, 255;--bs-secondary-bg:#e9e9e8;--bs-secondary-bg-rgb:233, 233, 232;--bs-tertiary-color:rgba(255, 255, 255, 0.5);--bs-tertiary-color-rgb:255, 255, 255;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248, 249, 250;--bs-heading-color:inherit;--bs-link-color:#fff;--bs-link-color-rgb:255, 255, 255;--bs-link-decoration:underline;--bs-link-hover-color:#cccccc;--bs-link-hover-color-rgb:204, 204, 204;--bs-code-color:#e83283;--bs-highlight-color:#fff;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:rgba(255, 255, 255, 0.2);--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.5rem;--bs-border-radius-sm:0.6rem;--bs-border-radius-lg:0.7rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:1px 3px 24px -1px rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 1px 1px rgba(0, 0, 0, 0.1);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(232, 50, 131, 0.25);--bs-form-valid-color:#41d7a7;--bs-form-valid-border-color:#41d7a7;--bs-form-invalid-color:#fd7e14;--bs-form-invalid-border-color:#fd7e14}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222, 226, 230;--bs-body-bg:#212529;--bs-body-bg-rgb:33, 37, 41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255, 255, 255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222, 226, 230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52, 58, 64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222, 226, 230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43, 48, 53;--bs-primary-text-emphasis:#f184b5;--bs-secondary-text-emphasis:rgba(255, 255, 255, 0.64);--bs-success-text-emphasis:#8de7ca;--bs-info-text-emphasis:#88e0fd;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#feb272;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#2e0a1a;--bs-secondary-bg-subtle:rgba(15, 15, 15, 0.88);--bs-success-bg-subtle:#0d2b21;--bs-info-bg-subtle:#0b2932;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#331904;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#8b1e4f;--bs-secondary-border-subtle:rgba(70, 70, 70, 0.64);--bs-success-border-subtle:#278164;--bs-info-border-subtle:#227a97;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#984c0c;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#f184b5;--bs-link-hover-color:#f49dc4;--bs-link-color-rgb:241, 132, 181;--bs-link-hover-color-rgb:244, 157, 196;--bs-code-color:#f184b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#8de7ca;--bs-form-valid-border-color:#8de7ca;--bs-form-invalid-color:#fd85a9;--bs-form-invalid-border-color:#fd85a9}*,::after,::before{box-sizing:border-box}@media(prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:2rem 0;color:inherit;border:0;border-top:var(--bs-border-width)solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:1rem;font-weight:700;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media(min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media(min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media(min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media(min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.6rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;line-height:inherit;font-size:calc(1.275rem + .3vw)}@media(min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button{cursor:pointer;filter:grayscale(1)}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-weight:300;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media(min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:300;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media(min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:300;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media(min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:300;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media(min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:300;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media(min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:300;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@media(min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:2rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-2rem;margin-bottom:2rem;font-size:.875em;color:rgba(255,255,255,.7)}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width)solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:1rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media(min-width:576px){.container,.container-sm{max-width:540px}}@media(min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media(min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media(min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media(min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0}.row-cols-auto>*{flex:none;width:auto}.row-cols-1>*{flex:none;width:100%}.row-cols-2>*{flex:none;width:50%}.row-cols-3>*{flex:none;width:33.33333333%}.row-cols-4>*{flex:none;width:25%}.row-cols-5>*{flex:none;width:20%}.row-cols-6>*{flex:none;width:16.66666667%}.col-auto{flex:none;width:auto}.col-1{flex:none;width:8.33333333%}.col-2{flex:none;width:16.66666667%}.col-3{flex:none;width:25%}.col-4{flex:none;width:33.33333333%}.col-5{flex:none;width:41.66666667%}.col-6{flex:none;width:50%}.col-7{flex:none;width:58.33333333%}.col-8{flex:none;width:66.66666667%}.col-9{flex:none;width:75%}.col-10{flex:none;width:83.33333333%}.col-11{flex:none;width:91.66666667%}.col-12{flex:none;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.5rem}.g-1,.gy-1{--bs-gutter-y:0.5rem}.g-2,.gx-2{--bs-gutter-x:1rem}.g-2,.gy-2{--bs-gutter-y:1rem}.g-3,.gx-3{--bs-gutter-x:2rem}.g-3,.gy-3{--bs-gutter-y:2rem}.g-4,.gx-4{--bs-gutter-x:3rem}.g-4,.gy-4{--bs-gutter-y:3rem}.g-5,.gx-5{--bs-gutter-x:6rem}.g-5,.gy-5{--bs-gutter-y:6rem}@media(min-width:576px){.col-sm{flex:1 0}.row-cols-sm-auto>*{flex:none;width:auto}.row-cols-sm-1>*{flex:none;width:100%}.row-cols-sm-2>*{flex:none;width:50%}.row-cols-sm-3>*{flex:none;width:33.33333333%}.row-cols-sm-4>*{flex:none;width:25%}.row-cols-sm-5>*{flex:none;width:20%}.row-cols-sm-6>*{flex:none;width:16.66666667%}.col-sm-auto{flex:none;width:auto}.col-sm-1{flex:none;width:8.33333333%}.col-sm-2{flex:none;width:16.66666667%}.col-sm-3{flex:none;width:25%}.col-sm-4{flex:none;width:33.33333333%}.col-sm-5{flex:none;width:41.66666667%}.col-sm-6{flex:none;width:50%}.col-sm-7{flex:none;width:58.33333333%}.col-sm-8{flex:none;width:66.66666667%}.col-sm-9{flex:none;width:75%}.col-sm-10{flex:none;width:83.33333333%}.col-sm-11{flex:none;width:91.66666667%}.col-sm-12{flex:none;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.5rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.5rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:1rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:1rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:2rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:2rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:3rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:3rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:6rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:6rem}}@media(min-width:768px){.col-md{flex:1 0}.row-cols-md-auto>*{flex:none;width:auto}.row-cols-md-1>*{flex:none;width:100%}.row-cols-md-2>*{flex:none;width:50%}.row-cols-md-3>*{flex:none;width:33.33333333%}.row-cols-md-4>*{flex:none;width:25%}.row-cols-md-5>*{flex:none;width:20%}.row-cols-md-6>*{flex:none;width:16.66666667%}.col-md-auto{flex:none;width:auto}.col-md-1{flex:none;width:8.33333333%}.col-md-2{flex:none;width:16.66666667%}.col-md-3{flex:none;width:25%}.col-md-4{flex:none;width:33.33333333%}.col-md-5{flex:none;width:41.66666667%}.col-md-6{flex:none;width:50%}.col-md-7{flex:none;width:58.33333333%}.col-md-8{flex:none;width:66.66666667%}.col-md-9{flex:none;width:75%}.col-md-10{flex:none;width:83.33333333%}.col-md-11{flex:none;width:91.66666667%}.col-md-12{flex:none;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.5rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.5rem}.g-md-2,.gx-md-2{--bs-gutter-x:1rem}.g-md-2,.gy-md-2{--bs-gutter-y:1rem}.g-md-3,.gx-md-3{--bs-gutter-x:2rem}.g-md-3,.gy-md-3{--bs-gutter-y:2rem}.g-md-4,.gx-md-4{--bs-gutter-x:3rem}.g-md-4,.gy-md-4{--bs-gutter-y:3rem}.g-md-5,.gx-md-5{--bs-gutter-x:6rem}.g-md-5,.gy-md-5{--bs-gutter-y:6rem}}@media(min-width:992px){.col-lg{flex:1 0}.row-cols-lg-auto>*{flex:none;width:auto}.row-cols-lg-1>*{flex:none;width:100%}.row-cols-lg-2>*{flex:none;width:50%}.row-cols-lg-3>*{flex:none;width:33.33333333%}.row-cols-lg-4>*{flex:none;width:25%}.row-cols-lg-5>*{flex:none;width:20%}.row-cols-lg-6>*{flex:none;width:16.66666667%}.col-lg-auto{flex:none;width:auto}.col-lg-1{flex:none;width:8.33333333%}.col-lg-2{flex:none;width:16.66666667%}.col-lg-3{flex:none;width:25%}.col-lg-4{flex:none;width:33.33333333%}.col-lg-5{flex:none;width:41.66666667%}.col-lg-6{flex:none;width:50%}.col-lg-7{flex:none;width:58.33333333%}.col-lg-8{flex:none;width:66.66666667%}.col-lg-9{flex:none;width:75%}.col-lg-10{flex:none;width:83.33333333%}.col-lg-11{flex:none;width:91.66666667%}.col-lg-12{flex:none;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.5rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.5rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:1rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:1rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:2rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:2rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:3rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:3rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:6rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:6rem}}@media(min-width:1200px){.col-xl{flex:1 0}.row-cols-xl-auto>*{flex:none;width:auto}.row-cols-xl-1>*{flex:none;width:100%}.row-cols-xl-2>*{flex:none;width:50%}.row-cols-xl-3>*{flex:none;width:33.33333333%}.row-cols-xl-4>*{flex:none;width:25%}.row-cols-xl-5>*{flex:none;width:20%}.row-cols-xl-6>*{flex:none;width:16.66666667%}.col-xl-auto{flex:none;width:auto}.col-xl-1{flex:none;width:8.33333333%}.col-xl-2{flex:none;width:16.66666667%}.col-xl-3{flex:none;width:25%}.col-xl-4{flex:none;width:33.33333333%}.col-xl-5{flex:none;width:41.66666667%}.col-xl-6{flex:none;width:50%}.col-xl-7{flex:none;width:58.33333333%}.col-xl-8{flex:none;width:66.66666667%}.col-xl-9{flex:none;width:75%}.col-xl-10{flex:none;width:83.33333333%}.col-xl-11{flex:none;width:91.66666667%}.col-xl-12{flex:none;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.5rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.5rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:1rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:1rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:2rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:2rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:3rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:3rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:6rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:6rem}}@media(min-width:1400px){.col-xxl{flex:1 0}.row-cols-xxl-auto>*{flex:none;width:auto}.row-cols-xxl-1>*{flex:none;width:100%}.row-cols-xxl-2>*{flex:none;width:50%}.row-cols-xxl-3>*{flex:none;width:33.33333333%}.row-cols-xxl-4>*{flex:none;width:25%}.row-cols-xxl-5>*{flex:none;width:20%}.row-cols-xxl-6>*{flex:none;width:16.66666667%}.col-xxl-auto{flex:none;width:auto}.col-xxl-1{flex:none;width:8.33333333%}.col-xxl-2{flex:none;width:16.66666667%}.col-xxl-3{flex:none;width:25%}.col-xxl-4{flex:none;width:33.33333333%}.col-xxl-5{flex:none;width:41.66666667%}.col-xxl-6{flex:none;width:50%}.col-xxl-7{flex:none;width:58.33333333%}.col-xxl-8{flex:none;width:66.66666667%}.col-xxl-9{flex:none;width:75%}.col-xxl-10{flex:none;width:83.33333333%}.col-xxl-11{flex:none;width:91.66666667%}.col-xxl-12{flex:none;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.5rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.5rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:1rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:1rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:2rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:2rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:3rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:3rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:6rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:6rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:initial;--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:initial;--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:initial;--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:initial;--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:2rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2)solid}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width)0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#fff;--bs-table-bg:#e83283;--bs-table-border-color:#ed5b9c;--bs-table-striped-bg:#e93c89;--bs-table-striped-color:#fff;--bs-table-active-bg:#ea478f;--bs-table-active-color:#fff;--bs-table-hover-bg:#ea418c;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#fff;--bs-table-bg:rgba(255, 255, 255, 0.4);--bs-table-border-color:rgba(255, 255, 255, 0.52);--bs-table-striped-bg:rgba(255, 255, 255, 0.43);--bs-table-striped-color:#000;--bs-table-active-bg:rgba(255, 255, 255, 0.46);--bs-table-active-color:#000;--bs-table-hover-bg:rgba(255, 255, 255, 0.445);--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#fff;--bs-table-bg:#41d7a7;--bs-table-border-color:#67dfb9;--bs-table-striped-bg:#4bd9ab;--bs-table-striped-color:#fff;--bs-table-active-bg:#54dbb0;--bs-table-active-color:#fff;--bs-table-hover-bg:#4fdaae;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#fff;--bs-table-bg:#39cbfb;--bs-table-border-color:#61d5fc;--bs-table-striped-bg:#43cefb;--bs-table-striped-color:#fff;--bs-table-active-bg:#4dd0fb;--bs-table-active-color:#fff;--bs-table-hover-bg:#48cffb;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#fff;--bs-table-bg:#ffc107;--bs-table-border-color:#ffcd39;--bs-table-striped-bg:#ffc413;--bs-table-striped-color:#fff;--bs-table-active-bg:#ffc720;--bs-table-active-color:#fff;--bs-table-hover-bg:#ffc61a;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#fff;--bs-table-bg:#fd7e14;--bs-table-border-color:#fd9843;--bs-table-striped-bg:#fd8420;--bs-table-striped-color:#fff;--bs-table-active-bg:#fd8b2c;--bs-table-active-color:#fff;--bs-table-hover-bg:#fd8826;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#e9e9e8;--bs-table-border-color:#bababa;--bs-table-striped-bg:#dddddc;--bs-table-striped-color:#000;--bs-table-active-bg:#d2d2d1;--bs-table-active-color:#fff;--bs-table-hover-bg:#d8d8d7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.75rem + 1px);padding-bottom:calc(.75rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.75rem 1.5rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:initial;background-clip:padding-box;border:1px solid rgba(255,255,255,.4);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:initial;border-color:rgba(255,255,255,.4);outline:0;box-shadow:none}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:rgba(255,255,255,.7);opacity:1}.form-control::placeholder{color:rgba(255,255,255,.7);opacity:1}.form-control:disabled{background-color:rgba(255,255,255,.1);opacity:1}.form-control::-webkit-file-upload-button{padding:.75rem 1.5rem;margin:-.75rem -1.5rem;-webkit-margin-end:1.5rem;margin-inline-end:1.5rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.75rem 1.5rem;margin:-.75rem -1.5rem;-webkit-margin-end:1.5rem;margin-inline-end:1.5rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.75rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:initial;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(1px * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + 1.5rem + calc(1px * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(1px * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(1px * 2))}.form-control-color{width:3rem;height:calc(1.5em + 1.5rem + calc(1px * 2));padding:.75rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(1px * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(1px * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.75rem 4.5rem .75rem 1.5rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:initial;background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right 1.5rem center;background-size:16px 12px;border:1px solid rgba(255,255,255,.4);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:rgba(255,255,255,.4);outline:0;box-shadow:none}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:1.5rem;background-image:none}.form-select:disabled{color:rgba(255,255,255,.7);background-color:rgba(255,255,255,.1)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:rgba(255, 255, 255, 0.3);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:50%;background-size:contain;border:1px solid rgba(255,255,255,.2);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:rgba(255,255,255,.4);outline:0;box-shadow:0 0 0 .25rem rgba(232,50,131,.25)}.form-check-input:checked{background-color:#e83283;border-color:#e83283}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#e83283;border-color:#e83283;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:0;border-radius:2em;transition:background-position .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:100%;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:initial}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #686dc3,none}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #686dc3,none}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#e83283;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#f8c2da}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:rgba(0,0,0,.2);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#e83283;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#f8c2da}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:rgba(0,0,0,.2);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(1px * 2));min-height:calc(3.5rem + calc(1px * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem 1.5rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media(prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem 1.5rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:1.5rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{transform:scale(.85)translateY(-.5rem)translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85)translateY(-.5rem)translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85)translateY(-.5rem)translateX(.15rem)}.form-floating>textarea:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem .75rem;z-index:-1;height:1.5em;content:"";background-color:initial;border-radius:var(--bs-border-radius)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem .75rem;z-index:-1;height:1.5em;content:"";background-color:initial;border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:rgba(255,255,255,.1)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.75rem 1.5rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:initial;border:1px solid rgba(255,255,255,.4);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:6rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1 * 1px);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem 1rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + 1.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2341d7a7' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .375rem)center;background-size:calc(.75em + .75rem)calc(.75em + .75rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 1.5rem);background-position:top calc(.375em + .375rem)right calc(.375em + .375rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2341d7a7' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");padding-right:8.25rem;background-position:right 1.5rem center,center right 4.5rem;background-size:16px 12px,calc(.75em + .75rem)calc(.75em + .75rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + 1.5rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem 1rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + 1.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23fd7e14'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23fd7e14' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .375rem)center;background-size:calc(.75em + .75rem)calc(.75em + .75rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 1.5rem);background-position:top calc(.375em + .375rem)right calc(.375em + .375rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23fd7e14'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23fd7e14' stroke='none'/%3e%3c/svg%3e");padding-right:8.25rem;background-position:right 1.5rem center,center right 4.5rem;background-size:16px 12px,calc(.75em + .75rem)calc(.75em + .75rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + 1.5rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:1.5rem;--bs-btn-padding-y:0.75rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:1px 3px 24px -1px rgba(0, 0, 0, 0.15);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y)var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width)solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#e83283;--bs-btn-border-color:#e83283;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#c52b6f;--bs-btn-hover-border-color:#ba2869;--bs-btn-focus-shadow-rgb:235, 81, 150;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ba2869;--bs-btn-active-border-color:#ae2662;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#e83283;--bs-btn-disabled-border-color:#e83283}.btn-secondary{--bs-btn-color:#000;--bs-btn-bg:rgba(255, 255, 255, 0.4);--bs-btn-border-color:rgba(255, 255, 255, 0.4);--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgba(255, 255, 255, 0.49);--bs-btn-hover-border-color:rgba(255, 255, 255, 0.46);--bs-btn-focus-shadow-rgb:149, 149, 149;--bs-btn-active-color:#000;--bs-btn-active-bg:rgba(255, 255, 255, 0.52);--bs-btn-active-border-color:rgba(255, 255, 255, 0.46);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:rgba(255, 255, 255, 0.4);--bs-btn-disabled-border-color:rgba(255, 255, 255, 0.4)}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#41d7a7;--bs-btn-border-color:#41d7a7;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#37b78e;--bs-btn-hover-border-color:#34ac86;--bs-btn-focus-shadow-rgb:94, 221, 180;--bs-btn-active-color:#fff;--bs-btn-active-bg:#34ac86;--bs-btn-active-border-color:#31a17d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#41d7a7;--bs-btn-disabled-border-color:#41d7a7}.btn-info{--bs-btn-color:#fff;--bs-btn-bg:#39cbfb;--bs-btn-border-color:#39cbfb;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#30add5;--bs-btn-hover-border-color:#2ea2c9;--bs-btn-focus-shadow-rgb:87, 211, 252;--bs-btn-active-color:#fff;--bs-btn-active-bg:#2ea2c9;--bs-btn-active-border-color:#2b98bc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#39cbfb;--bs-btn-disabled-border-color:#39cbfb}.btn-warning{--bs-btn-color:#fff;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#d9a406;--bs-btn-hover-border-color:#cc9a06;--bs-btn-focus-shadow-rgb:255, 202, 44;--bs-btn-active-color:#fff;--bs-btn-active-bg:#cc9a06;--bs-btn-active-border-color:#bf9105;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#fd7e14;--bs-btn-border-color:#fd7e14;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#d76b11;--bs-btn-hover-border-color:#ca6510;--bs-btn-focus-shadow-rgb:253, 145, 55;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ca6510;--bs-btn-active-border-color:#be5f0f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#fd7e14;--bs-btn-disabled-border-color:#fd7e14}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#e9e9e8;--bs-btn-border-color:#e9e9e8;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#c6c6c5;--bs-btn-hover-border-color:#bababa;--bs-btn-focus-shadow-rgb:198, 198, 197;--bs-btn-active-color:#fff;--bs-btn-active-bg:#bababa;--bs-btn-active-border-color:#afafae;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#e9e9e8;--bs-btn-disabled-border-color:#e9e9e8}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66, 70, 73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#e83283;--bs-btn-border-color:#e83283;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#e83283;--bs-btn-hover-border-color:#e83283;--bs-btn-focus-shadow-rgb:232, 50, 131;--bs-btn-active-color:#fff;--bs-btn-active-bg:#e83283;--bs-btn-active-border-color:#e83283;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#e83283;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#e83283;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:rgba(255, 255, 255, 0.4);--bs-btn-border-color:rgba(255, 255, 255, 0.4);--bs-btn-hover-color:#000;--bs-btn-hover-bg:rgba(255, 255, 255, 0.4);--bs-btn-hover-border-color:rgba(255, 255, 255, 0.4);--bs-btn-focus-shadow-rgb:255, 255, 255;--bs-btn-active-color:#000;--bs-btn-active-bg:rgba(255, 255, 255, 0.4);--bs-btn-active-border-color:rgba(255, 255, 255, 0.4);--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:rgba(255, 255, 255, 0.4);--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:rgba(255, 255, 255, 0.4);--bs-gradient:none}.btn-outline-success{--bs-btn-color:#41d7a7;--bs-btn-border-color:#41d7a7;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#41d7a7;--bs-btn-hover-border-color:#41d7a7;--bs-btn-focus-shadow-rgb:65, 215, 167;--bs-btn-active-color:#fff;--bs-btn-active-bg:#41d7a7;--bs-btn-active-border-color:#41d7a7;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#41d7a7;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#41d7a7;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#39cbfb;--bs-btn-border-color:#39cbfb;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#39cbfb;--bs-btn-hover-border-color:#39cbfb;--bs-btn-focus-shadow-rgb:57, 203, 251;--bs-btn-active-color:#fff;--bs-btn-active-bg:#39cbfb;--bs-btn-active-border-color:#39cbfb;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#39cbfb;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#39cbfb;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255, 193, 7;--bs-btn-active-color:#fff;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#fd7e14;--bs-btn-border-color:#fd7e14;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#fd7e14;--bs-btn-hover-border-color:#fd7e14;--bs-btn-focus-shadow-rgb:253, 126, 20;--bs-btn-active-color:#fff;--bs-btn-active-bg:#fd7e14;--bs-btn-active-border-color:#fd7e14;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fd7e14;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#fd7e14;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#e9e9e8;--bs-btn-border-color:#e9e9e8;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#e9e9e8;--bs-btn-hover-border-color:#e9e9e8;--bs-btn-focus-shadow-rgb:233, 233, 232;--bs-btn-active-color:#000;--bs-btn-active-bg:#e9e9e8;--bs-btn-active-border-color:#e9e9e8;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#e9e9e8;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#e9e9e8;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33, 37, 41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:217, 217, 217;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media(prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media(prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media(prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:rgba(255, 255, 255, 0.2);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:rgba(255, 255, 255, 0.2);--bs-dropdown-divider-margin-y:1rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:#fff;--bs-dropdown-link-hover-bg:rgba(0, 0, 0, 0.4);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#e83283;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:2rem;--bs-dropdown-item-padding-y:0.5rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:2rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y)var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width)solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y)0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y)var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:initial;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:initial}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y)var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y)var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:rgba(255, 255, 255, 0.2);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:rgba(255, 255, 255, 0.2);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#e83283;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--bs-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(-1 * var(--bs-border-width))}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:rgba(255, 255, 255, 0.7);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y)var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media(prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(232,50,131,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:0;--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:0;--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:#343a40;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width)solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width)solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:50rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#e83283}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:#fff;gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width)solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:initial}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:initial}.nav-fill .nav-item,.nav-fill>.nav-link{flex:auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:1rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y)var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y)var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:initial;border:var(--bs-border-width)solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media(prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:50%;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media(min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:initial!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1.75rem;--bs-card-spacer-x:2rem;--bs-card-title-spacer-y:1rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:rgba(255, 255, 255, 0.2);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.875rem;--bs-card-cap-padding-x:2rem;--bs-card-cap-bg:transparent;--bs-card-cap-color:#fff;--bs-card-height: ;--bs-card-color:#fff;--bs-card-bg:transparent;--bs-card-img-overlay-padding:2rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width)solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:auto;padding:var(--bs-card-spacer-y)var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y)var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width)solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius)var(--bs-card-inner-border-radius)0 0}.card-footer{padding:var(--bs-card-cap-padding-y)var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width)solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius)var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media(min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child)>.card-header,.card-group>.card:not(:last-child)>.card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-footer,.card-group>.card:not(:last-child)>.card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child)>.card-header,.card-group>.card:not(:first-child)>.card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-footer,.card-group>.card:not(:first-child)>.card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:rgba(255, 255, 255, 0.4);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%235d1434' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(232, 50, 131, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:#fff;--bs-accordion-active-bg:#e83283}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y)var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media(prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width))0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media(prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width)solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y)var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f184b5'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23f184b5'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:#fff;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#fff;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y)var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider,"/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:rgba(255, 255, 255, 0.3);--bs-pagination-border-width:0;--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:#fff;--bs-pagination-hover-bg:rgba(255, 255, 255, 0.4);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:#fff;--bs-pagination-focus-bg:rgba(255, 255, 255, 0.4);--bs-pagination-focus-box-shadow:none;--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#e83283;--bs-pagination-active-border-color:#e83283;--bs-pagination-disabled-color:rgba(255, 255, 255, 0.7);--bs-pagination-disabled-bg:rgba(255, 255, 255, 0.3);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y)var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width)solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(-1 * 0)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y)var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:2rem;--bs-alert-padding-y:2rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y)var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:6rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:2.5rem 2rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:rgba(0, 0, 0, 0.2);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#e83283;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media(prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height)var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media(prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:#fff;--bs-list-group-bg:transparent;--bs-list-group-border-color:rgba(255, 255, 255, 0.2);--bs-list-group-border-width:0;--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:2rem;--bs-list-group-item-padding-y:1rem;--bs-list-group-action-color:#fff;--bs-list-group-action-hover-color:#fff;--bs-list-group-action-hover-bg:rgba(255, 255, 255, 0.4);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:rgba(255, 255, 255, 0.7);--bs-list-group-disabled-bg:transparent;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#e83283;--bs-list-group-active-border-color:#e83283;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section,".")". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y)var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width)solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media(min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#fff;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(232, 50, 131, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:1em;height:1em;padding:.25em;color:var(--bs-btn-close-color);background:var(--bs-btn-close-bg)50%/1em no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:.5rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}:root,[data-bs-theme=light]{--bs-btn-close-filter: }[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:#fff;--bs-toast-header-bg:transparent;--bs-toast-header-border-color:rgba(255, 255, 255, 0.2);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width)solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y)var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width)solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:2rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:2rem;--bs-modal-header-padding-y:2rem;--bs-modal-header-padding:2rem 2rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@media(prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width)solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width)solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5)calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width)solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media(min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media(min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media(min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:1rem;--bs-tooltip-padding-y:0.5rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.7;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height)calc(var(--bs-tooltip-arrow-width) * .5)0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5)var(--bs-tooltip-arrow-height)calc(var(--bs-tooltip-arrow-width) * .5)0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5)var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5)0 calc(var(--bs-tooltip-arrow-width) * .5)var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y)var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:2rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:transparent;--bs-popover-body-padding-x:2rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:#fff;--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width)solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height)calc(var(--bs-popover-arrow-width) * .5)0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5)var(--bs-popover-arrow-height)calc(var(--bs-popover-arrow-width) * .5)0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5)var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width)solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5)0 calc(var(--bs-popover-arrow-width) * .5)var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y)var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width)solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y)var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media(prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;filter:var(--bs-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:initial;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:var(--bs-carousel-indicator-active-bg);background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media(prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#fff;--bs-carousel-caption-color:#fff;--bs-carousel-control-icon-filter: }[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;flex-shrink:0;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed)linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width)solid;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media(prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:2rem;--bs-offcanvas-padding-y:2rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media(max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media(max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media(min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}@media(max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media(max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media(min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}@media(max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media(max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media(min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}@media(max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media(max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media(min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}@media(max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media(max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media(max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media(min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:initial!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:initial!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media(prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width)solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y)var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5)calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y)var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#000!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#fff!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#fff!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(186,40,105,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(186,40,105,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(186,40,105,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(255,255,255,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,255,255,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,255,255,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(52,172,134,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(52,172,134,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(52,172,134,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(46,162,201,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(46,162,201,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(46,162,201,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(204,154,6,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(204,154,6,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(204,154,6,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(202,101,16,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(202,101,16,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(202,101,16,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(237,237,237,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(237,237,237,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(237,237,237,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0)var(--bs-focus-ring-y,0)var(--bs-focus-ring-blur,0)var(--bs-focus-ring-width)var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,.5));text-underline-offset:.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media(prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media(min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media(min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media(min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media(min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media(min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.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}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!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}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-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}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width)var(--bs-border-style)var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-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}.justify-content-evenly{justify-content:space-evenly!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-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-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}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.5rem!important}.m-2{margin:1rem!important}.m-3{margin:2rem!important}.m-4{margin:3rem!important}.m-5{margin:6rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-2{margin-right:1rem!important;margin-left:1rem!important}.mx-3{margin-right:2rem!important;margin-left:2rem!important}.mx-4{margin-right:3rem!important;margin-left:3rem!important}.mx-5{margin-right:6rem!important;margin-left:6rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.5rem!important}.mt-2{margin-top:1rem!important}.mt-3{margin-top:2rem!important}.mt-4{margin-top:3rem!important}.mt-5{margin-top:6rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.5rem!important}.me-2{margin-right:1rem!important}.me-3{margin-right:2rem!important}.me-4{margin-right:3rem!important}.me-5{margin-right:6rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.5rem!important}.mb-2{margin-bottom:1rem!important}.mb-3{margin-bottom:2rem!important}.mb-4{margin-bottom:3rem!important}.mb-5{margin-bottom:6rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.5rem!important}.ms-2{margin-left:1rem!important}.ms-3{margin-left:2rem!important}.ms-4{margin-left:3rem!important}.ms-5{margin-left:6rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.5rem!important}.p-2{padding:1rem!important}.p-3{padding:2rem!important}.p-4{padding:3rem!important}.p-5{padding:6rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-2{padding-right:1rem!important;padding-left:1rem!important}.px-3{padding-right:2rem!important;padding-left:2rem!important}.px-4{padding-right:3rem!important;padding-left:3rem!important}.px-5{padding-right:6rem!important;padding-left:6rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.5rem!important}.pt-2{padding-top:1rem!important}.pt-3{padding-top:2rem!important}.pt-4{padding-top:3rem!important}.pt-5{padding-top:6rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.5rem!important}.pe-2{padding-right:1rem!important}.pe-3{padding-right:2rem!important}.pe-4{padding-right:3rem!important}.pe-5{padding-right:6rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.5rem!important}.pb-2{padding-bottom:1rem!important}.pb-3{padding-bottom:2rem!important}.pb-4{padding-bottom:3rem!important}.pb-5{padding-bottom:6rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.5rem!important}.ps-2{padding-left:1rem!important}.ps-3{padding-left:2rem!important}.ps-4{padding-left:3rem!important}.ps-5{padding-left:6rem!important}.gap-0{gap:0!important}.gap-1{gap:.5rem!important}.gap-2{gap:1rem!important}.gap-3{gap:2rem!important}.gap-4{gap:3rem!important}.gap-5{gap:6rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.5rem!important}.row-gap-2{row-gap:1rem!important}.row-gap-3{row-gap:2rem!important}.row-gap-4{row-gap:3rem!important}.row-gap-5{row-gap:6rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!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-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:.125em!important}.link-offset-1-hover:hover{text-underline-offset:.125em!important}.link-offset-2{text-underline-offset:.25em!important}.link-offset-2-hover:hover{text-underline-offset:.25em!important}.link-offset-3{text-underline-offset:.375em!important}.link-offset-3-hover:hover{text-underline-offset:.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:initial!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media(min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.5rem!important}.m-sm-2{margin:1rem!important}.m-sm-3{margin:2rem!important}.m-sm-4{margin:3rem!important}.m-sm-5{margin:6rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-2{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-3{margin-right:2rem!important;margin-left:2rem!important}.mx-sm-4{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-5{margin-right:6rem!important;margin-left:6rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-sm-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.5rem!important}.mt-sm-2{margin-top:1rem!important}.mt-sm-3{margin-top:2rem!important}.mt-sm-4{margin-top:3rem!important}.mt-sm-5{margin-top:6rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.5rem!important}.me-sm-2{margin-right:1rem!important}.me-sm-3{margin-right:2rem!important}.me-sm-4{margin-right:3rem!important}.me-sm-5{margin-right:6rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.5rem!important}.mb-sm-2{margin-bottom:1rem!important}.mb-sm-3{margin-bottom:2rem!important}.mb-sm-4{margin-bottom:3rem!important}.mb-sm-5{margin-bottom:6rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.5rem!important}.ms-sm-2{margin-left:1rem!important}.ms-sm-3{margin-left:2rem!important}.ms-sm-4{margin-left:3rem!important}.ms-sm-5{margin-left:6rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.5rem!important}.p-sm-2{padding:1rem!important}.p-sm-3{padding:2rem!important}.p-sm-4{padding:3rem!important}.p-sm-5{padding:6rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-2{padding-right:1rem!important;padding-left:1rem!important}.px-sm-3{padding-right:2rem!important;padding-left:2rem!important}.px-sm-4{padding-right:3rem!important;padding-left:3rem!important}.px-sm-5{padding-right:6rem!important;padding-left:6rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-sm-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-sm-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.5rem!important}.pt-sm-2{padding-top:1rem!important}.pt-sm-3{padding-top:2rem!important}.pt-sm-4{padding-top:3rem!important}.pt-sm-5{padding-top:6rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.5rem!important}.pe-sm-2{padding-right:1rem!important}.pe-sm-3{padding-right:2rem!important}.pe-sm-4{padding-right:3rem!important}.pe-sm-5{padding-right:6rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.5rem!important}.pb-sm-2{padding-bottom:1rem!important}.pb-sm-3{padding-bottom:2rem!important}.pb-sm-4{padding-bottom:3rem!important}.pb-sm-5{padding-bottom:6rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.5rem!important}.ps-sm-2{padding-left:1rem!important}.ps-sm-3{padding-left:2rem!important}.ps-sm-4{padding-left:3rem!important}.ps-sm-5{padding-left:6rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.5rem!important}.gap-sm-2{gap:1rem!important}.gap-sm-3{gap:2rem!important}.gap-sm-4{gap:3rem!important}.gap-sm-5{gap:6rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.5rem!important}.row-gap-sm-2{row-gap:1rem!important}.row-gap-sm-3{row-gap:2rem!important}.row-gap-sm-4{row-gap:3rem!important}.row-gap-sm-5{row-gap:6rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-sm-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-sm-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-sm-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media(min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.5rem!important}.m-md-2{margin:1rem!important}.m-md-3{margin:2rem!important}.m-md-4{margin:3rem!important}.m-md-5{margin:6rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-2{margin-right:1rem!important;margin-left:1rem!important}.mx-md-3{margin-right:2rem!important;margin-left:2rem!important}.mx-md-4{margin-right:3rem!important;margin-left:3rem!important}.mx-md-5{margin-right:6rem!important;margin-left:6rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-md-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.5rem!important}.mt-md-2{margin-top:1rem!important}.mt-md-3{margin-top:2rem!important}.mt-md-4{margin-top:3rem!important}.mt-md-5{margin-top:6rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.5rem!important}.me-md-2{margin-right:1rem!important}.me-md-3{margin-right:2rem!important}.me-md-4{margin-right:3rem!important}.me-md-5{margin-right:6rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.5rem!important}.mb-md-2{margin-bottom:1rem!important}.mb-md-3{margin-bottom:2rem!important}.mb-md-4{margin-bottom:3rem!important}.mb-md-5{margin-bottom:6rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.5rem!important}.ms-md-2{margin-left:1rem!important}.ms-md-3{margin-left:2rem!important}.ms-md-4{margin-left:3rem!important}.ms-md-5{margin-left:6rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.5rem!important}.p-md-2{padding:1rem!important}.p-md-3{padding:2rem!important}.p-md-4{padding:3rem!important}.p-md-5{padding:6rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-2{padding-right:1rem!important;padding-left:1rem!important}.px-md-3{padding-right:2rem!important;padding-left:2rem!important}.px-md-4{padding-right:3rem!important;padding-left:3rem!important}.px-md-5{padding-right:6rem!important;padding-left:6rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-md-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-md-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.5rem!important}.pt-md-2{padding-top:1rem!important}.pt-md-3{padding-top:2rem!important}.pt-md-4{padding-top:3rem!important}.pt-md-5{padding-top:6rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.5rem!important}.pe-md-2{padding-right:1rem!important}.pe-md-3{padding-right:2rem!important}.pe-md-4{padding-right:3rem!important}.pe-md-5{padding-right:6rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.5rem!important}.pb-md-2{padding-bottom:1rem!important}.pb-md-3{padding-bottom:2rem!important}.pb-md-4{padding-bottom:3rem!important}.pb-md-5{padding-bottom:6rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.5rem!important}.ps-md-2{padding-left:1rem!important}.ps-md-3{padding-left:2rem!important}.ps-md-4{padding-left:3rem!important}.ps-md-5{padding-left:6rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.5rem!important}.gap-md-2{gap:1rem!important}.gap-md-3{gap:2rem!important}.gap-md-4{gap:3rem!important}.gap-md-5{gap:6rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.5rem!important}.row-gap-md-2{row-gap:1rem!important}.row-gap-md-3{row-gap:2rem!important}.row-gap-md-4{row-gap:3rem!important}.row-gap-md-5{row-gap:6rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-md-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-md-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-md-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media(min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.5rem!important}.m-lg-2{margin:1rem!important}.m-lg-3{margin:2rem!important}.m-lg-4{margin:3rem!important}.m-lg-5{margin:6rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-2{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-3{margin-right:2rem!important;margin-left:2rem!important}.mx-lg-4{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-5{margin-right:6rem!important;margin-left:6rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-lg-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.5rem!important}.mt-lg-2{margin-top:1rem!important}.mt-lg-3{margin-top:2rem!important}.mt-lg-4{margin-top:3rem!important}.mt-lg-5{margin-top:6rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.5rem!important}.me-lg-2{margin-right:1rem!important}.me-lg-3{margin-right:2rem!important}.me-lg-4{margin-right:3rem!important}.me-lg-5{margin-right:6rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.5rem!important}.mb-lg-2{margin-bottom:1rem!important}.mb-lg-3{margin-bottom:2rem!important}.mb-lg-4{margin-bottom:3rem!important}.mb-lg-5{margin-bottom:6rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.5rem!important}.ms-lg-2{margin-left:1rem!important}.ms-lg-3{margin-left:2rem!important}.ms-lg-4{margin-left:3rem!important}.ms-lg-5{margin-left:6rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.5rem!important}.p-lg-2{padding:1rem!important}.p-lg-3{padding:2rem!important}.p-lg-4{padding:3rem!important}.p-lg-5{padding:6rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-2{padding-right:1rem!important;padding-left:1rem!important}.px-lg-3{padding-right:2rem!important;padding-left:2rem!important}.px-lg-4{padding-right:3rem!important;padding-left:3rem!important}.px-lg-5{padding-right:6rem!important;padding-left:6rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-lg-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-lg-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.5rem!important}.pt-lg-2{padding-top:1rem!important}.pt-lg-3{padding-top:2rem!important}.pt-lg-4{padding-top:3rem!important}.pt-lg-5{padding-top:6rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.5rem!important}.pe-lg-2{padding-right:1rem!important}.pe-lg-3{padding-right:2rem!important}.pe-lg-4{padding-right:3rem!important}.pe-lg-5{padding-right:6rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.5rem!important}.pb-lg-2{padding-bottom:1rem!important}.pb-lg-3{padding-bottom:2rem!important}.pb-lg-4{padding-bottom:3rem!important}.pb-lg-5{padding-bottom:6rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.5rem!important}.ps-lg-2{padding-left:1rem!important}.ps-lg-3{padding-left:2rem!important}.ps-lg-4{padding-left:3rem!important}.ps-lg-5{padding-left:6rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.5rem!important}.gap-lg-2{gap:1rem!important}.gap-lg-3{gap:2rem!important}.gap-lg-4{gap:3rem!important}.gap-lg-5{gap:6rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.5rem!important}.row-gap-lg-2{row-gap:1rem!important}.row-gap-lg-3{row-gap:2rem!important}.row-gap-lg-4{row-gap:3rem!important}.row-gap-lg-5{row-gap:6rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-lg-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-lg-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-lg-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media(min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.5rem!important}.m-xl-2{margin:1rem!important}.m-xl-3{margin:2rem!important}.m-xl-4{margin:3rem!important}.m-xl-5{margin:6rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-2{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-3{margin-right:2rem!important;margin-left:2rem!important}.mx-xl-4{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-5{margin-right:6rem!important;margin-left:6rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-xl-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.5rem!important}.mt-xl-2{margin-top:1rem!important}.mt-xl-3{margin-top:2rem!important}.mt-xl-4{margin-top:3rem!important}.mt-xl-5{margin-top:6rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.5rem!important}.me-xl-2{margin-right:1rem!important}.me-xl-3{margin-right:2rem!important}.me-xl-4{margin-right:3rem!important}.me-xl-5{margin-right:6rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.5rem!important}.mb-xl-2{margin-bottom:1rem!important}.mb-xl-3{margin-bottom:2rem!important}.mb-xl-4{margin-bottom:3rem!important}.mb-xl-5{margin-bottom:6rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.5rem!important}.ms-xl-2{margin-left:1rem!important}.ms-xl-3{margin-left:2rem!important}.ms-xl-4{margin-left:3rem!important}.ms-xl-5{margin-left:6rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.5rem!important}.p-xl-2{padding:1rem!important}.p-xl-3{padding:2rem!important}.p-xl-4{padding:3rem!important}.p-xl-5{padding:6rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-2{padding-right:1rem!important;padding-left:1rem!important}.px-xl-3{padding-right:2rem!important;padding-left:2rem!important}.px-xl-4{padding-right:3rem!important;padding-left:3rem!important}.px-xl-5{padding-right:6rem!important;padding-left:6rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-xl-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-xl-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.5rem!important}.pt-xl-2{padding-top:1rem!important}.pt-xl-3{padding-top:2rem!important}.pt-xl-4{padding-top:3rem!important}.pt-xl-5{padding-top:6rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.5rem!important}.pe-xl-2{padding-right:1rem!important}.pe-xl-3{padding-right:2rem!important}.pe-xl-4{padding-right:3rem!important}.pe-xl-5{padding-right:6rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.5rem!important}.pb-xl-2{padding-bottom:1rem!important}.pb-xl-3{padding-bottom:2rem!important}.pb-xl-4{padding-bottom:3rem!important}.pb-xl-5{padding-bottom:6rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.5rem!important}.ps-xl-2{padding-left:1rem!important}.ps-xl-3{padding-left:2rem!important}.ps-xl-4{padding-left:3rem!important}.ps-xl-5{padding-left:6rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.5rem!important}.gap-xl-2{gap:1rem!important}.gap-xl-3{gap:2rem!important}.gap-xl-4{gap:3rem!important}.gap-xl-5{gap:6rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.5rem!important}.row-gap-xl-2{row-gap:1rem!important}.row-gap-xl-3{row-gap:2rem!important}.row-gap-xl-4{row-gap:3rem!important}.row-gap-xl-5{row-gap:6rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-xl-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-xl-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-xl-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media(min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.5rem!important}.m-xxl-2{margin:1rem!important}.m-xxl-3{margin:2rem!important}.m-xxl-4{margin:3rem!important}.m-xxl-5{margin:6rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-2{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-3{margin-right:2rem!important;margin-left:2rem!important}.mx-xxl-4{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-5{margin-right:6rem!important;margin-left:6rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-2{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-3{margin-top:2rem!important;margin-bottom:2rem!important}.my-xxl-4{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-5{margin-top:6rem!important;margin-bottom:6rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.5rem!important}.mt-xxl-2{margin-top:1rem!important}.mt-xxl-3{margin-top:2rem!important}.mt-xxl-4{margin-top:3rem!important}.mt-xxl-5{margin-top:6rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.5rem!important}.me-xxl-2{margin-right:1rem!important}.me-xxl-3{margin-right:2rem!important}.me-xxl-4{margin-right:3rem!important}.me-xxl-5{margin-right:6rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.5rem!important}.mb-xxl-2{margin-bottom:1rem!important}.mb-xxl-3{margin-bottom:2rem!important}.mb-xxl-4{margin-bottom:3rem!important}.mb-xxl-5{margin-bottom:6rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.5rem!important}.ms-xxl-2{margin-left:1rem!important}.ms-xxl-3{margin-left:2rem!important}.ms-xxl-4{margin-left:3rem!important}.ms-xxl-5{margin-left:6rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.5rem!important}.p-xxl-2{padding:1rem!important}.p-xxl-3{padding:2rem!important}.p-xxl-4{padding:3rem!important}.p-xxl-5{padding:6rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-2{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-3{padding-right:2rem!important;padding-left:2rem!important}.px-xxl-4{padding-right:3rem!important;padding-left:3rem!important}.px-xxl-5{padding-right:6rem!important;padding-left:6rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-2{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-3{padding-top:2rem!important;padding-bottom:2rem!important}.py-xxl-4{padding-top:3rem!important;padding-bottom:3rem!important}.py-xxl-5{padding-top:6rem!important;padding-bottom:6rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.5rem!important}.pt-xxl-2{padding-top:1rem!important}.pt-xxl-3{padding-top:2rem!important}.pt-xxl-4{padding-top:3rem!important}.pt-xxl-5{padding-top:6rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.5rem!important}.pe-xxl-2{padding-right:1rem!important}.pe-xxl-3{padding-right:2rem!important}.pe-xxl-4{padding-right:3rem!important}.pe-xxl-5{padding-right:6rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.5rem!important}.pb-xxl-2{padding-bottom:1rem!important}.pb-xxl-3{padding-bottom:2rem!important}.pb-xxl-4{padding-bottom:3rem!important}.pb-xxl-5{padding-bottom:6rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.5rem!important}.ps-xxl-2{padding-left:1rem!important}.ps-xxl-3{padding-left:2rem!important}.ps-xxl-4{padding-left:3rem!important}.ps-xxl-5{padding-left:6rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.5rem!important}.gap-xxl-2{gap:1rem!important}.gap-xxl-3{gap:2rem!important}.gap-xxl-4{gap:3rem!important}.gap-xxl-5{gap:6rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.5rem!important}.row-gap-xxl-2{row-gap:1rem!important}.row-gap-xxl-3{row-gap:2rem!important}.row-gap-xxl-4{row-gap:3rem!important}.row-gap-xxl-5{row-gap:6rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:.5rem!important;column-gap:.5rem!important}.column-gap-xxl-2{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-3{-moz-column-gap:2rem!important;column-gap:2rem!important}.column-gap-xxl-4{-moz-column-gap:3rem!important;column-gap:3rem!important}.column-gap-xxl-5{-moz-column-gap:6rem!important;column-gap:6rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media(min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}body{background-image:linear-gradient(90deg,#21373f,#5e62b0,#700936)}[data-bs-theme=dark] body{color:#fff}.table-secondary{--bs-table-hover-color:$white}.btn-secondary{color:#fff;border:none}.btn-secondary:focus,.btn-secondary:hover{color:#fff}.btn-secondary.disabled{color:#fff}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:0}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:0}.bg-light .btn{background-color:rgba(0,0,0,.2)}.form-control::-webkit-file-upload-button{background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);border:none}.form-control::file-selector-button,.input-group-text{background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);border:none}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:rgba(255,255,255,.1)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:rgba(255,255,255,.1)}.input-group-text{box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1)}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:0;border-left:none}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-right:none}.form-select:not([multiple]){transition:border-color .15s ease-in-out}.form-select:not([multiple]) option{color:#000}.bg-light .form-control{color:#343a40;border-color:rgba(0,0,0,.2)}.bg-light .form-control::-moz-placeholder{color:rgba(0,0,0,.2)}.bg-light .form-control::placeholder{color:rgba(0,0,0,.2)}.dropdown-menu{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.dropdown-menu a{color:#fff}.dropdown-menu .text-muted{color:rgba(255,255,255,.7)!important}.nav-tabs .nav-link{transition:none}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{color:#fff;isolation:auto}.nav-tabs .nav-link,.nav-tabs .nav-link.disabled{background-color:rgba(0,0,0,.2)}.nav-tabs .nav-item:first-child>.nav-link{border-radius:.5rem 0 0 .5rem}.nav-tabs .nav-item:last-child>.nav-link{border-radius:0 .5rem .5rem 0}.nav-tabs .nav-item .nav-link.active,.nav-tabs .nav-item.show .nav-link{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,#fff,rgba(255,255,255,.9) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);border-radius:.5rem;transform:scale(1.1)}.nav-tabs .nav-item .nav-link.active a,.nav-tabs .nav-item.show .nav-link a{color:#fff}.nav-tabs .nav-item .nav-link.active .text-muted,.nav-tabs .nav-item.show .nav-link .text-muted{color:rgba(255,255,255,.7)!important}.nav-tabs .nav-item .nav-link.active:focus,.nav-tabs .nav-item .nav-link.active:hover,.nav-tabs .nav-item.show .nav-link:focus,.nav-tabs .nav-item.show .nav-link:hover{color:#343a40}.nav-tabs .dropdown-menu{border-radius:.5rem}.nav-pills .nav-link{transition:none}.nav-pills .nav-link:focus,.nav-pills .nav-link:hover{color:#fff;isolation:auto}.alert{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);position:relative;overflow:hidden;color:#fff}.alert a{color:#fff}.alert .text-muted{color:rgba(255,255,255,.7)!important}.alert-primary::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#e83283}.alert-secondary::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:rgba(255,255,255,.4)}.alert-success::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#41d7a7}.alert-info::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#39cbfb}.alert-warning::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#ffc107}.alert-danger::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#fd7e14}.alert-light::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#e9e9e8}.alert-dark::after{position:absolute;top:0;left:0;width:.5rem;height:100%;content:"";background-color:#212529}.alert .alert-link{color:#fff}.badge.bg-secondary{--bs-bg-opacity:.4}.badge.bg-light{color:#343a40}.list-group{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.list-group a{color:#fff}.list-group .text-muted{color:rgba(255,255,255,.7)!important}.card{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);--bs-secondary-rgb:transparent}.card a{color:#fff}.card .text-muted{color:rgba(255,255,255,.7)!important}.card.bg-primary{background-image:none}.card.bg-success{background-image:none}.card.bg-info{background-image:none}.card.bg-warning{background-image:none}.card.bg-danger{background-image:none}.card.bg-light{background-image:none}.card.bg-dark{background-image:none}.card.bg-light .card-body,.card.bg-light .card-header{color:#343a40}.card.border-primary{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-primary a{color:#fff}.card.border-primary .text-muted{color:rgba(255,255,255,.7)!important}.card.border-primary .card-header{background-color:#e83283!important;border-bottom:none}.card.border-secondary{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-secondary a{color:#fff}.card.border-secondary .text-muted{color:rgba(255,255,255,.7)!important}.card.border-secondary .card-header{background-color:rgba(255,255,255,.4)!important;border-bottom:none}.card.border-success{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-success a{color:#fff}.card.border-success .text-muted{color:rgba(255,255,255,.7)!important}.card.border-success .card-header{background-color:#41d7a7!important;border-bottom:none}.card.border-info{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-info a{color:#fff}.card.border-info .text-muted{color:rgba(255,255,255,.7)!important}.card.border-info .card-header{background-color:#39cbfb!important;border-bottom:none}.card.border-warning{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-warning a{color:#fff}.card.border-warning .text-muted{color:rgba(255,255,255,.7)!important}.card.border-warning .card-header{background-color:#ffc107!important;border-bottom:none}.card.border-danger{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-danger a{color:#fff}.card.border-danger .text-muted{color:rgba(255,255,255,.7)!important}.card.border-danger .card-header{background-color:#fd7e14!important;border-bottom:none}.card.border-light{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-light a{color:#fff}.card.border-light .text-muted{color:rgba(255,255,255,.7)!important}.card.border-light .card-header{background-color:#e9e9e8!important;border-bottom:none;color:#343a40}.card.border-dark{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.card.border-dark a{color:#fff}.card.border-dark .text-muted{color:rgba(255,255,255,.7)!important}.card.border-dark .card-header{background-color:#212529!important;border-bottom:none}.card-header{font-weight:700}.accordion-item{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);box-shadow:none}.accordion-item a{color:#fff}.accordion-item .text-muted{color:rgba(255,255,255,.7)!important}.toast{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.toast a{color:#fff}.toast .text-muted{color:rgba(255,255,255,.7)!important}.popover{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.popover a{color:#fff}.popover .text-muted{color:rgba(255,255,255,.7)!important}.popover-header{border-bottom-color:rgba(255,255,255,.2)}.tooltip{-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.modal-content{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.modal-content a{color:#fff}.modal-content .text-muted{color:rgba(255,255,255,.7)!important}.offcanvas{border:none;box-shadow:inset 1px 1px rgba(255,255,255,.2),inset -1px -1px rgba(255,255,255,.1),1px 3px 24px -1px rgba(0,0,0,.15);background-color:initial;background-image:linear-gradient(125deg,rgba(255,255,255,.3),rgba(255,255,255,.2) 70%);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.offcanvas a{color:#fff}.offcanvas .text-muted{color:rgba(255,255,255,.7)!important}/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/src/Yavsc.Server/Directory.Packages.props b/src/Yavsc.Server/Directory.Packages.props deleted file mode 100644 index 661f437f0..000000000 --- a/src/Yavsc.Server/Directory.Packages.props +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs b/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs deleted file mode 100644 index 29062bc3c..000000000 --- a/src/Yavsc.Server/Helpers/EstimateSignatureFileHelper.cs +++ /dev/null @@ -1,158 +0,0 @@ -#nullable enable annotations - -using System.Security.Claims; -using System.Text; -using System.Text.Json; -using Microsoft.AspNetCore.Http; -using Yavsc.Models.Billing; -using Yavsc.Server.Models.FileSystem; -namespace Yavsc.Server.Helpers; - -/// -/// Filesystem counterpart of : writes -/// the wire-format JSON payload next to the user's other files, -/// under signatures/, and updates the user's disk quota. -/// -/// This is the JSON counterpart of the legacy -/// ReceiveProSignatureAsync method, which stored -/// sign-{billingCode}-{signType}-{estimateId}.png blobs. -/// We don't reuse that helper because (a) the wire format is no -/// longer a binary image, (b) there's no billingCode on -/// a freshly signed estimate in our model, and (c) the legacy -/// helper takes an whereas our pipeline -/// decodes a JSON body upstream of the controller and passes -/// in directly. -/// -public static class EstimateSignatureFileHelper -{ - /// - /// Sub-directory under the user's root where signature - /// payloads live. Kept short to leave room in PATH_MAX on - /// legacy filesystems; the rest of the filename is - /// sign-{type}-{estimateId}-{utcTicks}.json. - /// - public const string SignaturesSubdir = "signatures"; - - /// - /// Format string for signature file names. Public so the - /// migration and the admin tools can list by pattern. - /// - public static string FileNameFormat(SignatureType type, long estimateId, long utcTicks) - => $"sign-{type.ToString().ToLowerInvariant()}-{estimateId}-{utcTicks}.json"; - - /// - /// Persist a signature wire payload to disk. Returns the - /// file info (relative path under the user's root) suitable - /// for storing in ; the - /// caller is responsible for the database write. - /// - /// Signed-in user. Their - /// Identity.Name locates the disk root via - /// . - /// - /// Estimate this signature - /// attaches to. Used in the file name for human inspection - /// and to support multiple versions over time. - /// Provider or client signature. - /// Decoded wire payload (strokes + - /// coordinateMax + capturedAtUtc). Already validated - /// upstream. - /// Cancellation token forwarded to - /// the file write. - public static async Task ReceiveEstimateSignatureAsync( - this ClaimsPrincipal user, - long estimateId, - SignatureType type, - SignaturePadPayload payload, - SiteSettings siteSettings, - CancellationToken token = default) - { - if (user is null) throw new ArgumentNullException(nameof(user)); - if (payload is null) throw new ArgumentNullException(nameof(payload)); - if (estimateId <= 0) throw new ArgumentOutOfRangeException(nameof(estimateId)); - - // Ensure the user has a /signatures/ sub-directory we can - // write to. EnsureDestinationDirectory throws on invalid - // paths and creates the directory on the way; the - // SignaturesSubdir constant is a server-controlled value - // (not user-derived), so we skip the IsValidYavscPath - // check that ReceiveUserFile performs on user-supplied - // subpaths. - var root = user.EnsureDestinationDirectory(SignaturesSubdir, siteSettings); - - var fileName = FileNameFormat(type, estimateId, DateTime.UtcNow.Ticks); - var fullPath = Path.Combine(root, fileName); - - var envelope = new - { - format = "yavsc.signature/v1", - coordinateMax = payload.CoordinateMax, - capturedAtUtc = payload.CapturedAtUtc, - estimateId, - type = type.ToString(), - // Identity.Name is the username; we keep the wire - // payload keyed on the username rather than the - // numeric/guid Id so disk-side human inspection - // (e.g. cat sign-pro-1234-...json) is self-evident. - signerName = user.Identity?.Name, - strokes = payload.Strokes, - strokeCount = CountStrokes(payload.Strokes), - }; - - var json = JsonSerializer.Serialize(envelope, new JsonSerializerOptions { WriteIndented = true }); - await File.WriteAllTextAsync(fullPath, json, Encoding.UTF8, token).ConfigureAwait(false); - - // Quota update is the controller's responsibility: the - // helper has no DbContext access, and a ClaimsPrincipal - // is not an ApplicationUser. The controller looks up - // the user by Identity.Name and bumps DiskUsage after - // a successful database write. - - return new FileReceivedInfo(root, fileName); - } - - private static int CountStrokes(int[] strokes) - { - int n = 0; - for (int i = 0; i < strokes.Length;) - { - int k = strokes[i]; - if (k <= 0) break; - n++; - i += 1 + 2 * k; - } - return n; - } -} - -/// -/// Wire payload accepted by the signature endpoint and -/// persisted by . -/// Mirrors PostIt.Models.SignaturePadData's JSON shape -/// (without the disk-only envelope fields) so the two sides -/// stay trivially compatible. -/// -public class SignaturePadPayload -{ - /// - /// Normalised coordinate upper bound. Must be - /// PostIt.Models.SignaturePadData.CoordinateMax - /// (10_000) today; declared as a property so a future - /// resolution change can be replayed against the same - /// wire format. - /// - public int CoordinateMax { get; set; } = 10_000; - - /// - /// Client-reported capture time. The server may ignore - /// this for ordering (UTC now is the truth) but keeps it - /// for round-trip display. - /// - public DateTime CapturedAtUtc { get; set; } - - /// - /// Wire strokes. See - /// PostIt.Models.SignaturePadData for the format. - /// - public int[] Strokes { get; set; } = Array.Empty(); -} diff --git a/src/Yavsc.Server/Helpers/EstimateSignaturePersister.cs b/src/Yavsc.Server/Helpers/EstimateSignaturePersister.cs new file mode 100644 index 000000000..be8848e11 --- /dev/null +++ b/src/Yavsc.Server/Helpers/EstimateSignaturePersister.cs @@ -0,0 +1,111 @@ +#nullable enable annotations + +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using Yavsc.Models; +using Yavsc.Models.Billing; + +namespace Yavsc.Server.Helpers; + +/// +/// Shared signature-persistence logic for estimates, extracted from +/// BillingController.Sign. Stages the +/// row (find-or-add by (EstimateId, Type)) for the PostIt +/// wire-format payload — but does not call +/// SaveChanges. The caller owns the transaction so it can +/// combine the signature write with its own changes (e.g. setting a +/// validation date or a query status) in a single +/// SaveChanges. +/// +/// +/// The column is the source of +/// truth; there is no on-disk copy, so no disk-quota accounting is +/// involved. +/// +/// +public static class EstimateSignaturePersister +{ + /// + /// Stage a signature for signed by + /// as . Upserts + /// the row for the + /// (EstimateId, Type) pair, writing the wire payload + /// (Strokes, CoordinateMax, CapturedAtUtc) + /// and . Returns the staged + /// ; the caller persists with + /// SaveChanges. + /// + public static async Task StageAsync( + ApplicationDbContext db, + long estimateId, + SignatureType type, + string signerId, + SignaturePadPayload payload, + CancellationToken token = default) + { + if (db is null) throw new ArgumentNullException(nameof(db)); + if (payload is null) throw new ArgumentNullException(nameof(payload)); + if (estimateId <= 0) throw new ArgumentOutOfRangeException(nameof(estimateId)); + + // Find-or-add: the (EstimateId, Type) pair is unique, so a + // second submission for the same side of the estimate + // replaces the previous signature. EF translates this into a + // single UPDATE when the row exists and an INSERT otherwise; + // the unique index in ApplicationDbContext is the + // database-level guarantee that the contract holds if two + // requests race. + var signature = await db.Signatures + .FirstOrDefaultAsync(s => s.EstimateId == estimateId && s.Type == type, token) + .ConfigureAwait(false); + + if (signature is null) + { + signature = new Signature + { + EstimateId = estimateId, + SignerId = signerId, + Type = type, + }; + db.Signatures.Add(signature); + } + + signature.SignerId = signerId; + signature.CoordinateMax = payload.CoordinateMax; + signature.Strokes = payload.Strokes; + signature.CapturedAtUtc = payload.CapturedAtUtc; + + return signature; + } +} + +/// +/// Wire payload accepted by the signature path and staged by +/// . Mirrors +/// PostIt.Models.SignaturePadData's JSON shape (without the +/// disk-only envelope fields) so the two sides stay trivially +/// compatible. +/// +public class SignaturePadPayload +{ + /// + /// Normalised coordinate upper bound. Must be + /// PostIt.Models.SignaturePadData.CoordinateMax + /// (10_000) today; declared as a property so a future + /// resolution change can be replayed against the same + /// wire format. + /// + public int CoordinateMax { get; set; } = 10_000; + + /// + /// Client-reported capture time. The server may ignore + /// this for ordering (UTC now is the truth) but keeps it + /// for round-trip display. + /// + public DateTime CapturedAtUtc { get; set; } + + /// + /// Wire strokes. See + /// PostIt.Models.SignaturePadData for the format. + /// + public int[] Strokes { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/Yavsc.Server/Helpers/FileSystemHelpers.cs b/src/Yavsc.Server/Helpers/FileSystemHelpers.cs index bc7b9c81d..e0d5b8b9d 100644 --- a/src/Yavsc.Server/Helpers/FileSystemHelpers.cs +++ b/src/Yavsc.Server/Helpers/FileSystemHelpers.cs @@ -1,19 +1,17 @@ #nullable enable annotations - - - using System.Security.Claims; using Microsoft.AspNetCore.Html; using Microsoft.Extensions.FileProviders; +using Microsoft.AspNetCore.Http; +using ImageMagick; using Yavsc.Models; using Yavsc.Models.Streaming; using Yavsc.ViewModels; -using Microsoft.AspNetCore.Http; using Yavsc.Exceptions; using Yavsc.Abstract.Helpers; -using ImageMagick; using Yavsc.Server.Models.FileSystem; using Yavsc.ViewModels.UserFiles; + namespace Yavsc.Server.Helpers { public static class FileSystemHelpers @@ -34,7 +32,7 @@ namespace Yavsc.Server.Helpers { var item = new FileReceivedInfo( settings.Bills, - AbstractFileSystemHelpers.SignFileNameFormat("pro", billingCode, estimateId)); + AbstractFileSystemHelpers.SignFileNameFormat(signType, billingCode, estimateId)); var fi = new FileInfo(item.FullName); if (fi.Exists) item.Overridden = true; @@ -46,7 +44,7 @@ namespace Yavsc.Server.Helpers this ClaimsPrincipal user, string subpath, SiteSettings siteSettings) { - var root = Path.Combine(siteSettings.Blog, user.Identity.Name); + var root = Path.Combine(siteSettings.Blog, user.GetUserName()); if (!string.IsNullOrWhiteSpace(subpath)) { if (!subpath.IsValidYavscPath()) @@ -72,8 +70,12 @@ namespace Yavsc.Server.Helpers var root = Path.Combine(siteSettings.Blog, user.UserName); var fi = new FileInfo(Path.Combine(root, fileName)); if (!fi.Exists) return; + // Capture the length BEFORE deleting: FileInfo.Length throws + // FileNotFoundException once the file is gone, which would + // surface as a 400 even though the delete already succeeded. + var length = fi.Length; fi.Delete(); - user.DiskUsage -= fi.Length; + user.DiskUsage -= length; } public static FsOperationInfo DeleteUserDirOrFile(this ApplicationUser user, string dirName, SiteSettings siteSettings) @@ -87,8 +89,15 @@ namespace Yavsc.Server.Helpers { var fi = new FileInfo(Path.Combine(root, dirName)); if (!fi.Exists) return new FsOperationInfo { Done = false, ErrorCode = ErrorCode.NotFound, ErrorMessage = "non existent" }; + // Capture the length BEFORE deleting: FileInfo.Length + // throws FileNotFoundException once the file is gone, so + // reading it after fi.Delete() would throw — the caller's + // catch turns that into a 400 even though the file was + // already removed (the user sees "file does not exist" + // while the delete in fact succeeded). + var length = fi.Length; fi.Delete(); - user.DiskUsage -= fi.Length; + user.DiskUsage -= length; } else { diff --git a/src/Yavsc.Server/Helpers/ServiceExtensions.cs b/src/Yavsc.Server/Helpers/ServiceExtensions.cs index f20d50476..397eada85 100644 --- a/src/Yavsc.Server/Helpers/ServiceExtensions.cs +++ b/src/Yavsc.Server/Helpers/ServiceExtensions.cs @@ -1,5 +1,6 @@ #nullable enable annotations +using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; @@ -107,12 +108,27 @@ public static class ServiceExtensions ValidateAudience = true, ValidAudiences = audiences, RoleClaimType = Constants.RoleClaimType, - NameClaimType = Constants.NameClaimType, + // IdentityServer's ProfileService emits the name claim as + // JwtClaimTypes.Name ("name", short form). MapInboundClaims + // (below) is disabled, so it stays "name" on the principal — + // NameClaimType must therefore be "name" too, or + // User.Identity.Name / GetUserName() resolve to null. The + // previous Constants.NameClaimType (the Microsoft long URI) + // never matched the remapped xmlsoap URI either, so the name + // was unreachable on every bearer host regardless of scope. + NameClaimType = JwtClaimTypes.Name, }; - options.MapInboundClaims = true; + // Keep JWT claim names as-emitted ("sub", "name", …). The default + // inbound map rewrites "name" → xmlsoap name URI and "sub" → + // nameid URI, which neither Constants.NameClaimType (microsoft + // URI) nor GetUserName()'s FindFirstValue("name") could match. + // Roles are already emitted as Constants.RoleClaimType (a long + // URI), so disabling the map leaves IsInMsRole() working. + options.MapInboundClaims = false; options.ClaimsIssuer = authority; options.Audience = audiences[0]; + // Dev: every Yavsc resource service (Yavsc.Api, Yavsc.Blogs, // Yavsc.Org itself) validates JWTs against the OP that runs // on https://localhost:5001 with a self-signed dev cert. diff --git a/src/Yavsc.Server/Helpers/UserHelpers.cs b/src/Yavsc.Server/Helpers/UserHelpers.cs index f0a30995f..4511308be 100644 --- a/src/Yavsc.Server/Helpers/UserHelpers.cs +++ b/src/Yavsc.Server/Helpers/UserHelpers.cs @@ -14,9 +14,9 @@ namespace Yavsc.Server.Helpers if (readerId == null) { var userPosts = dbContext.blogSpotPublications.Include( - b => b.BlogPost - ).Where(x => x.BlogPost.AuthorId == posterId) - .Select(x => x.BlogPost).ToArray(); + b => b.Post + ).Where(x => x.Post.AuthorId == posterId) + .Select(x => x.Post).ToArray(); return userPosts; } else diff --git a/src/Yavsc.Server/Models/ApplicationDbContext.cs b/src/Yavsc.Server/Models/ApplicationDbContext.cs index 76933bcad..85ed8f8d9 100644 --- a/src/Yavsc.Server/Models/ApplicationDbContext.cs +++ b/src/Yavsc.Server/Models/ApplicationDbContext.cs @@ -509,5 +509,9 @@ namespace Yavsc.Models public DbSet ModerationLogs { get; set; } public DbSet NominativeServiceCommands { get; set; } + + public DbSet EstimateAttachedFiles { get; set; } + + public DbSet QueryAttachedFiles { get; set; } } } diff --git a/src/Yavsc.Server/Models/Billing/AttachFileRequest.cs b/src/Yavsc.Server/Models/Billing/AttachFileRequest.cs new file mode 100644 index 000000000..204237bd4 --- /dev/null +++ b/src/Yavsc.Server/Models/Billing/AttachFileRequest.cs @@ -0,0 +1,13 @@ +namespace Yavsc.Models.Billing +{ + /// + /// Corps de requête pour attacher un fichier de l'espace perso à un + /// devis (fournisseur) ou à une demande (client), par référence à son + /// identifiant . + /// + public class AttachFileRequest + { + /// Identifiant du fichier référencé. + public long FileId { get; set; } + } +} \ No newline at end of file diff --git a/src/Yavsc.Server/Models/Billing/Estimate.cs b/src/Yavsc.Server/Models/Billing/Estimate.cs index 74680e7b7..8a26faab9 100644 --- a/src/Yavsc.Server/Models/Billing/Estimate.cs +++ b/src/Yavsc.Server/Models/Billing/Estimate.cs @@ -8,6 +8,7 @@ namespace Yavsc.Models.Billing { using Models.Workflow; using Newtonsoft.Json; + using System.Linq; public partial class Estimate : IEstimate { @@ -78,14 +79,39 @@ namespace Yavsc.Models.Billing public DateTime ClientValidationDate { get; set; } /// - /// All signatures captured against this estimate, in - /// capture order. Multiple versions per (Type, SignerId) - /// are allowed; the controller reads the most recent - /// when asked. See . + /// All signatures captured against this estimate, keyed by + /// . The persister upserts by + /// (EstimateId, Type), so there is at most one row per type. + /// This collection is the persisted shape and stays + /// [JsonIgnore]: the wire payload exposes signatures + /// only through the dedicated and + /// projections, since an + /// estimate has exactly these two signature slots and never + /// more. See . /// [InverseProperty(nameof(Signature.Estimate))] + [JsonIgnore, System.Text.Json.Serialization.JsonIgnore] public virtual ICollection Signatures { get; set; } = new List(); + /// + /// The provider's signature on this estimate, or null when the + /// provider has not signed. Projected from + /// ; not mapped (the table is + /// ). + /// + [NotMapped] + public Signature? SignaturePro + => Signatures?.FirstOrDefault(s => s.Type == SignatureType.Pro); + + /// + /// The client's signature on this estimate, or null when the + /// client has not signed. Projected from + /// ; not mapped. + /// + [NotMapped] + public Signature? SignatureClient + => Signatures?.FirstOrDefault(s => s.Type == SignatureType.Client); + } } diff --git a/src/Yavsc.Server/Models/Billing/EstimateAttachedFile.cs b/src/Yavsc.Server/Models/Billing/EstimateAttachedFile.cs new file mode 100644 index 000000000..c4b7ccf2b --- /dev/null +++ b/src/Yavsc.Server/Models/Billing/EstimateAttachedFile.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using Yavsc.Models.Blog; + +namespace Yavsc.Models.Billing +{ + /// + /// Pièce jointe d'un devis, attachée **par référence** à un fichier + /// de l'espace de stockage personnel du fournisseur + /// (). Le fichier reste dans l'espace perso + /// du fournisseur (pas de duplication) ; seul le lien est persisté. + /// Le téléchargement est servi par le host Blogs, qui autorise + /// l'appelant (fournisseur ou client du devis) via cette table. + /// + [PrimaryKey(nameof(FileId), nameof(EstimateId))] + public class EstimateAttachedFile + { + /// + /// Identifiant du fichier référencé (partie de la clé composite). + /// + public long FileId { get; set; } + + [ForeignKey("FileId")] + public virtual UploadedFile File { get; set; } + + /// + /// Identifiant du devis auquel le fichier est rattaché + /// (partie de la clé composite). + /// + public long EstimateId { get; set; } + + [ForeignKey("EstimateId")] + public virtual Estimate Estimate { get; set; } + } +} \ No newline at end of file diff --git a/src/Yavsc.Server/Models/Billing/QueryAttachedFile.cs b/src/Yavsc.Server/Models/Billing/QueryAttachedFile.cs new file mode 100644 index 000000000..853fe252f --- /dev/null +++ b/src/Yavsc.Server/Models/Billing/QueryAttachedFile.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using Yavsc.Models.Blog; + +namespace Yavsc.Models.Billing +{ + /// + /// Pièce jointe d'une demande (NominativeServiceCommand), attachée + /// **par référence** à un fichier de l'espace de stockage personnel + /// du client (). Le fichier reste dans + /// l'espace perso du client (pas de duplication) ; seul le lien est + /// persisté. Le téléchargement est servi par le host Blogs, qui + /// autorise l'appelant (client ou fournisseur de la demande) via + /// cette table. + /// + [PrimaryKey(nameof(FileId), nameof(CommandId))] + public class QueryAttachedFile + { + /// + /// Identifiant du fichier référencé (partie de la clé composite). + /// + public long FileId { get; set; } + + [ForeignKey("FileId")] + public virtual UploadedFile File { get; set; } + + /// + /// Identifiant de la demande à laquelle le fichier est rattaché + /// (partie de la clé composite). + /// + public long CommandId { get; set; } + + [ForeignKey("CommandId")] + public virtual NominativeServiceCommand Command { get; set; } + } +} \ No newline at end of file diff --git a/src/Yavsc.Server/Models/Billing/Signature.cs b/src/Yavsc.Server/Models/Billing/Signature.cs index 10da97f1b..7e568dbd1 100644 --- a/src/Yavsc.Server/Models/Billing/Signature.cs +++ b/src/Yavsc.Server/Models/Billing/Signature.cs @@ -7,8 +7,8 @@ namespace Yavsc.Models.Billing; /// /// One captured signature, attached to a single /// . Multiple versions are allowed per -/// (EstimateId, Type, SignerId) tuple — the controller reads -/// the most recent when asked. The wire-format payload is the +/// (EstimateId, Type, SignerId) tuple — the controller reads the +/// most recent when asked. The wire-format payload is the /// same int[] shape PostIt produces (see /// PostIt.Models.SignaturePadData): a length-prefixed /// sequence of strokes, each stroke being @@ -16,13 +16,12 @@ namespace Yavsc.Models.Billing; /// CoordinateMax]. /// /// -/// Why a separate table (instead of a JSON column on -/// ): the jalon 1 spec calls for at least -/// two distinct signatures per estimate cycle (provider -/// validation + client agreement) and the audit value of -/// preserving superseded versions. A dedicated table also keeps -/// the row narrow, which matters for -/// list views. +/// The column is the source of truth: the +/// signature is captured and stored entirely in-app (PostIt) and +/// round-tripped as JSON, with no on-disk copy. A dedicated table +/// (instead of a JSON column on ) keeps the +/// row narrow for list views and preserves +/// superseded versions for audit. /// /// public class Signature @@ -32,7 +31,7 @@ public class Signature public long EstimateId { get; set; } - [ForeignKey(nameof(EstimateId)), JsonIgnore] + [ForeignKey(nameof(EstimateId)), JsonIgnore, System.Text.Json.Serialization.JsonIgnore] public virtual Estimate Estimate { get; set; } /// @@ -49,7 +48,7 @@ public class Signature [Required] public string SignerId { get; set; } - [ForeignKey(nameof(SignerId)), JsonIgnore] + [ForeignKey(nameof(SignerId)), JsonIgnore, System.Text.Json.Serialization.JsonIgnore] public virtual ApplicationUser Signer { get; set; } public SignatureType Type { get; set; } @@ -75,16 +74,4 @@ public class Signature public int[] Strokes { get; set; } = Array.Empty(); public DateTime CapturedAtUtc { get; set; } - - /// - /// Path to the JSON-serialised wire payload on disk, - /// relative to UserFilesDirName. The disk copy is the - /// source of truth for the wire bytes; the - /// column is a denormalised index for queries. They are - /// written together in the same transaction by the - /// controller; the migration should keep them in sync - /// through ApplicationDbContext.SaveChanges. - /// - [Required] - public string FilePath { get; set; } -} +} \ No newline at end of file diff --git a/src/Yavsc.Server/Models/Blog/BlogAttachedFile.cs b/src/Yavsc.Server/Models/Blog/BlogAttachedFile.cs index a857665d6..4b024e637 100644 --- a/src/Yavsc.Server/Models/Blog/BlogAttachedFile.cs +++ b/src/Yavsc.Server/Models/Blog/BlogAttachedFile.cs @@ -1,3 +1,4 @@ +#nullable enable annotations using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; @@ -33,6 +34,19 @@ namespace Yavsc.Models.Blog /// /// public string ContentType { get; set; } + + /// + /// Propriétaire du fichier dans l'espace de stockage personnel. + /// Renseigné pour les fichiers téléversés via api/v1/fs + /// (référencés par / + /// ). Reste null pour les + /// pièces jointes de blog historiques (), + /// qui ne sont pas adressées par propriétaire. + /// + public string? OwnerId { get; set; } + + [ForeignKey("OwnerId")] + public virtual ApplicationUser? Owner { get; set; } } [PrimaryKey(nameof(FileId), nameof(PostId))] diff --git a/src/Yavsc.Server/Models/Blog/BlogPost.cs b/src/Yavsc.Server/Models/Blog/BlogPost.cs index 6dc8ff750..e05bf5600 100644 --- a/src/Yavsc.Server/Models/Blog/BlogPost.cs +++ b/src/Yavsc.Server/Models/Blog/BlogPost.cs @@ -57,7 +57,7 @@ namespace Yavsc.Models.Blog get; set; } - [Display(Name = "Utilisateur ayant modifé le dernier")] + [Display(Name = "Utilisateur ayant modifié le dernier")] public string? UserModified { get; set; @@ -96,14 +96,23 @@ namespace Yavsc.Models.Blog [InverseProperty("Post")] public virtual List Comments { get; set; } + [InverseProperty("Post")] + public virtual BlogSpotPublication Publication { get; set; } + /// /// Whether this post is published. Not a column: the /// existence of a row in BlogSpotPublication /// is the source of truth. EF skips this property via - /// [NotMapped] so no migration is needed. The - /// service hydrates it after each fetch (single bulk - /// lookup, not N+1) and it surfaces through the wire - /// as part of the JSON-serialised BlogPost. + /// [NotMapped] so no migration is needed. + /// + /// This is a value BlogSpotService owns and + /// sets explicitly on every post it returns — a single + /// bulk lookup against blogSpotPublications for a + /// list, an AnyAsync for a single post — so the + /// flag is correct whether or not the EF query Included + /// the navigation. Clients + /// (PostIt) read it as-is off the wire and must not + /// recompute it. /// [NotMapped] public bool IsPublished { get; set; } @@ -147,5 +156,7 @@ namespace Yavsc.Models.Blog }).ToList() ?? new List(); } } + + } } diff --git a/src/Yavsc.Server/Models/Blog/BlogPostEditViewModel.cs b/src/Yavsc.Server/Models/Blog/BlogPostEditViewModel.cs index 152e2e9a6..5c3c1e225 100644 --- a/src/Yavsc.Server/Models/Blog/BlogPostEditViewModel.cs +++ b/src/Yavsc.Server/Models/Blog/BlogPostEditViewModel.cs @@ -6,7 +6,7 @@ namespace Yavsc.Models.Blog public BlogPostEditViewModel() { } - + public BlogPostEditViewModel(BlogPost post, bool publish) { Id = post.Id; diff --git a/src/Yavsc.Server/Models/Blog/BlogspotPublication.cs b/src/Yavsc.Server/Models/Blog/BlogspotPublication.cs index bcae52218..c724f4821 100644 --- a/src/Yavsc.Server/Models/Blog/BlogspotPublication.cs +++ b/src/Yavsc.Server/Models/Blog/BlogspotPublication.cs @@ -7,9 +7,9 @@ namespace Yavsc.Models public class BlogSpotPublication { [Key] - public long BlogpostId { get; set; } - - [ForeignKey("BlogpostId")] - public virtual BlogPost BlogPost{ get; set; } + public long PostId { get; set; } + + [ForeignKey("PostId")] + public virtual BlogPost Post{ get; set; } } } diff --git a/src/Yavsc.Server/Models/FileSystem/FileReceivedInfo.cs b/src/Yavsc.Server/Models/FileSystem/FileReceivedInfo.cs index 63627c116..602cc4bf0 100644 --- a/src/Yavsc.Server/Models/FileSystem/FileReceivedInfo.cs +++ b/src/Yavsc.Server/Models/FileSystem/FileReceivedInfo.cs @@ -43,11 +43,29 @@ namespace Yavsc.Server.Models.FileSystem } public string DestDir { get; set; } - + public string FileName { get; set; } public bool Overridden { get; set; } public bool QuotaOffense { get; set; } + + /// + /// Identifiant de la ligne + /// créée à l'enregistrement du fichier dans l'espace perso + /// (api/v1/fs). Nul si aucune métadonnée n'a été persistée. + /// + public long? FileId { get; set; } + + /// + /// Taille du fichier reçu (octets), pour le reflet côté métadonnées. + /// + public long Length { get; set; } + + /// + /// Type MIME du fichier reçu. + /// + public string ContentType { get; set; } + public string FullName { get => Path.Combine(DestDir, FileName); } } diff --git a/src/Yavsc.Server/Services/BlogSpotService.cs b/src/Yavsc.Server/Services/BlogSpotService.cs index b4bf4c7ad..5d2ad5eaa 100644 --- a/src/Yavsc.Server/Services/BlogSpotService.cs +++ b/src/Yavsc.Server/Services/BlogSpotService.cs @@ -61,7 +61,7 @@ public class BlogSpotService blogFilesSubdir); var di = new DirectoryInfo(destDir); - if (!di.Exists) + if (!di.Exists) { logger.LogInformation("Creating directory for blog attachments: {destDir}", destDir); di.Create(); @@ -108,10 +108,17 @@ public class BlogSpotService public BlogPost Create(string userId, BlogPost post ) { // Sauvegarder le post d'abord pour obtenir son ID - // Le createur vient de l'authentification, donc on ne le prend pas du post + // Le créateur vient de l'authentification, donc on ne le prend pas du post post.AuthorId = userId; _context.BlogSpot.Add(post); _context.SaveChanges(userId); + // A freshly created post has no BlogSpotPublication row, + // so the server-side truth is "not published". The client + // may have sent isPublished:true in the POST body, but + // publication is toggled separately via SetPublishAsync; + // overwrite whatever was deserialized so the create + // response reflects the actual state. + post.IsPublished = false; return post; } @@ -126,7 +133,7 @@ public class BlogSpotService if (!auth.Succeeded) throw new AuthorizationFailureException(auth); - var pub = await _context.blogSpotPublications.AnyAsync(x => x.BlogpostId == blog.Id); + var pub = await _context.blogSpotPublications.AnyAsync(x => x.PostId == blog.Id); ScrubAclForViewer(blog, user); return new BlogPostEditViewModel(blog, pub); @@ -139,21 +146,24 @@ public class BlogSpotService .Include(p => p.Tags) .Include(p => p.Comments) .Include(p => p.ACL) + .Include(p => p.Publication) .SingleAsync(m => m.Id == blogPostId); if (blog == null) return null; - // Hydrate le flag [NotMapped] depuis la table de publication. - blog.IsPublished = await _context.blogSpotPublications - .AnyAsync(pub => pub.BlogpostId == blogPostId); - var auth = await _authorizationService.AuthorizeAsync(user, blog, new ReadPermission()); if (!auth.Succeeded) throw new AuthorizationFailureException(auth); ScrubAclForViewer(blog, user); + // IsPublished is a value the service serves to clients; + // set it from the loaded Publication navigation (Details + // Includes it above) rather than leaving it at the + // default false a deserialized entity would have. + blog.IsPublished = blog.Publication != null; + foreach (var c in blog.Comments) c.Author = _context.Users.First(u => u.Id == c.AuthorId); @@ -176,7 +186,7 @@ public class BlogSpotService _context.Update(blog); var publication = await _context.blogSpotPublications - .SingleOrDefaultAsync(p => p.BlogpostId == blogEdit.Id); + .SingleOrDefaultAsync(p => p.PostId == blogEdit.Id); if (publication != null) { @@ -185,7 +195,7 @@ public class BlogSpotService } else if (blogEdit.Publish) { - _context.blogSpotPublications.Add(new BlogSpotPublication { BlogpostId = blogEdit.Id }); + _context.blogSpotPublications.Add(new BlogSpotPublication { PostId = blogEdit.Id }); } _context.SaveChanges(user.GetUserId()); @@ -231,43 +241,38 @@ public class BlogSpotService .Include(p => p.ACL) .Include(p => p.Tags) .Include(p => p.Comments) - .Where(p => p.ACL == null - || p.ACL.Count == 0 + .Include(p => p.Publication) + .Where(p => p.Publication != null || p.AuthorId == viewerId || (userCircles != null && p.ACL.Any(a => userCircles.Contains(a.CircleId)))); } else { posts = _context.blogSpotPublications - .Include(p => p.BlogPost) - .Include(b => b.BlogPost.Author) - .Include(p => p.BlogPost.ACL) - .Include(p => p.BlogPost.Tags) - .Include(p => p.BlogPost.Comments) - .Where(p => p.BlogPost.ACL == null || p.BlogPost.ACL.Count == 0) - .Select(p => p.BlogPost) + .Include(p => p.Post) + .Include(b => b.Post.Author) + .Include(p => p.Post.ACL) + .Include(p => p.Post.Tags) + .Include(p => p.Post.Comments) + .Select(p => p.Post) .ToArray(); } - var materialised = posts.ToList(); + var materialized = posts.ToList(); - var postIds = materialised.Select(p => p.Id).ToList(); - if (postIds.Count > 0) - { - var publishedIds = await _context.blogSpotPublications - .Where(pub => postIds.Contains(pub.BlogpostId)) - .Select(pub => pub.BlogpostId) - .ToListAsync(); + // IsPublished is a value the service owns: hydrate it + // from blogSpotPublications in one bulk lookup so it is + // correct whether or not the branch above Included the + // Publication navigation (the anonymous branch does + // not). Without this, a freshly deserialized list would + // report IsPublished == false for every post. + var blogPosts = materialized.OfType().ToList(); + await HydrateIsPublishedAsync(blogPosts); - var publishedSet = publishedIds.ToHashSet(); - foreach (var post in materialised.OfType()) - post.IsPublished = publishedSet.Contains(post.Id); - } - - foreach (var post in materialised.OfType()) + foreach (var post in blogPosts) ScrubAclForViewer(post, user); - return materialised + return materialized .OrderByDescending(p => p.DateModified) .Skip(skip) .Take(take); @@ -308,10 +313,17 @@ public class BlogSpotService public async Task GetBlogPostAsync(long value) { - return await _context.BlogSpot + var blog = await _context.BlogSpot .Include(b => b.Author) .Include(b => b.ACL) .SingleOrDefaultAsync(x => x.Id == value); + if (blog != null) + // Serve the publication state from its source of truth + // so the delete response's isPublished is correct — the + // query above does not Include the Publication navigation. + blog.IsPublished = await _context.blogSpotPublications + .AnyAsync(p => p.PostId == blog.Id); + return blog; } public async Task SetPublishAsync(ClaimsPrincipal user, long postId, bool publish) @@ -323,11 +335,11 @@ public class BlogSpotService if (!auth.Succeeded) throw new AuthorizationFailureException(auth); - var existing = await _context.blogSpotPublications.SingleOrDefaultAsync(p => p.BlogpostId == postId); + var existing = await _context.blogSpotPublications.SingleOrDefaultAsync(p => p.PostId == postId); if (publish) { if (existing == null) - _context.blogSpotPublications.Add(new BlogSpotPublication { BlogpostId = postId }); + _context.blogSpotPublications.Add(new BlogSpotPublication { PostId = postId }); } else { @@ -345,6 +357,29 @@ public class BlogSpotService post.ACL = new List(); } + /// + /// Populate for each post in + /// from the blogSpotPublications + /// table — a single bulk lookup, not one query per post. The + /// IsPublished flag is a wire-only value the service owns: + /// clients (PostIt) read it as-is off the JSON response rather + /// than computing it themselves. Call this on every collection + /// the service returns to a caller, so the flag is correct + /// regardless of whether the EF query Included the + /// navigation. + /// + private async Task HydrateIsPublishedAsync(IReadOnlyList posts) + { + if (posts.Count == 0) return; + var ids = posts.Select(p => p.Id).ToArray(); + var publishedIds = await _context.blogSpotPublications + .Where(p => ids.Contains(p.PostId)) + .Select(p => p.PostId) + .ToHashSetAsync(); + foreach (var post in posts) + post.IsPublished = publishedIds.Contains(post.Id); + } + private static bool IsOwner(BlogPost post, ClaimsPrincipal? user) { if (user?.Identity?.IsAuthenticated != true) return false; diff --git a/src/Yavsc.Server/Services/MailSender.cs b/src/Yavsc.Server/Services/MailSender.cs index 81518eddb..9a6c60ed9 100644 --- a/src/Yavsc.Server/Services/MailSender.cs +++ b/src/Yavsc.Server/Services/MailSender.cs @@ -140,6 +140,10 @@ namespace Yavsc.Services catch (FormatException ex) { logger.LogError(ex, "Refusing to send email because the recipient or sender address is malformed. To={To}, From={From}", email, siteSettings.Owner.EMail); + logger.LogError(ex.Message); + if (ex.InnerException!=null) + logger.LogError(ex.InnerException.Message); + return string.Empty; } catch (SmtpCommandException ex) diff --git a/src/Yavsc.Server/Services/PermissionHandler.cs b/src/Yavsc.Server/Services/PermissionHandler.cs index 77ee9fd57..3958f7ff3 100644 --- a/src/Yavsc.Server/Services/PermissionHandler.cs +++ b/src/Yavsc.Server/Services/PermissionHandler.cs @@ -59,7 +59,7 @@ public class PermissionHandler : IAuthorizationHandler { return applicationDbContext.blogSpotPublications - .Any(p=>p.BlogpostId == blogPost.Id); + .Any(p=>p.PostId == blogPost.Id); } return false; } diff --git a/src/Yavsc.Server/Services/ProfileService.cs b/src/Yavsc.Server/Services/ProfileService.cs index 3b7b5121a..4c915f5c9 100644 --- a/src/Yavsc.Server/Services/ProfileService.cs +++ b/src/Yavsc.Server/Services/ProfileService.cs @@ -28,35 +28,40 @@ namespace Yavsc.Services var claims = new List { new Claim(JwtClaimTypes.Subject,user.Id.ToString()), }; - List claimAdds = new List(); foreach (var scope in context.RequestedResources.ParsedScopes) { if (context.Client.AllowedScopes.Contains(scope.ParsedName)) { claims.Add(new Claim(JwtClaimTypes.Scope, scope.ParsedName)); - claimAdds.Add(scope.ParsedName); } } - if (claimAdds.Contains(JwtClaimTypes.Profile)) + // name / email / roles are gated on the client being granted + // the "profile" identity scope. They MUST land in the *access + // token*: the Blogs/Api bearer resource hosts read the login + // via GetUserName() (FindFirstValue("name")) and roles via + // IsInMsRole() from the access token. But "profile" is an + // identity scope, which only appears in the id-token caller's + // ParsedScopes — never in the access-token caller's — so the + // previous gate (claimAdds.Contains("profile")) left name, + // email and roles out of every access token, and GetUserName() + // resolved to null on every bearer host (the MyFiles upload + // 500). Gate on the client's AllowedScopes instead, which is + // caller-independent, so both the access token and the id + // token carry these claims for any user grant whose client is + // allowed "profile". + if (context.Client.AllowedScopes.Contains(JwtClaimTypes.Profile)) { - claimAdds.Remove("profile"); - claimAdds.Add(JwtClaimTypes.Name); - claimAdds.Add(JwtClaimTypes.Email); - claimAdds.Add(Constants.RoleClaimType); - } - - if (claimAdds.Contains(JwtClaimTypes.Name)) - claims.Add(new Claim(JwtClaimTypes.Name, user.FullName)); - - if (claimAdds.Contains(JwtClaimTypes.Email)) + // "name" carries the login (UserName), not the display + // FullName: GetUserName() / User.Identity.Name feed + // filesystem paths (user files root, avatar file name), + // and FullName can contain spaces. No bearer-host + // consumer displays this claim as a human name today. + claims.Add(new Claim(JwtClaimTypes.Name, user.UserName)); claims.Add(new Claim(JwtClaimTypes.Email, user.Email)); - - if (claimAdds.Contains(Constants.RoleClaimType)) - { var roles = await this._userManager.GetRolesAsync(user); - if (roles.Count()>0) + if (roles.Count() > 0) { claims.AddRange(roles.Select(r => new Claim(Constants.RoleClaimType, r))); } diff --git a/src/Yavsc.Server/Yavsc.Server.csproj b/src/Yavsc.Server/Yavsc.Server.csproj index bb2022fc6..61c426ec0 100644 --- a/src/Yavsc.Server/Yavsc.Server.csproj +++ b/src/Yavsc.Server/Yavsc.Server.csproj @@ -9,8 +9,8 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 diff --git a/src/Yavsc.Tests.Shared/Directory.Packages.props b/src/Yavsc.Tests.Shared/Directory.Packages.props deleted file mode 100644 index d2342f795..000000000 --- a/src/Yavsc.Tests.Shared/Directory.Packages.props +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/src/cli/Directory.Packages.props b/src/cli/Directory.Packages.props deleted file mode 100644 index c1a40467d..000000000 --- a/src/cli/Directory.Packages.props +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/cli/cli.csproj b/src/cli/cli.csproj index 382dff670..7b95a7e68 100644 --- a/src/cli/cli.csproj +++ b/src/cli/cli.csproj @@ -7,8 +7,8 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 diff --git a/src/Yavsc.Api.Test/ActivityApiControllerTests.cs b/src/test/Yavsc.Api.Tests/ActivityApiControllerTests.cs similarity index 100% rename from src/Yavsc.Api.Test/ActivityApiControllerTests.cs rename to src/test/Yavsc.Api.Tests/ActivityApiControllerTests.cs diff --git a/src/Yavsc.Api.Test/ApiCollection.cs b/src/test/Yavsc.Api.Tests/ApiCollection.cs similarity index 100% rename from src/Yavsc.Api.Test/ApiCollection.cs rename to src/test/Yavsc.Api.Tests/ApiCollection.cs diff --git a/src/Yavsc.Api.Test/BillingControllerTests.cs b/src/test/Yavsc.Api.Tests/BillingControllerTests.cs similarity index 100% rename from src/Yavsc.Api.Test/BillingControllerTests.cs rename to src/test/Yavsc.Api.Tests/BillingControllerTests.cs diff --git a/src/Yavsc.Api.Test/EstimateApiControllerTests.cs b/src/test/Yavsc.Api.Tests/EstimateApiControllerTests.cs similarity index 85% rename from src/Yavsc.Api.Test/EstimateApiControllerTests.cs rename to src/test/Yavsc.Api.Tests/EstimateApiControllerTests.cs index cb945f51a..568175610 100644 --- a/src/Yavsc.Api.Test/EstimateApiControllerTests.cs +++ b/src/test/Yavsc.Api.Tests/EstimateApiControllerTests.cs @@ -209,9 +209,12 @@ public sealed class EstimateApiControllerTests : IClassFixture /// Seeds an estimate owned by alice (the provider) for the given - /// client, optionally already validated by the client. + /// client, optionally already validated by the client. By default the + /// estimate is also provider-validated (i.e. the provider has submitted + /// it); pass = false to seed a + /// freshly inserted request the provider hasn't worked on yet. /// - private long SeedEstimate(string clientId, bool clientValidated) + private long SeedEstimate(string clientId, bool clientValidated, bool providerValidated = true) { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); @@ -229,6 +232,7 @@ public sealed class EstimateApiControllerTests : IClassFixture +/// Integration tests for the estimate attachment endpoints +/// (GET/POST/DELETE api/v1/estimate/{id}/attachments), which +/// attach a file from the provider's personal storage to an estimate +/// **by reference** (). +/// +/// Coverage mirrors the authorization contract documented on the +/// controller: only the provider (estimate.OwnerId) may attach or +/// detach, the file must belong to the caller, and listing is open to +/// both parties (provider/client) plus admin. A third party is refused. +/// The download of the bytes themselves is served by the Blogs host and +/// exercised in Yavsc.Blogs.Tests; these tests cover only the +/// metadata/relationship endpoints that live on the API host. +/// +[Collection("Yavsc Api")] +public sealed class EstimateAttachmentsApiTests : IClassFixture +{ + private readonly ApiWebServerFixture _fixture; + + public EstimateAttachmentsApiTests(ApiWebServerFixture fixture) + { + _fixture = fixture; + } + + private HttpClient NewClient(string subject = "alice", string scope = "api") + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + var http = new HttpClient(handler) + { + BaseAddress = new Uri(_fixture.BaseAddress) + }; + http.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope)); + return http; + } + + /// + /// Reset the graph (seeding alice the provider and bob the client), + /// create an estimate owned by alice for bob, and seed two personal + /// rows: one owned by alice (the provider's + /// own file) and one owned by bob (a file that does NOT belong to the + /// provider, used to assert the ownership guard on attach). + /// + private (long estimateId, long aliceFileId, long bobFileId) Seed() + { + _fixture.ResetAndSeedActivityGraph(); + + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var estimate = new Estimate + { + ClientId = "bob", + OwnerId = "alice", + CommandType = BillingCodes.Rdv, + Title = "Devis prestation", + Description = "Devis de test", + AttachedFiles = new List(), + AttachedGraphics = new List(), + Bill = new List + { + new() { Name = "Prestation", Description = "Prestation de base", Count = 1, UnitaryCost = 120m, Currency = "EUR" }, + }, + }; + db.Estimates.Add(estimate); + db.SaveChanges(); + + var aliceFile = new UploadedFile + { + OwnerId = "alice", + Path = "provider-doc.pdf", + ContentType = "application/pdf", + Length = 2048, + }; + var bobFile = new UploadedFile + { + OwnerId = "bob", + Path = "client-note.txt", + ContentType = "text/plain", + Length = 64, + }; + db.UploadedFiles.AddRange(aliceFile, bobFile); + db.SaveChanges(); + + return (estimate.Id, aliceFile.Id, bobFile.Id); + } + + private int CountAttachments(long estimateId) + { + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return db.EstimateAttachedFiles.Count(a => a.EstimateId == estimateId); + } + + [Fact] + public async Task Provider_attaching_own_file_creates_the_link() + { + var (estimateId, aliceFileId, _) = Seed(); + + using var http = NewClient("alice"); + var response = await http.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.True(response.StatusCode == HttpStatusCode.OK, + $"Expected 200, got {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + Assert.Equal(1, CountAttachments(estimateId)); + + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var link = Assert.Single(db.EstimateAttachedFiles.Where(a => a.EstimateId == estimateId)); + Assert.Equal(aliceFileId, link.FileId); + } + + [Fact] + public async Task Attaching_the_same_file_twice_is_idempotent() + { + var (estimateId, aliceFileId, _) = Seed(); + + using var http = NewClient("alice"); + await http.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + var second = await http.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, second.StatusCode); + Assert.Equal(1, CountAttachments(estimateId)); + } + + [Fact] + public async Task Non_provider_cannot_attach_to_an_estimate() + { + var (estimateId, aliceFileId, _) = Seed(); + + // bob is the client of the estimate, not the provider. + using var http = NewClient("bob"); + var response = await http.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal(0, CountAttachments(estimateId)); + } + + [Fact] + public async Task Provider_cannot_attach_a_file_they_do_not_own() + { + var (estimateId, _, bobFileId) = Seed(); + + // alice is the provider, but the file belongs to bob. + using var http = NewClient("alice"); + var response = await http.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = bobFileId }, + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal(0, CountAttachments(estimateId)); + } + + [Fact] + public async Task Provider_detaches_a_file() + { + var (estimateId, aliceFileId, _) = Seed(); + + using var http = NewClient("alice"); + await http.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + Assert.Equal(1, CountAttachments(estimateId)); + + var response = await http.DeleteAsync( + $"/api/v1/estimate/{estimateId}/attachments/{aliceFileId}", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, CountAttachments(estimateId)); + } + + [Fact] + public async Task Non_provider_cannot_detach() + { + var (estimateId, aliceFileId, _) = Seed(); + + using var http = NewClient("alice"); + await http.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + + using var bob = NewClient("bob"); + var response = await bob.DeleteAsync( + $"/api/v1/estimate/{estimateId}/attachments/{aliceFileId}", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal(1, CountAttachments(estimateId)); + } + + [Fact] + public async Task Provider_lists_attachments() + { + var (estimateId, aliceFileId, _) = Seed(); + + using var http = NewClient("alice"); + await http.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + + var response = await http.GetAsync( + $"/api/v1/estimate/{estimateId}/attachments", + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, + $"Expected 200, got {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var doc = JsonDocument.Parse(body); + var item = Assert.Single(doc.RootElement.EnumerateArray()); + Assert.Equal(aliceFileId, item.GetProperty("fileId").GetInt64()); + Assert.Equal("provider-doc.pdf", item.GetProperty("path").GetString()); + Assert.Equal("alice", item.GetProperty("ownerId").GetString()); + } + + [Fact] + public async Task Client_can_list_attachments_as_a_party() + { + var (estimateId, aliceFileId, _) = Seed(); + + using var alice = NewClient("alice"); + await alice.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + + // bob is the client of the estimate: a party, may list. + using var bob = NewClient("bob"); + var response = await bob.GetAsync( + $"/api/v1/estimate/{estimateId}/attachments", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Third_party_cannot_list_attachments() + { + var (estimateId, aliceFileId, _) = Seed(); + + using var alice = NewClient("alice"); + await alice.PostAsJsonAsync( + $"/api/v1/estimate/{estimateId}/attachments", + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + + // carol is neither the provider nor the client. + using var carol = NewClient("carol"); + var response = await carol.GetAsync( + $"/api/v1/estimate/{estimateId}/attachments", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Listing_an_unknown_estimate_returns_404() + { + _fixture.ResetAndSeedActivityGraph(); + using var http = NewClient("alice"); + + var response = await http.GetAsync( + "/api/v1/estimate/999999/attachments", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } +} \ No newline at end of file diff --git a/src/test/Yavsc.Api.Tests/EstimatePdfGenerationTests.cs b/src/test/Yavsc.Api.Tests/EstimatePdfGenerationTests.cs new file mode 100644 index 000000000..e0f156048 --- /dev/null +++ b/src/test/Yavsc.Api.Tests/EstimatePdfGenerationTests.cs @@ -0,0 +1,191 @@ +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using Yavsc.Helpers; +using Yavsc.Models.Billing; +using Yavsc.ViewModels.Gen; + +namespace Yavsc.Api.Test; + +// Unit test of the estimate PDF generation pipeline (no HTTP fixture +// needed): it exercises TeXHelpers.SignatureToTikz — the vector drawing +// of the provider and client signatures — and TeXHelpers.GenerateEstimatePdf +// — the lualatex stdin compile that writes .pdf into DestDir. The +// TeX source mirrors what the Estimate_tex template emits for a devis +// signed by both parties: two side-by-side minipages, each with a +// tikzpicture bounded by \useasboundingbox and the strokes drawn by +// SignatureToTikz. +// +// The PDF test requires the system `lualatex` (and the TeX Live packages +// listed in the README runtime-deps section) on the host running the +// tests. On hosts without lualatex it skips cleanly rather than failing. +public sealed class EstimatePdfGenerationTests +{ + // A provider signature: a downstroke, a cross, and a tap (dot). + // PostIt wire format — [k, x0,y0, x1,y1, …] per stroke, coords in + // [0, 10000], y-down. + private static readonly int[] ProStrokes = + { + 4, 2000, 8000, 2000, 5000, 2000, 2000, 2100, 1900, + 3, 2000, 5000, 3500, 4500, 3500, 5100, + 1, 4000, 3000, + }; + + // A client signature: a wavy stroke and a shorter one. + private static readonly int[] CliStrokes = + { + 5, 1000, 7000, 1500, 4000, 2200, 3000, 3000, 2500, 4000, 1500, + 2, 3000, 6000, 4200, 5500, + }; + + [Fact] + public void SignatureToTikz_RendersBothSignaturesAsVectorDraws() + { + var pro = MakeSig(SignatureType.Pro, ProStrokes); + var cli = MakeSig(SignatureType.Client, CliStrokes); + + var proTex = pro.SignatureToTikz(5, 2.5).ToString(); + var cliTex = cli.SignatureToTikz(5, 2.5).ToString(); + + Assert.Contains("\\draw[line cap=round, line join=round, line width=1.5pt]", proTex); + Assert.Contains("\\fill", proTex); // the single-point tap → dot + Assert.Contains("(1.000,0.500)", proTex); // first point (2000,8000), y flipped + Assert.Contains("\\draw[line cap=round, line join=round, line width=1.5pt]", cliTex); + Assert.Contains("--", cliTex); + } + + [Fact] + public void SignatureToTikz_EmptySignatureIsEmpty() + { + var empty = MakeSig(SignatureType.Pro, Array.Empty()); + Assert.Equal("", empty.SignatureToTikz(5, 2.5).ToString()); + } + + [Fact] + public void GenerateEstimatePdf_WithBothSignatures_ProducesPdfInDestDir() + { + if (!IsLualatexAvailable()) + throw Xunit.Sdk.SkipException.ForSkip( + "lualatex is not installed on this host; skipping PDF generation test. " + + "Install the TeX Live runtime deps documented in the README."); + + var destDir = Path.Combine(Path.GetTempPath(), + "yavsc-estimate-pdf-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(destDir); + + try + { + var pro = MakeSig(SignatureType.Pro, ProStrokes); + var cli = MakeSig(SignatureType.Client, CliStrokes); + var baseName = "estimate-test-both-signed"; + + var model = new PdfGenerationViewModel + { + TeXSource = BuildEstimateTeX(pro, cli), + BaseFileName = baseName, + DestDir = destDir, + }; + + var fo = model.GenerateEstimatePdf(); + + // The PDF must exist, at the expected path, and be non-empty. + var expectedPath = Path.Combine(destDir, baseName + ".pdf"); + Assert.True(fo is not null && fo.Exists, + "GenerateEstimatePdf returned a non-existent file. Error: " + + model.GenerationErrorMessage?.Value); + Assert.Equal(expectedPath, fo.FullName); + Assert.True(fo.Length > 0, "generated PDF is empty"); + + // Only the PDF should persist in DestDir — aux/log artifacts + // are cleaned up by GenerateEstimatePdf on success. + var leftovers = Directory.GetFiles(destDir) + .Where(p => !Path.GetFileName(p).Equals(baseName + ".pdf", StringComparison.Ordinal)) + .ToArray(); + Assert.Empty(leftovers); + } + finally + { + if (Directory.Exists(destDir)) + Directory.Delete(destDir, recursive: true); + } + } + + private static Signature MakeSig(SignatureType type, int[] strokes) + => new() + { + Type = type, + CoordinateMax = 10_000, + Strokes = strokes, + CapturedAtUtc = new DateTime(2026, 9, 22, 0, 0, 0, DateTimeKind.Utc), + }; + + // Builds the LaTeX source the Estimate_tex template would emit for a + // devis signed by both parties: same preamble (incl. tikz + bera) and + // the two-minipage signature foot, with the strokes drawn by + // SignatureToTikz. + private static string BuildEstimateTeX(Signature pro, Signature cli) + { + var fr = CultureInfo.CreateSpecificCulture("fr-FR"); + var date = new DateTime(2026, 9, 22).ToString("dddd dd MMMM yyyy", fr); + var proTikz = pro.SignatureToTikz(5, 2.5).ToString(); + var cliTikz = cli.SignatureToTikz(5, 2.5).ToString(); + + return $@"\documentclass[french,11pt]{{article}} +\usepackage{{eurosym}} +\usepackage{{babel}} +\usepackage[T1]{{fontenc}} +\usepackage[utf8]{{inputenc}} +\usepackage[a4paper]{{geometry}} +\usepackage{{units}} +\usepackage{{bera}} +\usepackage{{graphicx}} +\usepackage{{fancyhdr}} +\usepackage{{fp}} +\usepackage{{tikz}} +\geometry{{verbose,tmargin=4em,bmargin=8em,lmargin=6em,rmargin=6em}} +\setlength{{\parindent}}{{0pt}} +\begin{{document}} +Devis n°1 --- test signe par les deux parties + +\vspace{{2em}} +\noindent +\begin{{minipage}}[t]{{0.45\textwidth}} + \textbf{{Signature du fournisseur}}\\ +\small Valide le {date}\\ +\begin{{tikzpicture}} + \useasboundingbox (0,0) rectangle (5,2.5); + {proTikz} +\end{{tikzpicture}} +\end{{minipage}}\hfill +\begin{{minipage}}[t]{{0.45\textwidth}} + \textbf{{Signature du client}}\\ +\small Valide le {date}\\ +\begin{{tikzpicture}} + \useasboundingbox (0,0) rectangle (5,2.5); + {cliTikz} +\end{{tikzpicture}} +\end{{minipage}} +\end{{document}} +"; + } + + private static bool IsLualatexAvailable() + { + try + { + using var p = Process.Start(new ProcessStartInfo("lualatex", "--version") + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }); + if (p is null) return false; + return p.WaitForExit(5000) && p.ExitCode == 0; + } + catch + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs b/src/test/Yavsc.Api.Tests/Fixtures/ApiWebServerFixture.cs similarity index 93% rename from src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs rename to src/test/Yavsc.Api.Tests/Fixtures/ApiWebServerFixture.cs index 51e939ceb..5d040fd27 100644 --- a/src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs +++ b/src/test/Yavsc.Api.Tests/Fixtures/ApiWebServerFixture.cs @@ -24,7 +24,6 @@ public sealed class ApiWebServerFixture : WebHostFixture private const string DbProviderEnvVar = "YAVSC_API_TEST_DB_PROVIDER"; private const string NpgsqlAdminConnectionEnvVar = "YAVSC_API_TEST_NPGSQL_ADMIN_CONNECTION"; private const string DedicatedNpgsqlDatabaseName = "yavscTestDb"; - private const string DefaultDevelopmentConnectionString = "Server=localhost;Port=5432;Database=yavscdev;Username=yavscdev;Password=8*5idas;Include Error Detail=true"; protected override int HttpsPort => 5104; @@ -168,25 +167,32 @@ public sealed class ApiWebServerFixture : WebHostFixture private static string BuildAdminConnectionString() { + // The admin connection string (host + credentials) is supplied + // via environment, never embedded in source — committing a + // real dev password would leak it into git history. The Npgsql + // path is opt-in, so requiring the env var is fine: a missing + // value produces a clear failure rather than a silent fallback + // onto someone's local dev database. var configured = Environment.GetEnvironmentVariable(NpgsqlAdminConnectionEnvVar); - var source = string.IsNullOrWhiteSpace(configured) - ? DefaultDevelopmentConnectionString - : configured; + if (string.IsNullOrWhiteSpace(configured)) + throw new InvalidOperationException( + $"{NpgsqlAdminConnectionEnvVar} is not set. The Npgsql test " + + "provider requires an admin PostgreSQL connection string " + + "in that environment variable (e.g. " + + "'Server=localhost;Port=5432;Database=postgres;" + + "Username=yavscdev;Password=...;Include Error Detail=true')."); - var builder = new NpgsqlConnectionStringBuilder(source) + var builder = new NpgsqlConnectionStringBuilder(configured) { Pooling = false, IncludeErrorDetail = true }; - if (string.IsNullOrWhiteSpace(configured)) - { + // The admin connection targets the maintenance database + // (postgres) so we can CREATE/DROP the dedicated test database; + // whatever database the env var names is overridden here. + if (string.IsNullOrWhiteSpace(builder.Database)) builder.Database = "postgres"; - } - else if (string.IsNullOrWhiteSpace(builder.Database)) - { - builder.Database = "postgres"; - } return builder.ToString(); } diff --git a/src/test/Yavsc.Api.Tests/FrontOfficeApiControllerTests.cs b/src/test/Yavsc.Api.Tests/FrontOfficeApiControllerTests.cs new file mode 100644 index 000000000..b2a8fdbee --- /dev/null +++ b/src/test/Yavsc.Api.Tests/FrontOfficeApiControllerTests.cs @@ -0,0 +1,342 @@ +using System.Linq; +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Yavsc.Api.Test.Fixtures; +using Yavsc.Helpers; +using Yavsc.Models; +using Yavsc.Models.Billing; +using Yavsc.Models.Workflow; +using Yavsc.Tests.Shared; + +namespace Yavsc.Api.Test; + +[Collection("Yavsc Api")] +public sealed class FrontOfficeApiControllerTests : IClassFixture +{ + private readonly ApiWebServerFixture _fixture; + + public FrontOfficeApiControllerTests(ApiWebServerFixture fixture) + { + _fixture = fixture; + } + + private HttpClient NewClient(string subject = "alice", string scope = "api") + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + + var http = new HttpClient(handler) + { + BaseAddress = new Uri(_fixture.BaseAddress) + }; + http.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope)); + return http; + } + + // Minimal valid PostIt wire-format signature: one stroke of one + // point (k=1, then 2*k coords). See PostIt.Models.SignaturePadData. + private static readonly int[] SampleStrokes = { 1, 5000, 5000 }; + + private static HttpContent NoSignature() => JsonBody(new { }); + + private static HttpContent WithSignature(SignatureType? type = null) => + type is null + ? JsonBody(new { strokes = SampleStrokes, coordinateMax = 10_000 }) + : JsonBody(new { strokes = SampleStrokes, coordinateMax = 10_000, signatureType = type }); + + private static JsonContent JsonBody(object value) => + JsonContent.Create(value, mediaType: new MediaTypeHeaderValue("application/json")); + + /// + /// Seeds an Rdv query and, when is + /// true, an estimate linked to it via CommandId. The provider is + /// (default alice) and the client is + /// (default bob) — pass the same value + /// for both to model a user who is both parties. Returns the query + /// id and (when seeded) the estimate id. + /// + private (long queryId, long estimateId) SeedQuery( + bool withEstimate, + string performer = "alice", + string client = "bob") + { + WorkflowHelpers.ConfigureBillingService(); + _fixture.ResetAndSeedActivityGraph(); + + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var location = db.Locations.Single(l => l.Address == "1 rue du Test"); + + var query = new RdvQuery + { + ActivityCode = "dev", + ClientId = client, + PerformerId = performer, + Consent = true, + UserCreated = performer, + UserModified = performer, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + EventDate = DateTime.UtcNow.AddDays(1), + Location = location, + Reason = "Initial rendez-vous", + Status = Yavsc.QueryStatus.Inserted, + }; + db.RdvQueries.Add(query); + db.SaveChanges(); + + long estimateId = 0; + if (withEstimate) + { + var estimate = new Estimate + { + CommandId = query.Id, + ClientId = client, + OwnerId = performer, + CommandType = BillingCodes.Rdv, + Title = "Devis prestation", + Description = "Devis de test", + AttachedFiles = new List(), + AttachedGraphics = new List(), + Bill = new List + { + new() { Name = "Prestation", Description = "Prestation de base", Count = 1, UnitaryCost = 120m, Currency = "EUR" }, + }, + }; + db.Estimates.Add(estimate); + db.SaveChanges(); + estimateId = estimate.Id; + } + + return (query.Id, estimateId); + } + + [Fact] + public async Task Front_accept_query_updates_status_without_server_error() + { + var (queryId, _) = SeedQuery(withEstimate: false); + + using var http = NewClient(); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + NoSignature(), + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var assertScope = _fixture.Services.CreateScope(); + var assertDb = assertScope.ServiceProvider.GetRequiredService(); + var updated = assertDb.RdvQueries.Single(q => q.Id == queryId); + + // Default caller is "alice" (the provider) → ProAccepted. + Assert.Equal(QueryStatus.ProAccepted, updated.Status); + } + + [Fact] + public async Task Provider_accept_with_signature_sets_ProValidationDate_and_Pro_signature() + { + var (queryId, estimateId) = SeedQuery(withEstimate: true); + + using var http = NewClient(subject: "alice"); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + WithSignature(), + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var assertScope = _fixture.Services.CreateScope(); + var assertDb = assertScope.ServiceProvider.GetRequiredService(); + + Assert.Equal(QueryStatus.ProAccepted, assertDb.RdvQueries.Single(q => q.Id == queryId).Status); + + var estimate = assertDb.Estimates.Single(e => e.Id == estimateId); + Assert.True(estimate.ProviderValidationDate != default); + Assert.True(estimate.ClientValidationDate == default); + + var signature = assertDb.Signatures.Single(s => s.EstimateId == estimateId); + Assert.Equal(SignatureType.Pro, signature.Type); + Assert.Equal("alice", signature.SignerId); + } + + [Fact] + public async Task Client_accept_with_signature_sets_ClientValidationDate_and_Client_signature() + { + var (queryId, estimateId) = SeedQuery(withEstimate: true); + + using var http = NewClient(subject: "bob"); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + WithSignature(), + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var assertScope = _fixture.Services.CreateScope(); + var assertDb = assertScope.ServiceProvider.GetRequiredService(); + + Assert.Equal(QueryStatus.ClientAccepted, assertDb.RdvQueries.Single(q => q.Id == queryId).Status); + + var estimate = assertDb.Estimates.Single(e => e.Id == estimateId); + Assert.True(estimate.ClientValidationDate != default); + Assert.True(estimate.ProviderValidationDate == default); + + var signature = assertDb.Signatures.Single(s => s.EstimateId == estimateId); + Assert.Equal(SignatureType.Client, signature.Type); + Assert.Equal("bob", signature.SignerId); + } + + [Fact] + public async Task Reject_sets_status_Rejected() + { + var (queryId, _) = SeedQuery(withEstimate: false); + + using var http = NewClient(subject: "alice"); + var response = await http.PostAsync( + $"/api/v1/front/query/reject?billingCode=Rdv&queryId={queryId}", + NoSignature(), + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var assertScope = _fixture.Services.CreateScope(); + var assertDb = assertScope.ServiceProvider.GetRequiredService(); + Assert.Equal(QueryStatus.Rejected, assertDb.RdvQueries.Single(q => q.Id == queryId).Status); + } + + [Fact] + public async Task Non_party_actor_is_forbidden() + { + var (queryId, _) = SeedQuery(withEstimate: false); + + using var http = NewClient(subject: "carol"); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + NoSignature(), + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Accept_with_signature_but_no_estimate_returns_400() + { + var (queryId, _) = SeedQuery(withEstimate: false); + + using var http = NewClient(subject: "alice"); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + WithSignature(), + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Reject_with_signature_returns_400() + { + var (queryId, _) = SeedQuery(withEstimate: true); + + using var http = NewClient(subject: "alice"); + var response = await http.PostAsync( + $"/api/v1/front/query/reject?billingCode=Rdv&queryId={queryId}", + WithSignature(), + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Estimate_payload_carries_existing_signature_strokes() + { + var (queryId, estimateId) = SeedQuery(withEstimate: true); + + // Provider accepts with a signature. + using (var pro = NewClient(subject: "alice")) + { + var r = await pro.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + WithSignature(), + TestContext.Current.CancellationToken); + Assert.True(r.IsSuccessStatusCode, await r.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + } + + // The estimate read endpoint carries the signature strokes on + // the estimate payload, so PostIt can repaint them on reopen. + using var reader = NewClient(subject: "alice"); + var response = await reader.GetAsync( + $"/api/v1/estimate/{estimateId}", + TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); + + // Signatures ride on the estimate payload as two dedicated + // slots, not a list: the provider signed here, so signaturePro + // is populated and signatureClient is absent/null. + var proSig = body.GetProperty("signaturePro"); + Assert.Equal(JsonValueKind.Object, proSig.ValueKind); + Assert.Equal(JsonValueKind.Array, proSig.GetProperty("strokes").ValueKind); + Assert.True(body.TryGetProperty("signatureClient", out var cli) + && cli.ValueKind == JsonValueKind.Null, + "signatureClient should be null when only the provider has signed"); + } + + // When one user is both the provider and the client of a query, the + // server cannot infer which side they are signing as from identity + // alone (it would always pick the provider). The caller declares the + // side (signatureType=1 here = Client); the server honors it because + // the caller is that party, and stores a Client signature. + [Fact] + public async Task Accept_declared_client_side_stores_client_signature_when_user_is_both_parties() + { + var (queryId, estimateId) = SeedQuery(withEstimate: true, performer: "alice", client: "alice"); + + using var http = NewClient(subject: "alice"); + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + WithSignature(SignatureType.Client), // signing as the client + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, $"Unexpected status {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var assertScope = _fixture.Services.CreateScope(); + var assertDb = assertScope.ServiceProvider.GetRequiredService(); + + Assert.Equal(QueryStatus.ClientAccepted, assertDb.RdvQueries.Single(q => q.Id == queryId).Status); + + var estimate = assertDb.Estimates.Single(e => e.Id == estimateId); + Assert.True(estimate.ClientValidationDate != default); + Assert.True(estimate.ProviderValidationDate == default); + + var signature = assertDb.Signatures.Single(s => s.EstimateId == estimateId); + Assert.Equal(SignatureType.Client, signature.Type); + Assert.Equal("alice", signature.SignerId); + } + + // A provider may not sign as the client (and vice versa): declaring + // the other party's side is refused even though the caller is a + // party to the query. Write is authorized only for the signature's + // author. + [Fact] + public async Task Accept_declaring_other_party_side_is_forbidden() + { + var (queryId, _) = SeedQuery(withEstimate: true); // provider alice, client bob + + using var http = NewClient(subject: "alice"); // alice is the provider + var response = await http.PostAsync( + $"/api/v1/front/query/accept?billingCode=Rdv&queryId={queryId}", + WithSignature(SignatureType.Client), // alice tries to sign as the client + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } +} \ No newline at end of file diff --git a/src/test/Yavsc.Api.Tests/FrontOfficeQueryAttachmentsApiTests.cs b/src/test/Yavsc.Api.Tests/FrontOfficeQueryAttachmentsApiTests.cs new file mode 100644 index 000000000..d4438ef4e --- /dev/null +++ b/src/test/Yavsc.Api.Tests/FrontOfficeQueryAttachmentsApiTests.cs @@ -0,0 +1,256 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Yavsc.Api.Test.Fixtures; +using Yavsc.Helpers; +using Yavsc.Models; +using Yavsc.Models.Billing; +using Yavsc.Models.Blog; +using Yavsc.Models.Workflow; +using Yavsc.Tests.Shared; + +namespace Yavsc.Api.Test; + +/// +/// Integration tests for the query (demande) attachment endpoints +/// (GET/POST/DELETE api/v1/front/query/{queryId}/attachments), +/// which attach a file from the client's personal storage to their +/// NominativeServiceCommand **by reference** +/// (). +/// +/// Coverage mirrors the controller contract: only the client +/// (the demande's ClientId) may attach or detach, the file must +/// belong to the caller, and listing is open to both parties +/// (client/provider) plus admin. A third party is refused. +/// +[Collection("Yavsc Api")] +public sealed class FrontOfficeQueryAttachmentsApiTests : IClassFixture +{ + private readonly ApiWebServerFixture _fixture; + + public FrontOfficeQueryAttachmentsApiTests(ApiWebServerFixture fixture) + { + _fixture = fixture; + } + + private HttpClient NewClient(string subject = "bob", string scope = "api") + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + var http = new HttpClient(handler) + { + BaseAddress = new Uri(_fixture.BaseAddress) + }; + http.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope)); + return http; + } + + /// + /// Seed an Rdv query from bob (client) to alice (provider) and two + /// personal rows: one owned by bob (the + /// client's own file) and one owned by alice (a file that does NOT + /// belong to the client, used to assert the ownership guard). + /// + private (long queryId, long bobFileId, long aliceFileId) Seed() + { + WorkflowHelpers.ConfigureBillingService(); + _fixture.ResetAndSeedActivityGraph(); + + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var location = db.Locations.Single(l => l.Address == "1 rue du Test"); + + var query = new RdvQuery + { + ActivityCode = "dev", + ClientId = "bob", + PerformerId = "alice", + Consent = true, + UserCreated = "bob", + UserModified = "bob", + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + EventDate = DateTime.UtcNow.AddDays(1), + Location = location, + Reason = "Demande de devis", + Status = QueryStatus.InProgress, + Description = "Demande en attente", + }; + db.RdvQueries.Add(query); + db.SaveChanges(); + + var bobFile = new UploadedFile + { + OwnerId = "bob", + Path = "demande-photo.jpg", + ContentType = "image/jpeg", + Length = 4096, + }; + var aliceFile = new UploadedFile + { + OwnerId = "alice", + Path = "provider-spec.pdf", + ContentType = "application/pdf", + Length = 1024, + }; + db.UploadedFiles.AddRange(bobFile, aliceFile); + db.SaveChanges(); + + return (query.Id, bobFile.Id, aliceFile.Id); + } + + private int CountAttachments(long queryId) + { + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return db.QueryAttachedFiles.Count(a => a.CommandId == queryId); + } + + private string AttachUrl(long queryId) => + $"/api/v1/front/query/{queryId}/attachments?billingCode=Rdv"; + + private string DetachUrl(long queryId, long fileId) => + $"/api/v1/front/query/{queryId}/attachments/{fileId}?billingCode=Rdv"; + + [Fact] + public async Task Client_attaching_own_file_creates_the_link() + { + var (queryId, bobFileId, _) = Seed(); + + using var http = NewClient("bob"); + var response = await http.PostAsJsonAsync( + AttachUrl(queryId), + new { fileId = bobFileId }, + TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.True(response.StatusCode == HttpStatusCode.OK, + $"Expected 200, got {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + Assert.Equal(1, CountAttachments(queryId)); + + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var link = Assert.Single(db.QueryAttachedFiles.Where(a => a.CommandId == queryId)); + Assert.Equal(bobFileId, link.FileId); + } + + [Fact] + public async Task Provider_cannot_attach_to_a_query() + { + var (queryId, bobFileId, _) = Seed(); + + // alice is the provider of the query; only the client attaches. + using var http = NewClient("alice"); + var response = await http.PostAsJsonAsync( + AttachUrl(queryId), + new { fileId = bobFileId }, + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal(0, CountAttachments(queryId)); + } + + [Fact] + public async Task Client_cannot_attach_a_file_they_do_not_own() + { + var (queryId, _, aliceFileId) = Seed(); + + // bob is the client, but the file belongs to alice. + using var http = NewClient("bob"); + var response = await http.PostAsJsonAsync( + AttachUrl(queryId), + new { fileId = aliceFileId }, + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal(0, CountAttachments(queryId)); + } + + [Fact] + public async Task Client_detaches_a_file() + { + var (queryId, bobFileId, _) = Seed(); + + using var http = NewClient("bob"); + await http.PostAsJsonAsync( + AttachUrl(queryId), + new { fileId = bobFileId }, + TestContext.Current.CancellationToken); + Assert.Equal(1, CountAttachments(queryId)); + + var response = await http.DeleteAsync( + DetachUrl(queryId, bobFileId), + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(0, CountAttachments(queryId)); + } + + [Fact] + public async Task Provider_cannot_detach() + { + var (queryId, bobFileId, _) = Seed(); + + using var bob = NewClient("bob"); + await bob.PostAsJsonAsync( + AttachUrl(queryId), + new { fileId = bobFileId }, + TestContext.Current.CancellationToken); + + using var alice = NewClient("alice"); + var response = await alice.DeleteAsync( + DetachUrl(queryId, bobFileId), + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal(1, CountAttachments(queryId)); + } + + [Fact] + public async Task Provider_lists_attachments_as_a_party() + { + var (queryId, bobFileId, _) = Seed(); + + using var bob = NewClient("bob"); + await bob.PostAsJsonAsync( + AttachUrl(queryId), + new { fileId = bobFileId }, + TestContext.Current.CancellationToken); + + // alice is the provider: a party, may list. + using var alice = NewClient("alice"); + var response = await alice.GetAsync(AttachUrl(queryId), TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, + $"Expected 200, got {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var doc = JsonDocument.Parse(body); + var item = Assert.Single(doc.RootElement.EnumerateArray()); + Assert.Equal(bobFileId, item.GetProperty("fileId").GetInt64()); + Assert.Equal("bob", item.GetProperty("ownerId").GetString()); + } + + [Fact] + public async Task Third_party_cannot_list_attachments() + { + var (queryId, bobFileId, _) = Seed(); + + using var bob = NewClient("bob"); + await bob.PostAsJsonAsync( + AttachUrl(queryId), + new { fileId = bobFileId }, + TestContext.Current.CancellationToken); + + using var carol = NewClient("carol"); + var response = await carol.GetAsync(AttachUrl(queryId), TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } +} \ No newline at end of file diff --git a/src/Yavsc.Api.Test/HairCutQueryApiControllerTests.cs b/src/test/Yavsc.Api.Tests/HairCutQueryApiControllerTests.cs similarity index 100% rename from src/Yavsc.Api.Test/HairCutQueryApiControllerTests.cs rename to src/test/Yavsc.Api.Tests/HairCutQueryApiControllerTests.cs diff --git a/src/Yavsc.Api.Test/HairMultiCutQueryApiControllerTests.cs b/src/test/Yavsc.Api.Tests/HairMultiCutQueryApiControllerTests.cs similarity index 100% rename from src/Yavsc.Api.Test/HairMultiCutQueryApiControllerTests.cs rename to src/test/Yavsc.Api.Tests/HairMultiCutQueryApiControllerTests.cs diff --git a/src/Yavsc.Api.Test/HomeControllerTests.cs b/src/test/Yavsc.Api.Tests/HomeControllerTests.cs similarity index 100% rename from src/Yavsc.Api.Test/HomeControllerTests.cs rename to src/test/Yavsc.Api.Tests/HomeControllerTests.cs diff --git a/src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs b/src/test/Yavsc.Api.Tests/RdvQueryApiControllerTests.cs similarity index 100% rename from src/Yavsc.Api.Test/RdvQueryApiControllerTests.cs rename to src/test/Yavsc.Api.Tests/RdvQueryApiControllerTests.cs diff --git a/src/Yavsc.Api.Test/Yavsc.Api.Test.csproj b/src/test/Yavsc.Api.Tests/Yavsc.Api.Tests.csproj similarity index 63% rename from src/Yavsc.Api.Test/Yavsc.Api.Test.csproj rename to src/test/Yavsc.Api.Tests/Yavsc.Api.Tests.csproj index ebe96dfa8..cf41a7d4b 100644 --- a/src/Yavsc.Api.Test/Yavsc.Api.Test.csproj +++ b/src/test/Yavsc.Api.Tests/Yavsc.Api.Tests.csproj @@ -4,12 +4,12 @@ enable enable false - Yavsc.Api.Test + Yavsc.Api.Tests true 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 @@ -19,13 +19,12 @@ - - - - - + + + + diff --git a/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs b/src/test/Yavsc.Blogs.Tests/BlogAclApiTests.cs similarity index 98% rename from src/Yavsc.Blogs.Tests/BlogAclApiTests.cs rename to src/test/Yavsc.Blogs.Tests/BlogAclApiTests.cs index 174838173..e47ab4e57 100644 --- a/src/Yavsc.Blogs.Tests/BlogAclApiTests.cs +++ b/src/test/Yavsc.Blogs.Tests/BlogAclApiTests.cs @@ -13,7 +13,7 @@ using static Yavsc.Constants; namespace Yavsc.Blogs.Tests; /// -/// Behavioural tests for BlogAclApiController.PostCircleAuthorizationToBlogPost: +/// Behavioral tests for BlogAclApiController.PostCircleAuthorizationToBlogPost: /// POST /api/v1/blogacl with a JSON body of /// CircleAuthorizationToBlogPost (CircleId + BlogPostId). /// @@ -84,7 +84,7 @@ public sealed class BlogAclApiTests : IClassFixture // rather than being rewritten to ClaimTypes.NameIdentifier. // The controller, however, reads the user id via // User.FindFirstValue(ClaimTypes.NameIdentifier), so we add - // an explicit nameid claim to keep the legacy lookup happy. + // an explicit NameIdentifier claim to keep the legacy lookup happy. http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Bearer", @@ -106,7 +106,7 @@ public sealed class BlogAclApiTests : IClassFixture /// when POSTs the /// shape { "circleId": <id> } — the exact body the /// PostIt client builds from - /// (which only carries CircleId). The server deserialises + /// (which only carries CircleId). The server deserializes /// it into , leaves /// BlogPostId at its default(long) = 0, attaches /// no Target navigation, and EF Core refuses to INSERT diff --git a/src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs b/src/test/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs rename to src/test/Yavsc.Blogs.Tests/BlogApiMappedClaimsTests.cs diff --git a/src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs b/src/test/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs rename to src/test/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs diff --git a/src/Yavsc.Blogs.Tests/BlogApiTests.cs b/src/test/Yavsc.Blogs.Tests/BlogApiTests.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/BlogApiTests.cs rename to src/test/Yavsc.Blogs.Tests/BlogApiTests.cs diff --git a/src/test/Yavsc.Blogs.Tests/BlogSpotServiceIndexTests.cs b/src/test/Yavsc.Blogs.Tests/BlogSpotServiceIndexTests.cs new file mode 100644 index 000000000..fa1b9c379 --- /dev/null +++ b/src/test/Yavsc.Blogs.Tests/BlogSpotServiceIndexTests.cs @@ -0,0 +1,164 @@ +using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; +using Yavsc.Blogspot; +using Yavsc.Models; +using Yavsc.Models.Blog; +using Yavsc.Blogs.Tests.Fixtures; + +namespace Yavsc.Blogs.Tests; + +/// +/// Direct service-level tests for . +/// +/// The HTTP tests in all drive the +/// [Authorize("BlogScope")]-protected BlogApiController, +/// so the request is always authenticated by the time it reaches the +/// service. The MVC BlogspotController.Index action, by +/// contrast, is [AllowAnonymous] — that is the one caller that +/// actually exercises the unauthenticated branch of +/// , the branch that reads from +/// blogSpotPublications and projects to p.Post. +/// +/// These tests pin that behaviour at the service layer: a +/// BlogPost that has a BlogSpotPublication row is +/// listed for an anonymous viewer, and one without is not. Seeding +/// runs against the same shared SQLite store as the rest of the +/// Yavsc Blogs collection, so the real +/// (registered by +/// ) is resolved from DI and run +/// end-to-end — no mocking. +/// +[Collection("Yavsc Blogs")] +public sealed class BlogSpotServiceIndexTests : IClassFixture +{ + private readonly BlogsWebServerFixture _fixture; + + public BlogSpotServiceIndexTests(BlogsWebServerFixture fixture) + { + _fixture = fixture; + } + + /// An unauthenticated principal: a + /// with no authentication type has + /// == false, so + /// takes its anonymous + /// branch. This mirrors what the JwtBearer middleware hands the + /// MVC [AllowAnonymous] action when no bearer token is + /// presented. + private static ClaimsPrincipal Anonymous() + => new(new ClaimsIdentity()); + + /// Resolve the real from the + /// fixture's DI container and run against it, + /// disposing the request scope (and its scoped DbContext) afterwards. + /// The service itself is not ; the scope is, + /// so the scope is what we must dispose to avoid leaking a DbContext + /// against the shared SQLite connection across tests. + private async Task WithServiceAsync( + Func> body) + { + using var scope = _fixture.Services.CreateScope(); + var service = scope.ServiceProvider.GetRequiredService(); + return await body(service); + } + + private void Publish(long postId) + { + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + // The publication flag is the existence of a row in + // blogSpotPublications — same write SetPublishAsync performs + // in production. Inserting it directly is the most faithful + // way to seed "this post is published" without routing + // through the authorize-gated PUT/publish endpoint. + db.blogSpotPublications.Add(new BlogSpotPublication { PostId = postId }); + db.SaveChanges(); + } + + [Fact] + public async Task Index_lists_a_published_post_for_anonymous_access() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + var publishedId = _fixture.SeedBlogPost("alice", "Published for all"); + + Publish(publishedId); + + var posts = await WithServiceAsync(async svc => + { + var result = await svc.Index(Anonymous(), id: "", skip: 0, take: 25); + return result.OfType().ToArray(); + }); + + var listed = Assert.Single(posts); + Assert.Equal("Published for all", listed.Title); + // The service serves IsPublished from the + // blogSpotPublications table — the anonymous branch does + // not Include the Publication navigation, so this proves + // the value is hydrated by the service, not derived from + // the EF graph. + Assert.True(listed.IsPublished); + } + + [Fact] + public async Task Index_serves_IsPublished_false_for_a_draft_for_anonymous_access() + { + // Anonymous viewers never receive a draft (the branch + // queries blogSpotPublications), so this is checked through + // the authenticated branch: the service must still serve + // IsPublished == false for a post with no publication row, + // not the default of a freshly loaded entity. + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + var draftId = _fixture.SeedBlogPost("alice", "Draft, not published"); + + var viewer = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim("sub", "alice")], authenticationType: "Bearer")); + + var posts = await WithServiceAsync(async svc => + { + var result = await svc.Index(viewer, id: "", skip: 0, take: 25); + return result.OfType().ToArray(); + }); + + var draft = Assert.Single(posts); + Assert.Equal(draftId, draft.Id); + Assert.False(draft.IsPublished); + } + + [Fact] + public async Task Index_does_not_list_an_unpublished_post_for_anonymous_access() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + var publishedId = _fixture.SeedBlogPost("alice", "Published for all"); + _fixture.SeedBlogPost("alice", "Draft, not published"); + + // Only the first post is published; the draft has no + // BlogSpotPublication row. + Publish(publishedId); + + var titles = await WithServiceAsync(async svc => + { + var result = await svc.Index(Anonymous(), id: "", skip: 0, take: 25); + return result.Select(p => p.Title).ToArray(); + }); + + Assert.Contains("Published for all", titles); + Assert.DoesNotContain("Draft, not published", titles); + } + + [Fact] + public async Task Index_returns_an_empty_list_for_anonymous_access_when_nothing_is_published() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + // A draft with no publication row: invisible to anonymous. + _fixture.SeedBlogPost("alice", "Draft, not published"); + + var result = await WithServiceAsync(svc => + svc.Index(Anonymous(), id: "", skip: 0, take: 25)); + + Assert.Empty(result); + } +} \ No newline at end of file diff --git a/src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs b/src/test/Yavsc.Blogs.Tests/CircleMembersApiTests.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/CircleMembersApiTests.cs rename to src/test/Yavsc.Blogs.Tests/CircleMembersApiTests.cs diff --git a/src/Yavsc.Blogs.Tests/CreateMultipartContentTests.cs b/src/test/Yavsc.Blogs.Tests/CreateMultipartContentTests.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/CreateMultipartContentTests.cs rename to src/test/Yavsc.Blogs.Tests/CreateMultipartContentTests.cs diff --git a/src/test/Yavsc.Blogs.Tests/FileSystemApiTests.cs b/src/test/Yavsc.Blogs.Tests/FileSystemApiTests.cs new file mode 100644 index 000000000..0e36a6018 --- /dev/null +++ b/src/test/Yavsc.Blogs.Tests/FileSystemApiTests.cs @@ -0,0 +1,397 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Yavsc.Blogs.Tests.Fixtures; +using Yavsc.Models; +using Yavsc.Models.Billing; +using Yavsc.Models.Blog; +using Yavsc.Models.Relationship; +using Yavsc.Models.Workflow; +using Yavsc.Tests.Shared; + +namespace Yavsc.Blogs.Tests; + +/// +/// Integration tests for the personal-storage file endpoints +/// (api/v1/fs) served by FileSystemApiController on the +/// Blogs host. +/// +/// Two concerns: +/// +/// Upload records metadata: a multipart +/// POST api/v1/fs persists an row +/// owned by the caller and returns its fileId, so the client +/// can later attach that file to an estimate/query by +/// reference. +/// Download authorization: +/// GET api/v1/fs/file/{fileId} serves the bytes from the +/// owner's personal space. The owner may always read; a party +/// (client or provider) of an estimate/query the file is attached +/// to may read; a third party is refused. The bytes live on the +/// Blogs host's disk — this is the cross-container reason the +/// download endpoint lives here and not on the API host. +/// +/// +[Collection("Yavsc Blogs")] +public sealed class FileSystemApiTests : IClassFixture +{ + private readonly BlogsWebServerFixture _fixture; + + public FileSystemApiTests(BlogsWebServerFixture fixture) + { + _fixture = fixture; + } + + private HttpClient NewClient(string subject = "alice", string? name = null) + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + var http = new HttpClient(handler) + { + BaseAddress = new Uri(_fixture.Addresses.First(a => a.StartsWith("https://"))) + }; + // Pass a distinct name to mirror production (sub = GUID, name = + // login); defaults to subject for the existing tests where the + // two are equal. + http.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, name: name)); + return http; + } + + private static string PhysicalFile(BlogsWebServerFixture fixture, string owner, string fileName) => + Path.Combine(fixture.BlogFilesRoot, owner, fileName); + + /// Write a physical file into the owner's personal space, + /// so GetFile can stream it back. Creates the directory if + /// needed. + private static void WritePhysicalFile(BlogsWebServerFixture fixture, string owner, string fileName, byte[] content) + { + var path = PhysicalFile(fixture, owner, fileName); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllBytes(path, content); + } + + private long SeedUploadedFile(string ownerId, string path, string contentType, long length) + { + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var file = new UploadedFile + { + OwnerId = ownerId, + Path = path, + ContentType = contentType, + Length = length, + }; + db.UploadedFiles.Add(file); + db.SaveChanges(); + return file.Id; + } + + [Fact] + public async Task Post_fs_creates_an_UploadedFile_row_owned_by_the_caller() + { + _fixture.ResetDatabase(); + // DiskQuota must be non-zero: ReceiveUserFile flags QuotaOffense + // once usage >= DiskQuota, and the controller skips the metadata + // row when QuotaOffense is set. A fresh user has quota 0. + _fixture.SeedUser("alice", u => u.DiskQuota = 10_000_000); + + var payload = System.Text.Encoding.UTF8.GetBytes("hello personal storage"); + + using var form = new MultipartFormDataContent(); + var fileContent = new ByteArrayContent(payload); + fileContent.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + form.Add(fileContent, "file", "note.txt"); + + using var http = NewClient("alice"); + var response = await http.PostAsync("/api/v1/fs", form, TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, + $"Expected 200, got {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var doc = JsonDocument.Parse(body); + var item = Assert.Single(doc.RootElement.EnumerateArray()); + var fileId = item.GetProperty("fileId").GetInt64(); + Assert.True(fileId > 0, $"Expected a positive fileId, got {fileId}. Body: {body}"); + Assert.Equal("note.txt", item.GetProperty("fileName").GetString()); + Assert.False(item.GetProperty("quotaOffense").GetBoolean()); + + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var row = Assert.Single(db.UploadedFiles.Where(u => u.OwnerId == "alice")); + Assert.Equal("note.txt", row.Path); + Assert.Equal("text/plain", row.ContentType); + Assert.Equal(payload.Length, row.Length); + + // The file physically landed under {BlogFilesRoot}/alice/note.txt. + Assert.True(File.Exists(PhysicalFile(_fixture, "alice", "note.txt")), + "Expected the uploaded file on disk under the owner's personal root"); + } + + /// + /// Regression for the "Mes fichiers" page listing nothing: the + /// personal-storage tree is keyed by the user's login + /// (UserName) — EnsureDestinationDirectory writes + /// under {Blog}/{name-claim}, the download endpoint and + /// avatar paths use user.UserName — but GET api/v1/fs + /// passed User.GetUserId() (the sub claim, a GUID in + /// production) to GetUserFiles, so the listing looked under + /// {Blog}/{sub-GUID}, an empty tree. This test decouples + /// sub from the login (mirroring production) and asserts the + /// listing returns the file stored under the login. + /// + [Fact] + public async Task Get_fs_lists_files_stored_under_the_user_login() + { + _fixture.ResetDatabase(); + // sub = "alice-sub" (GUID-like), login = "alice" — the shape + // ProfileService mints in production. DiskQuota non-zero so the + // upload persists the UploadedFile row (see Post_fs test). + _fixture.SeedUser("alice", u => + { + u.Id = "alice-sub"; + u.DiskQuota = 10_000_000; + }); + + var payload = System.Text.Encoding.UTF8.GetBytes("list me"); + + using var form = new MultipartFormDataContent(); + var fileContent = new ByteArrayContent(payload); + fileContent.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + form.Add(fileContent, "file", "note.txt"); + + // Upload with a token whose sub is the GUID-like id and whose + // name is the login — EnsureDestinationDirectory keys off "name". + using (var http = NewClient("alice-sub", name: "alice")) + { + var post = await http.PostAsync("/api/v1/fs", form, TestContext.Current.CancellationToken); + var postBody = await post.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(post.StatusCode == HttpStatusCode.OK, + $"Upload failed: {(int)post.StatusCode} ({post.StatusCode}): {postBody}"); + } + + // The file landed under the login, not the subject. + Assert.True(File.Exists(PhysicalFile(_fixture, "alice", "note.txt")), + "Expected the uploaded file under {BlogFilesRoot}/alice/note.txt (the login)"); + Assert.False(Directory.Exists(Path.Combine(_fixture.BlogFilesRoot, "alice-sub")), + "The subject-named directory must NOT exist — storage is keyed by the login"); + + // GET /api/v1/fs must list under the login and find the file. + using var get = NewClient("alice-sub", name: "alice"); + var response = await get.GetAsync("/api/v1/fs", TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + Assert.True(response.StatusCode == HttpStatusCode.OK, + $"List failed: {(int)response.StatusCode} ({response.StatusCode}): {body}"); + + using var doc = JsonDocument.Parse(body); + Assert.True(doc.RootElement.TryGetProperty("files", out var filesEl), + $"Response has no 'files' property. Body: {body}"); + var names = filesEl.EnumerateArray().Select(f => f.GetProperty("name").GetString()).ToArray(); + Assert.Contains("note.txt", names); + + // The UploadedFile row (OwnerId = sub) is enriched onto the + // entry, so the client can attach the file by reference. + var noteEntry = filesEl.EnumerateArray() + .First(f => f.GetProperty("name").GetString() == "note.txt"); + Assert.True(noteEntry.TryGetProperty("id", out var idEl) && idEl.GetInt64() > 0, + "Expected the enriched UploadedFile id on the listed entry"); + } + + [Fact] + public async Task GetFile_owner_can_download_their_own_file() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + + var bytes = System.Text.Encoding.UTF8.GetBytes("owner payload"); + WritePhysicalFile(_fixture, "alice", "owner-note.txt", bytes); + var fileId = SeedUploadedFile("alice", "owner-note.txt", "text/plain", bytes.Length); + + using var http = NewClient("alice"); + var response = await http.GetAsync( + $"/api/v1/fs/file/{fileId}", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var downloaded = await response.Content.ReadAsByteArrayAsync(TestContext.Current.CancellationToken); + Assert.Equal(bytes, downloaded); + } + + [Fact] + public async Task GetFile_refuses_a_third_party() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + _fixture.SeedUser("carol"); + + var bytes = System.Text.Encoding.UTF8.GetBytes("private to alice"); + WritePhysicalFile(_fixture, "alice", "secret.txt", bytes); + var fileId = SeedUploadedFile("alice", "secret.txt", "text/plain", bytes.Length); + + // carol is neither the owner nor a party to any estimate/query + // this file is attached to. + using var http = NewClient("carol"); + var response = await http.GetAsync( + $"/api/v1/fs/file/{fileId}", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + /// + /// Seed the graph needed to prove a party (the client of an + /// estimate the file is attached to) can download a file they do + /// not own: alice owns the file and is the provider; bob is the + /// client; the file is attached to the estimate by reference. + /// + private long SeedPartyDownloadGraph(out long estimateId, out string fileName) + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + _fixture.SeedUser("bob"); + + fileName = "party-spec.pdf"; + var bytes = System.Text.Encoding.UTF8.GetBytes("party payload"); + + using var scope = _fixture.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // Location + PerformerProfile(alice): Estimate.OwnerId is a FK + // to PerformerProfile.PerformerId, so the provider profile must + // exist before the estimate can be inserted. + var location = new Location { Address = "1 rue du Test", Latitude = 48.8, Longitude = 2.3 }; + db.Locations.Add(location); + db.SaveChanges(); + + db.Performers.Add(new PerformerProfile + { + PerformerId = "alice", + SIREN = "12345678901234", + OrganizationAddressId = location.Id, + ExerciseCountryCode = "fr", + AcceptNotifications = true, + AcceptPublicContact = true, + Active = true, + Rate = 5, + WebSite = "https://alice.dev", + }); + db.SaveChanges(); + + var estimate = new Estimate + { + OwnerId = "alice", + ClientId = "bob", + CommandType = "Rdv", + Title = "Devis party test", + Description = "Devis", + AttachedFiles = new List(), + AttachedGraphics = new List(), + Bill = new List + { + new() { Name = "Prestation", Description = "Prestation", Count = 1, UnitaryCost = 10m, Currency = "EUR" }, + }, + }; + db.Estimates.Add(estimate); + db.SaveChanges(); + estimateId = estimate.Id; + + var file = new UploadedFile + { + OwnerId = "alice", + Path = fileName, + ContentType = "application/pdf", + Length = bytes.Length, + }; + db.UploadedFiles.Add(file); + db.SaveChanges(); + + db.EstimateAttachedFiles.Add(new EstimateAttachedFile + { + FileId = file.Id, + EstimateId = estimate.Id, + }); + db.SaveChanges(); + + // The bytes must exist on disk for GetFile to stream them. + WritePhysicalFile(_fixture, "alice", fileName, bytes); + return file.Id; + } + + [Fact] + public async Task GetFile_party_client_of_an_attached_estimate_can_download() + { + var fileId = SeedPartyDownloadGraph(out _, out _); + + // bob is the client of the estimate the file is attached to: + // a party, authorized to read even though alice owns the file. + using var http = NewClient("bob"); + var response = await http.GetAsync( + $"/api/v1/fs/file/{fileId}", TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.True(response.StatusCode == HttpStatusCode.OK, + $"Expected 200, got {(int)response.StatusCode} ({response.StatusCode}): {body}"); + var downloaded = await response.Content.ReadAsByteArrayAsync(TestContext.Current.CancellationToken); + Assert.Equal(System.Text.Encoding.UTF8.GetBytes("party payload"), downloaded); + } + + [Fact] + public async Task GetFile_party_provider_of_an_attached_estimate_can_download() + { + var fileId = SeedPartyDownloadGraph(out _, out _); + + // alice is the owner too, so this also exercises the owner path; + // the provider of an attached estimate is authorized regardless. + using var http = NewClient("alice"); + var response = await http.GetAsync( + $"/api/v1/fs/file/{fileId}", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task GetFile_unknown_id_returns_404() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + + using var http = NewClient("alice"); + var response = await http.GetAsync( + "/api/v1/fs/file/999999", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + /// + /// Regression for the "delete returns 40x yet the file is gone" + /// report: DeleteUserDirOrFile read FileInfo.Length + /// after fi.Delete(), and FileInfo.Length + /// throws FileNotFoundException once the file no longer + /// exists. The controller's catch turned that into a 400 + /// even though the file had already been removed — the user saw + /// "le fichier n'existe pas sur le serveur" while the delete in + /// fact succeeded. This test asserts a clean 200 and that the + /// file is gone from disk. + /// + [Fact] + public async Task Delete_fs_removes_the_file_and_returns_200() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + + var bytes = System.Text.Encoding.UTF8.GetBytes("delete me"); + WritePhysicalFile(_fixture, "alice", "doomed.txt", bytes); + + using var http = NewClient("alice"); + var response = await http.DeleteAsync( + "/api/v1/fs/doomed.txt", TestContext.Current.CancellationToken); + var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + Assert.True(response.StatusCode == HttpStatusCode.OK, + $"Expected 200, got {(int)response.StatusCode} ({response.StatusCode}): {body}"); + Assert.False(File.Exists(PhysicalFile(_fixture, "alice", "doomed.txt")), + "The file must be gone from disk after a successful delete."); + } +} \ No newline at end of file diff --git a/src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs b/src/test/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs rename to src/test/Yavsc.Blogs.Tests/Fixtures/BlogsWebServerFixture.cs diff --git a/src/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs b/src/test/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs similarity index 97% rename from src/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs rename to src/test/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs index ab4418c57..df977dc4e 100644 --- a/src/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs +++ b/src/test/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs @@ -33,7 +33,7 @@ public sealed class MappedClaimsBlogsWebServerFixture : IDisposable, IBackendFix JwtSecurityTokenHandler.DefaultInboundClaimTypeMap["sub"] = ClaimTypes.NameIdentifier; var builder = WebApplication.CreateBuilder(); - builder.WebHost.UseUrls("http://127.0.0.1:5104"); + builder.WebHost.UseUrls("http://127.0.0.1:5105"); builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("Yavsc.Blogs.Tests.MappedClaims", _inMemoryRoot)); @@ -88,7 +88,7 @@ public sealed class MappedClaimsBlogsWebServerFixture : IDisposable, IBackendFix _app.MapControllers(); _app.StartAsync().GetAwaiter().GetResult(); - Addresses = ["http://127.0.0.1:5104"]; + Addresses = ["http://127.0.0.1:5105"]; Services = _app.Services; } diff --git a/src/Yavsc.Blogs.Tests/Fixtures/TestApiTransport.cs b/src/test/Yavsc.Blogs.Tests/Fixtures/TestApiTransport.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/Fixtures/TestApiTransport.cs rename to src/test/Yavsc.Blogs.Tests/Fixtures/TestApiTransport.cs diff --git a/src/test/Yavsc.Blogs.Tests/IBlogPostWireShapeTests.cs b/src/test/Yavsc.Blogs.Tests/IBlogPostWireShapeTests.cs new file mode 100644 index 000000000..bb38725cd --- /dev/null +++ b/src/test/Yavsc.Blogs.Tests/IBlogPostWireShapeTests.cs @@ -0,0 +1,87 @@ +using System.Text.Json; +using Yavsc.Blogspot; +using Yavsc.Models.Blog; + +namespace Yavsc.Blogs.Tests; + +/// +/// Wire-shape tests pinning that +/// is carried on the JSON the blog API list endpoint emits. +/// +/// The blog API host (Yavsc.Blogs/Program.cs) serialises +/// with System.Text.Json (the .AddControllers() default — no +/// AddNewtonsoftJson), and BlogApiController.GetBlogspot +/// returns IEnumerable<IBlogPost>. System.Text.Json emits +/// only the members of the declared type, so a property that +/// exists solely on the concrete BlogPost entity is dropped from +/// the payload. IsPublished used to be one such property: it was +/// on BlogPost and BlogPostDto but not on +/// , so the list response omitted it and PostIt +/// read IsPublished == false for every post — including +/// published ones. +/// +/// These tests reproduce that serialisation path directly +/// (declared type IEnumerable<IBlogPost> through +/// ) and assert +/// isPublished survives the round trip. They live in +/// Yavsc.Blogs.Tests — the assembly that owns the blog API +/// contract — so a regression at the wire boundary is caught here, +/// where it manifested in production (PostIt showing a published +/// post's status as false). +/// +public sealed class IBlogPostWireShapeTests +{ + private static readonly JsonSerializerOptions WebDefaults + = new(JsonSerializerDefaults.Web); + + [Fact] + public void IsPublished_is_emitted_when_list_is_serialised_as_IEnumerable_IBlogPost() + { + var post = new BlogPost + { + Id = 13, + Title = "Published", + AuthorId = "alice", + Article = "x", + }; + post.IsPublished = true; + + // Declared type mirrors GetBlogspot's return: IEnumerable. + IEnumerable list = new List { post }; + + // Serialize with the DECLARED type, the way ASP.NET Core's + // SystemTextJsonOutputFormatter hands context.ObjectType (the + // action return type) to JsonSerializer.Serialize. + var json = JsonSerializer.Serialize(list, typeof(IEnumerable), + WebDefaults); + + using var doc = JsonDocument.Parse(json); + var element = doc.RootElement[0]; + // Web defaults use camelCase, so the property is "isPublished". + Assert.True(element.TryGetProperty("isPublished", out var isPublished), + $"isPublished was dropped from the list payload. JSON: {json}"); + Assert.True(isPublished.GetBoolean()); + } + + [Fact] + public void IsPublished_round_trips_through_BlogPostDto_on_the_client() + { + // The shape the client (PostIt) receives and deserialises with + // PropertyNameCaseInsensitive = true (cf. YavscApiClient). + var json = """ + [{ + "id": 13, + "title": "Published", + "article": "x", + "authorId": "alice", + "isPublished": true + }] + """; + + var posts = JsonSerializer.Deserialize>(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + + var post = Assert.Single(posts); + Assert.True(post.IsPublished); + } +} \ No newline at end of file diff --git a/src/Yavsc.Blogs.Tests/JwtClaimMappingCollection.cs b/src/test/Yavsc.Blogs.Tests/JwtClaimMappingCollection.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/JwtClaimMappingCollection.cs rename to src/test/Yavsc.Blogs.Tests/JwtClaimMappingCollection.cs diff --git a/src/Yavsc.Blogs.Tests/PublishEndpointTests.cs b/src/test/Yavsc.Blogs.Tests/PublishEndpointTests.cs similarity index 100% rename from src/Yavsc.Blogs.Tests/PublishEndpointTests.cs rename to src/test/Yavsc.Blogs.Tests/PublishEndpointTests.cs diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/test/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj similarity index 67% rename from src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj rename to src/test/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj index 590f27867..28934efaf 100644 --- a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj +++ b/src/test/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj @@ -9,8 +9,8 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 @@ -21,16 +21,15 @@ - - - - - + + + + - \ No newline at end of file + diff --git a/src/Yavsc.Org.Tests/ComputeKidTests.cs b/src/test/Yavsc.Org.Tests/ComputeKidTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/ComputeKidTests.cs rename to src/test/Yavsc.Org.Tests/ComputeKidTests.cs diff --git a/src/Yavsc.Org.Tests/Controllers/ClientControllerCollectionTests.cs b/src/test/Yavsc.Org.Tests/Controllers/ClientControllerCollectionTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/Controllers/ClientControllerCollectionTests.cs rename to src/test/Yavsc.Org.Tests/Controllers/ClientControllerCollectionTests.cs diff --git a/src/Yavsc.Org.Tests/Controllers/CommandFormsControllerTests.cs b/src/test/Yavsc.Org.Tests/Controllers/CommandFormsControllerTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/Controllers/CommandFormsControllerTests.cs rename to src/test/Yavsc.Org.Tests/Controllers/CommandFormsControllerTests.cs diff --git a/src/Yavsc.Org.Tests/Controllers/CommentsApiIntegrationTests.cs b/src/test/Yavsc.Org.Tests/Controllers/CommentsApiIntegrationTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/Controllers/CommentsApiIntegrationTests.cs rename to src/test/Yavsc.Org.Tests/Controllers/CommentsApiIntegrationTests.cs diff --git a/src/Yavsc.Org.Tests/Controllers/CommentsControllerTests.cs b/src/test/Yavsc.Org.Tests/Controllers/CommentsControllerTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/Controllers/CommentsControllerTests.cs rename to src/test/Yavsc.Org.Tests/Controllers/CommentsControllerTests.cs diff --git a/src/Yavsc.Org.Tests/Controllers/TestWebApplicationFactoryIsolationTests.cs b/src/test/Yavsc.Org.Tests/Controllers/TestWebApplicationFactoryIsolationTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/Controllers/TestWebApplicationFactoryIsolationTests.cs rename to src/test/Yavsc.Org.Tests/Controllers/TestWebApplicationFactoryIsolationTests.cs diff --git a/src/Yavsc.Org.Tests/DictionnaireMetierTests.cs b/src/test/Yavsc.Org.Tests/DictionnaireMetierTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/DictionnaireMetierTests.cs rename to src/test/Yavsc.Org.Tests/DictionnaireMetierTests.cs diff --git a/src/Yavsc.Org.Tests/DumpHtml.cs b/src/test/Yavsc.Org.Tests/DumpHtml.cs similarity index 100% rename from src/Yavsc.Org.Tests/DumpHtml.cs rename to src/test/Yavsc.Org.Tests/DumpHtml.cs diff --git a/src/Yavsc.Org.Tests/Fakes/RecordingSmtpClient.cs b/src/test/Yavsc.Org.Tests/Fakes/RecordingSmtpClient.cs similarity index 100% rename from src/Yavsc.Org.Tests/Fakes/RecordingSmtpClient.cs rename to src/test/Yavsc.Org.Tests/Fakes/RecordingSmtpClient.cs diff --git a/src/Yavsc.Org.Tests/Fakes/RecordingSmtpClientFactory.cs b/src/test/Yavsc.Org.Tests/Fakes/RecordingSmtpClientFactory.cs similarity index 100% rename from src/Yavsc.Org.Tests/Fakes/RecordingSmtpClientFactory.cs rename to src/test/Yavsc.Org.Tests/Fakes/RecordingSmtpClientFactory.cs diff --git a/src/Yavsc.Org.Tests/Mandatory/BatchTests.cs b/src/test/Yavsc.Org.Tests/Mandatory/BatchTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/Mandatory/BatchTests.cs rename to src/test/Yavsc.Org.Tests/Mandatory/BatchTests.cs diff --git a/src/Yavsc.Org.Tests/Mandatory/Remoting.cs b/src/test/Yavsc.Org.Tests/Mandatory/Remoting.cs similarity index 59% rename from src/Yavsc.Org.Tests/Mandatory/Remoting.cs rename to src/test/Yavsc.Org.Tests/Mandatory/Remoting.cs index 7def1a43f..5caf905b4 100644 --- a/src/Yavsc.Org.Tests/Mandatory/Remoting.cs +++ b/src/test/Yavsc.Org.Tests/Mandatory/Remoting.cs @@ -1,7 +1,10 @@ using System.Security.Cryptography.X509Certificates; using System.Net.Security; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using IdentityModel.Client; using Xunit; +using Yavsc.Server.Helpers; namespace Yavsc.Org.Tests { @@ -70,6 +73,72 @@ namespace Yavsc.Org.Tests } + /// + /// Regression for the MyFiles upload 500 / null + /// User.Identity.Name: the access token minted by the + /// real IdentityServer (resource-owner password grant) must + /// carry a name claim whose value is the user's login + /// (UserName), and + /// — which reads FindFirstValue("name") — must resolve + /// to it on a principal built the way + /// AddYavscJwtBearer builds one + /// (MapInboundClaims=false, NameClaimType="name"). + /// + /// Prerequisites wired by : + /// the test client's AllowedScopes include + /// openid+profile, and the standard identity + /// resources are seeded by the production pipeline. Requesting + /// openid profile makes ProfileService emit + /// name = user.UserName. + /// + [Fact] + public async Task PasswordToken_carries_name_claim_equal_to_login() + { + var serverUrl = GetServerUrl(); + var cancellationToken = TestContext.Current.CancellationToken; + + var client = NewHttpClient(); + var tokenEndpoint = await ResolveTokenEndpointAsync(client, serverUrl, cancellationToken); + + var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest + { + Address = tokenEndpoint, + ClientId = RequireNonEmpty(_serverFixture.TestClientId, nameof(_serverFixture.TestClientId)), + ClientSecret = RequireNonEmpty(_serverFixture.TestClientSecret, nameof(_serverFixture.TestClientSecret)), + UserName = RequireNonEmpty(_serverFixture.TestingUserName, nameof(_serverFixture.TestingUserName)), + Password = RequireNonEmpty(_serverFixture.TestingUserPassword, nameof(_serverFixture.TestingUserPassword)), + // "openid profile" triggers ProfileService to emit the + // "name" claim (user.UserName). Without profile, the + // token carries only sub + scopes and GetUserName() + // resolves to null — the exact regression behind the + // MyFiles upload 500. "test" is the API scope, so a real + // JWT access token (not just an id_token) is issued. + Scope = "openid profile test" + }, cancellationToken); + + Assert.False(response.IsError, + $"Token request failed: {response.Error} ({response.ErrorDescription})"); + + // Decode the access token straight from the payload so short + // claim names ("name", "sub") stay as-emitted — mirrors + // AddYavscJwtBearer with MapInboundClaims=false. + var jwt = new JwtSecurityToken(response.AccessToken); + var nameClaim = jwt.Claims.FirstOrDefault(c => c.Type == "name"); + Assert.True(nameClaim is not null, + "access_token has no 'name' claim; ProfileService did not emit it " + + "(is the 'profile' scope in the client's AllowedScopes and requested?)."); + Assert.Equal(_serverFixture.TestingUserName, nameClaim!.Value); + + // Rebuild the principal the way a bearer consumer host does + // and exercise the real GetUserName() helper used by the + // file-system / avatar paths. + var identity = new ClaimsIdentity( + jwt.Claims, authenticationType: "Bearer", + nameType: "name", roleType: null); + var principal = new ClaimsPrincipal(identity); + Assert.Equal(_serverFixture.TestingUserName, principal.GetUserName()); + } + [Fact] public async Task GetSignin_returns_a_page() { diff --git a/src/Yavsc.Org.Tests/Mandatory/Resources.cs b/src/test/Yavsc.Org.Tests/Mandatory/Resources.cs similarity index 100% rename from src/Yavsc.Org.Tests/Mandatory/Resources.cs rename to src/test/Yavsc.Org.Tests/Mandatory/Resources.cs diff --git a/src/Yavsc.Org.Tests/Mandatory/Services.cs b/src/test/Yavsc.Org.Tests/Mandatory/Services.cs similarity index 100% rename from src/Yavsc.Org.Tests/Mandatory/Services.cs rename to src/test/Yavsc.Org.Tests/Mandatory/Services.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/AbstractTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/AbstractTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/AbstractTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/AbstractTests.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/ApplicationUserDisplayTemplateTests.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/AvatarFallbackTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/AvatarFallbackTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/AvatarFallbackTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/AvatarFallbackTests.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs similarity index 71% rename from src/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs index 4e8ae0d6f..6504be24d 100644 --- a/src/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs +++ b/src/test/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs @@ -8,6 +8,16 @@ using Xunit; namespace Yavsc { + // BillingService and Config expose static mutable registries that + // WorkflowHelpers.ConfigureBillingService() also mutates from the + // WebServerFixture host build (HostingExtensions.cs). Without a + // collection, xUnit runs this class in parallel with the + // "Yavsc Server" collection, and the host build's ConfigureBilling + // call can land between this test's two ConfigureBilling calls, + // breaking the idempotency count assertion (a flaky failure that + // surfaces whenever the fixture's timing shifts). Pinning the class + // to the same collection serializes it with the host build. + [Collection("Yavsc Server")] [Trait("regression", "II")] public class BillingServiceTests { diff --git a/src/Yavsc.Org.Tests/NonRegression/Database.cs b/src/test/Yavsc.Org.Tests/NonRegression/Database.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/Database.cs rename to src/test/Yavsc.Org.Tests/NonRegression/Database.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/EMailling.cs b/src/test/Yavsc.Org.Tests/NonRegression/EMailling.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/EMailling.cs rename to src/test/Yavsc.Org.Tests/NonRegression/EMailling.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/FileServerUrlHelpersTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/FileServerUrlHelpersTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/FileServerUrlHelpersTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/FileServerUrlHelpersTests.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/OidcSeedTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/OidcSeedTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/OidcSeedTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/OidcSeedTests.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/PerformerCodeInputValidationTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/PerformerCodeInputValidationTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/PerformerCodeInputValidationTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/PerformerCodeInputValidationTests.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/SetActivityCountryValidationViewTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/SetActivityCountryValidationViewTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/SetActivityCountryValidationViewTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/SetActivityCountryValidationViewTests.cs diff --git a/src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs b/src/test/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs rename to src/test/Yavsc.Org.Tests/NonRegression/UserDisplayHelpersTests.cs diff --git a/src/Yavsc.Org.Tests/Resources/Test.TestResources.resx b/src/test/Yavsc.Org.Tests/Resources/Test.TestResources.resx similarity index 100% rename from src/Yavsc.Org.Tests/Resources/Test.TestResources.resx rename to src/test/Yavsc.Org.Tests/Resources/Test.TestResources.resx diff --git a/src/Yavsc.Org.Tests/Services/FileSystemAuthManagerTests.cs b/src/test/Yavsc.Org.Tests/Services/FileSystemAuthManagerTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/Services/FileSystemAuthManagerTests.cs rename to src/test/Yavsc.Org.Tests/Services/FileSystemAuthManagerTests.cs diff --git a/src/test/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs b/src/test/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs new file mode 100644 index 000000000..4a7600596 --- /dev/null +++ b/src/test/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs @@ -0,0 +1,91 @@ +using System.Net; +using System.Net.Http; +using IdentityServer8.Stores; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Yavsc.Tests.Shared; +using Xunit; + +namespace Yavsc.Org.Tests.Smoke; + +/// +/// Smoke for the Account BC (login / registration / OIDC discovery). +/// Hits the in-memory test server through +/// : no sockets, no +/// self-signed certificates, no real DB (the +/// AddInMemoryCollection in WebServerFixture.SetupHost +/// points the EF context at the in-memory provider). +/// +/// GET /signin is the public login endpoint served by +/// Yavsc.Org/Controllers/Accounting/AccountController +/// (decorated [HttpGet(YavscConstants.SigninPath)] with +/// YavscConstants.SigninPath = "~/signin"). A 200 means the +/// entire pipeline (routing + Razor + IdentityServer + EF + DI) +/// is wired correctly end-to-end. +/// +public class AccountSmokeTests : IClassFixture +{ + private readonly TestWebApplicationFactory _factory; + + public AccountSmokeTests(TestWebApplicationFactory factory) + { + _factory = factory; + } + + + + [Fact] + public async Task ResourceStore_get_all_resources_does_not_throw() + { + using var scope = _factory.Services.CreateScope(); + var resourceStore = scope.ServiceProvider.GetRequiredService(); + + var exception = await Record.ExceptionAsync(resourceStore.GetAllResourcesAsync); + + Assert.Null(exception); + } + + // Regression: anonymous access to a restricted page used to redirect to + // the ASP.NET Identity default LoginPath "/Account/Login", which has no + // controller here. The application cookie must redirect to the real + // sign-in endpoint at /signin instead. + [Fact] + public async Task Anonymous_access_to_protected_page_redirects_to_signin() + { + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + }); + + // EstimateController is [Authorize]; GET /Estimate hits Index(). + var response = await client.GetAsync("/Estimate", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + var location = response.Headers.Location; + Assert.NotNull(location); + Assert.Equal("/signin", location.AbsolutePath); + Assert.Contains("ReturnUrl=%2FEstimate", location.Query); + } + + // Regression: the POST Delete action existed, but neither the GET + // Delete view nor a rendered confirmation page was reachable — + // Views/Account/Delete.cshtml was missing. An authenticated GET + // must now return the confirmation page (200). + [Fact] + public async Task Authenticated_get_account_delete_returns_confirmation_page() + { + var client = _factory.CreateClient(new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + }); + client.DefaultRequestHeaders.Add( + TestAuthPolicyProvider.HeaderName, TestAuthPolicyProvider.AdminRole); + + var response = await client.GetAsync("/Account/Delete", TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync( + TestContext.Current.CancellationToken); + Assert.Contains("Supprimer mon compte", body); + } +} diff --git a/src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs b/src/test/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs rename to src/test/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs diff --git a/src/Yavsc.Org.Tests/StaticAssetsPathsTests.cs b/src/test/Yavsc.Org.Tests/StaticAssetsPathsTests.cs similarity index 100% rename from src/Yavsc.Org.Tests/StaticAssetsPathsTests.cs rename to src/test/Yavsc.Org.Tests/StaticAssetsPathsTests.cs diff --git a/src/Yavsc.Org.Tests/TestUserMiddleware.cs b/src/test/Yavsc.Org.Tests/TestUserMiddleware.cs similarity index 100% rename from src/Yavsc.Org.Tests/TestUserMiddleware.cs rename to src/test/Yavsc.Org.Tests/TestUserMiddleware.cs diff --git a/src/Yavsc.Org.Tests/TestUserStartupFilter.cs b/src/test/Yavsc.Org.Tests/TestUserStartupFilter.cs similarity index 100% rename from src/Yavsc.Org.Tests/TestUserStartupFilter.cs rename to src/test/Yavsc.Org.Tests/TestUserStartupFilter.cs diff --git a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs b/src/test/Yavsc.Org.Tests/TestWebApplicationFactory.cs similarity index 100% rename from src/Yavsc.Org.Tests/TestWebApplicationFactory.cs rename to src/test/Yavsc.Org.Tests/TestWebApplicationFactory.cs diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/test/Yavsc.Org.Tests/WebServerFixture.cs similarity index 87% rename from src/Yavsc.Org.Tests/WebServerFixture.cs rename to src/test/Yavsc.Org.Tests/WebServerFixture.cs index 5ca1daa06..522103023 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/test/Yavsc.Org.Tests/WebServerFixture.cs @@ -1,4 +1,5 @@ using IdentityServer8.EntityFramework.Entities; +using IdentityServer8.EntityFramework.Mappers; using IdentityServer8.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -238,6 +239,26 @@ public sealed class WebServerFixture : WebHostFixture AddAuthorizedClient(scope, TestClientId, TestClientSecret); } + // EnsureDefaultApplicationScopes seeds the standard identity + // resources (openid, profile) once at host startup, but a + // ResetAndMigrateDatabase call does EnsureDeleted on the shared + // in-memory store, which wipes them — and ReseedAuthTestData + // previously only re-added the client. Without openid/profile + // present, any token request asking for those scopes returns + // invalid_scope, so a password-grant token carrying the "name" + // claim (scope "openid profile") could not be minted after a + // reset. Re-seed them here to mirror the startup seed. + var identityResources = configDb.Set(); + if (!identityResources.Any(r => r.Name == "openid")) + { + identityResources.Add(new IdentityResources.OpenId().ToEntity()); + } + if (!identityResources.Any(r => r.Name == "profile")) + { + identityResources.Add(new IdentityResources.Profile().ToEntity()); + } + configDb.SaveChanges(); + _sharedTestClientId = TestClientId; _sharedTestClientSecret = TestClientSecret; _sharedTestingUserName = TestingUserName; @@ -284,7 +305,12 @@ public sealed class WebServerFixture : WebHostFixture { ClientId = testClientId, AccessTokenLifetime = 3600000, - AccessTokenType = 1, + // AccessTokenType = 0 → Jwt in IdentityServer8 (1 = Reference, + // the inverse of Duende's enum). The real PostIt client issues + // JWT access tokens so the Blogs/Api bearer hosts can validate + // them with JwtBearer; mirror that here so the token can be + // decoded and its "name" claim asserted. + AccessTokenType = 0, ClientName = "Testing client", Enabled = true, RequireClientSecret = true @@ -327,6 +353,23 @@ public sealed class WebServerFixture : WebHostFixture ClientId = testingClient.Id, Scope = "test" }); + // Allow the identity scopes so a resource-owner-password token + // can request "openid profile" and trigger ProfileService to + // emit the "name" claim (user.UserName). Without profile in the + // client's AllowedScopes, IdentityServer strips the scope from + // the request and the access_token carries no "name" — the + // regression that left GetUserName() / the MyFiles upload root + // null. Additive only: existing tests request "test" alone. + configDb.Set().Add(new ClientScope + { + ClientId = testingClient.Id, + Scope = "openid" + }); + configDb.Set().Add(new ClientScope + { + ClientId = testingClient.Id, + Scope = "profile" + }); configDb.SaveChanges(); } diff --git a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj b/src/test/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj similarity index 89% rename from src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj rename to src/test/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj index bdacafc5c..7a364f2d6 100644 --- a/src/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj +++ b/src/test/Yavsc.Org.Tests/Yavsc.Org.Tests.csproj @@ -11,8 +11,8 @@ $(MSBuildProjectDirectory)\test.runsettings 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 @@ -30,7 +30,6 @@ - @@ -43,9 +42,9 @@ - - - + + + diff --git a/src/Yavsc.Org.Tests/appsettings.json b/src/test/Yavsc.Org.Tests/appsettings.json similarity index 100% rename from src/Yavsc.Org.Tests/appsettings.json rename to src/test/Yavsc.Org.Tests/appsettings.json diff --git a/src/Yavsc.Org.Tests/test.runsettings b/src/test/Yavsc.Org.Tests/test.runsettings similarity index 100% rename from src/Yavsc.Org.Tests/test.runsettings rename to src/test/Yavsc.Org.Tests/test.runsettings diff --git a/src/Yavsc.Org.Tests/xunit.runner.json b/src/test/Yavsc.Org.Tests/xunit.runner.json similarity index 100% rename from src/Yavsc.Org.Tests/xunit.runner.json rename to src/test/Yavsc.Org.Tests/xunit.runner.json diff --git a/src/test/Yavsc.Server.Tests/BlogSpotServiceDetailsTests.cs b/src/test/Yavsc.Server.Tests/BlogSpotServiceDetailsTests.cs new file mode 100644 index 000000000..713957804 --- /dev/null +++ b/src/test/Yavsc.Server.Tests/BlogSpotServiceDetailsTests.cs @@ -0,0 +1,222 @@ +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using Yavsc.Models.Blog; +using Yavsc.Server.Exceptions; +using Yavsc.Server.Tests.Fixtures; + +namespace Yavsc.Server.Tests; + +/// +/// Service-level tests for . +/// +/// loads a +/// (with Author, Tags, Comments, ACL and +/// Publication navigations), runs a resource-based +/// ReadPermission authorization against it, scrubs the ACL for +/// non-owners, derives from the +/// loaded Publication navigation, and eagerly loads each +/// comment's Author. These tests pin each of those behaviours +/// at the service layer against the real +/// (the public/owner/ +/// sponsor/Administrator disjunction) and a real SQLite store — no +/// mocking. +/// +/// Seeding and the service call resolve through separate DI +/// scopes so the DbContext that writes the seed rows is disposed +/// before the one reads through, +/// mirroring the per-request DbContext lifetime in production. +/// +[Collection("Yavsc Server")] +public sealed class BlogSpotServiceDetailsTests : IClassFixture +{ + private readonly ServerServicesFixture _fixture; + + public BlogSpotServiceDetailsTests(ServerServicesFixture fixture) + { + _fixture = fixture; + } + + /// An authenticated principal whose sub claim is + /// . + /// reads sub first, which is what the JwtBearer middleware + /// in production preserves (MapInboundClaims = false), and what + /// compares against + /// . + private static ClaimsPrincipal Authenticated(string userId, + params Claim[] extra) + { + var claims = new List { new("sub", userId) }; + claims.AddRange(extra); + return new ClaimsPrincipal( + new ClaimsIdentity(claims, authenticationType: "Bearer")); + } + + /// An unauthenticated principal: a + /// with no authentication type has + /// == false. The + /// service still runs authorization — a published post's + /// ReadPermission succeeds purely on IsPublic, with no + /// identity required. + private static ClaimsPrincipal Anonymous() + => new(new ClaimsIdentity()); + + /// The Microsoft-format role claim + /// + /// reads when checking the "Administrator" bypass in + /// . + private static Claim AdminRole() + => new("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", + "Administrator"); + + [Fact] + public async Task Details_returns_the_post_for_its_owner_and_keeps_the_acl() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + var circleId = _fixture.SeedCircle("alice", "alice-private"); + var postId = _fixture.SeedBlogPost("alice", "Owner reads own draft"); + _fixture.SeedAcl(postId, circleId); + + var blog = await _fixture.WithServiceAsync(svc => + svc.Details(Authenticated("alice"), postId)); + + Assert.Equal(postId, blog.Id); + Assert.Equal("Owner reads own draft", blog.Title); + // The owner is the one viewer ScrubAclForViewer keeps the ACL + // for — a non-owner would see an empty list. + Assert.Single(blog.ACL!); + // No BlogSpotPublication row was seeded, so IsPublished is + // derived from the (null) Publication navigation as false. + Assert.False(blog.IsPublished); + } + + [Fact] + public async Task Details_serves_IsPublished_true_when_a_publication_row_exists() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + var postId = _fixture.SeedBlogPost("alice", "Published post"); + _fixture.Publish(postId); + + var blog = await _fixture.WithServiceAsync(svc => + svc.Details(Authenticated("alice"), postId)); + + // IsPublished is a wire-only value the service derives from + // the loaded Publication navigation (Details Includes it). + Assert.True(blog.IsPublished); + } + + [Fact] + public async Task Details_lets_an_anonymous_viewer_read_a_published_post_and_scrubs_its_acl() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + var circleId = _fixture.SeedCircle("alice", "alice-private"); + var postId = _fixture.SeedBlogPost("alice", "Public post"); + // The ACL is real, but a non-owner viewer must not see it. + _fixture.SeedAcl(postId, circleId); + _fixture.Publish(postId); + + var blog = await _fixture.WithServiceAsync(svc => + svc.Details(Anonymous(), postId)); + + // ReadPermission succeeds purely on IsPublic (the publication + // row) — no identity needed. + Assert.Equal(postId, blog.Id); + Assert.True(blog.IsPublished); + // ScrubAclForViewer: the anonymous viewer is not the owner, so + // the ACL is replaced with an empty list rather than leaked. + Assert.Empty(blog.ACL!); + } + + [Fact] + public async Task Details_denies_a_draft_to_a_non_owner_who_is_not_a_sponsor() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + _fixture.SeedUser("bob"); + var postId = _fixture.SeedBlogPost("alice", "Alice's draft"); + + // No publication row (not public), bob is not the owner, bob + // is not a member of any circle alice owns, bob is not an + // Administrator — every branch of ReadPermission fails. + await Assert.ThrowsAsync(() => + _fixture.WithServiceAsync(svc => + svc.Details(Authenticated("bob"), postId))); + } + + [Fact] + public async Task Details_lets_a_sponsor_read_a_draft_and_scrubs_its_acl() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + _fixture.SeedUser("bob"); + // Sponsorship = bob is a member of a circle alice owns. + var circleId = _fixture.SeedCircle("alice", "alice-friends"); + _fixture.SeedCircleMember(circleId, "bob"); + var postId = _fixture.SeedBlogPost("alice", "Draft for friends"); + _fixture.SeedAcl(postId, circleId); + + var blog = await _fixture.WithServiceAsync(svc => + svc.Details(Authenticated("bob"), postId)); + + // ReadPermission succeeds on the IsSponsor branch. + Assert.Equal(postId, blog.Id); + Assert.False(blog.IsPublished); + // bob is not the owner, so the ACL is scrubbed even though he + // was granted read access via the circle. + Assert.Empty(blog.ACL!); + } + + [Fact] + public async Task Details_lets_an_administrator_read_a_draft() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + _fixture.SeedUser("root"); + var postId = _fixture.SeedBlogPost("alice", "Draft, admin view"); + + var blog = await _fixture.WithServiceAsync(svc => + svc.Details(Authenticated("root", AdminRole()), postId)); + + // The Administrator bypass in PermissionHandler short-circuits + // the public/owner/sponsor checks. + Assert.Equal(postId, blog.Id); + Assert.False(blog.IsPublished); + } + + [Fact] + public async Task Details_loads_each_comment_author() + { + _fixture.ResetDatabase(); + _fixture.SeedUser("alice"); + _fixture.SeedUser("bob"); + var postId = _fixture.SeedBlogPost("alice", "Post with comments"); + _fixture.SeedComment(postId, "bob", "Nice post!"); + + var blog = await _fixture.WithServiceAsync(svc => + svc.Details(Authenticated("alice"), postId)); + + var comment = Assert.Single(blog.Comments); + // Details explicitly loads c.Author from _context.Users for + // every comment; the navigation must not be left null. + Assert.NotNull(comment.Author); + Assert.Equal("bob", comment.Author.UserName); + } + + [Fact] + public async Task Details_throws_for_a_missing_post() + { + _fixture.ResetDatabase(); + + // The query uses SingleAsync, which throws + // InvalidOperationException when no row matches — the + // `if (blog == null) return null` guard below it is unreachable. + // Pin that current behaviour so a future switch to + // SingleOrDefaultAsync (which would make the guard live) is a + // deliberate, reviewed change rather than a silent one. + await Assert.ThrowsAsync(() => + _fixture.WithServiceAsync(svc => + svc.Details(Authenticated("alice"), 424242))); + } +} \ No newline at end of file diff --git a/src/test/Yavsc.Server.Tests/Fixtures/ServerServicesFixture.cs b/src/test/Yavsc.Server.Tests/Fixtures/ServerServicesFixture.cs new file mode 100644 index 000000000..f57f65839 --- /dev/null +++ b/src/test/Yavsc.Server.Tests/Fixtures/ServerServicesFixture.cs @@ -0,0 +1,452 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Npgsql; +using Yavsc.Models; +using Yavsc.Models.Access; +using Yavsc.Models.Blog; +using Yavsc.Models.Relationship; +using Yavsc.Services; + +namespace Yavsc.Server.Tests.Fixtures; + +/// +/// Service-level test host for and the +/// other Yavsc.Server services that take an +/// + . +/// +/// Unlike BlogsWebServerFixture (in Yavsc.Blogs.Tests), +/// this fixture does not spin up Kestrel, JWT bearer, or MVC +/// controllers. takes the +/// as a parameter, +/// so the caller controls identity directly — there is no HTTP/auth +/// middleware in the path under test. Building only the DI graph the +/// service actually resolves keeps these tests fast and isolates the +/// service layer from the blog API's transport concerns, which +/// Yavsc.Blogs.Tests already covers end-to-end. +/// +/// Database provider. Mirrors +/// ApiWebServerFixture: SQLite :memory: by default, or +/// real PostgreSQL when YAVSC_SERVER_TEST_DB_PROVIDER=npgsql is +/// set. Production runs on Npgsql, so the Npgsql path — which applies +/// the real Yavsc.Org migrations against a dedicated test +/// database — is the one that catches provider-specific bugs (FK +/// enforcement, type mappings, migration drift). SQLite stays the +/// default so the suite runs out-of-the-box with no external server. +/// +/// +/// SQLite: a named :memory: database +/// (Cache=Shared) backed by a single connection held open for the +/// fixture's lifetime, schema via EnsureCreated. Reset is +/// EnsureDeleted + EnsureCreated. +/// Npgsql: a dedicated database +/// yavscServerTestDb (created from the admin connection if it +/// does not exist), schema via Migrate using the +/// Yavsc.Org migrations assembly. Reset purges the blog-graph +/// tables in FK order — children before parents — so no DELETE +/// violates a constraint, instead of dropping the database (which +/// would force a full re-migration per test). +/// The real and the +/// real : Details calls +/// IAuthorizationService.AuthorizeAsync(user, blog, new +/// ReadPermission()), and answers +/// that requirement with the public/owner/sponsor/Administrator +/// disjunction that the tests below pin. No mocking. +/// A trivial +/// stub: Details never touches the file system, but the DI +/// container needs an instance to construct the service. +/// +/// +/// Marked so a single +/// fixture instance (one store, one DI container) is shared across +/// every [Collection("Yavsc Server")] test class. Each test +/// resets the store via . +/// +[CollectionDefinition("Yavsc Server")] +public sealed class ServerServicesFixture : IDisposable +{ + private const string DbProviderEnvVar = "YAVSC_SERVER_TEST_DB_PROVIDER"; + private const string NpgsqlAdminConnectionEnvVar = "YAVSC_SERVER_TEST_NPGSQL_ADMIN_CONNECTION"; + private const string DedicatedNpgsqlDatabaseName = "yavscServerTestDb"; + + private static readonly object _npgsqlLock = new(); + private static string? _sharedNpgsqlConnectionString; + + private readonly SqliteConnection? _sqliteConnection; + private readonly ServiceProvider _services; + + public IServiceProvider Services => _services; + + public ServerServicesFixture() + { + var services = new ServiceCollection(); + + if (UseNpgsqlProvider()) + { + var npgsqlConnectionString = EnsureNpgsqlDatabaseCreated(); + services.AddDbContext(opt => + opt.UseNpgsql(npgsqlConnectionString, + x => x.MigrationsAssembly("Yavsc.Org"))); + } + else + { + // Mode=Memory + Cache=Shared: a named in-memory database + // every connection string referencing "YavscServerTests" + // resolves to the same backing store, as long as at least + // one connection stays open against it. _sqliteConnection + // is held for the fixture's lifetime (disposed in Dispose), + // so the store survives across scoped DbContexts per test. + _sqliteConnection = new SqliteConnection( + "Data Source=YavscServerTests;Mode=Memory;Cache=Shared"); + _sqliteConnection.Open(); + + services.AddDbContext(opt => + // UseSqlite(DbConnection) keeps _sqliteConnection alive + // for the DbContext's lifetime instead of letting EF + // open and close its own — without this each DbContext + // would get a fresh connection pointing at an empty + // store. + opt.UseSqlite(_sqliteConnection)); + } + + // BlogSpotService.AttachFiles writes under siteSettings.Blog. + // Details itself doesn't read it, but the service constructor + // throws if SiteSettings.Blog is null, so configure a temp + // value to keep construction alive. + services.Configure(s => s.Blog = "server-tests-files-root"); + + // Trivial file-system auth: Details never calls into it, but + // the DI container needs an instance to construct the service. + services.AddSingleton( + new NoopFileSystemAuthManager()); + + // The real BlogSpotService — same shape the production host + // builds (ApplicationDbContext, IAuthorizationService, + // IFileSystemAuthManager, IOptions, + // ILoggerFactory). + services.AddScoped(); + + // The real PermissionHandler: Details authorizes with + // ReadPermission, and PermissionHandler resolves it via the + // IsPublic / IsOwner / IsSponsor / Administrator disjunction. + services.AddScoped(); + + // Default authorization services register + // IAuthorizationService (DefaultAuthorizationService), which + // collects the IAuthorizationHandler above and runs it against + // the requirement passed to AuthorizeAsync. + services.AddAuthorization(); + services.AddLogging(); + + _services = services.BuildServiceProvider(); + + // Create the schema once. Npgsql uses Migrate (the Yavsc.Org + // migrations) so the test schema matches production; SQLite uses + // EnsureCreated (idempotent, model-derived). ResetDatabase + // re-creates / purges per test. + using (var scope = _services.CreateScope()) + { + var db = scope.ServiceProvider + .GetRequiredService(); + if (UseNpgsqlProvider()) + db.Database.Migrate(); + else + db.Database.EnsureCreated(); + } + } + + /// True when YAVSC_SERVER_TEST_DB_PROVIDER=npgsql + /// is set — the fixture then runs against a real PostgreSQL server + /// instead of the SQLite in-memory default. + public static bool UseNpgsqlProvider() + => string.Equals( + Environment.GetEnvironmentVariable(DbProviderEnvVar), + "npgsql", + StringComparison.OrdinalIgnoreCase); + + /// Reset the store to a known empty state for the next + /// test. SQLite: EnsureDeleted + EnsureCreated (the + /// named in-memory store survives because _sqliteConnection is held + /// open). Npgsql: purge the blog-graph tables in FK order — + /// children before parents — so no DELETE trips a foreign key, + /// avoiding a full re-migration per test. + public void ResetDatabase() + { + using var scope = _services.CreateScope(); + var db = scope.ServiceProvider + .GetRequiredService(); + + if (UseNpgsqlProvider()) + { + // Children before parents. BlogAttachedFile, Comment and + // BlogSpotPublication reference BlogSpot; CircleAuthorization + // ToBlogPost references BlogSpot + Circle; BlogTag references + // BlogSpot + Tag; BlogSpot references ApplicationUser; + // CircleMember references Circle + ApplicationUser. + db.BlogAttachedFiles.RemoveRange(db.BlogAttachedFiles); + db.Comment.RemoveRange(db.Comment); + db.blogSpotPublications.RemoveRange(db.blogSpotPublications); + db.CircleAuthorizationToBlogPost + .RemoveRange(db.CircleAuthorizationToBlogPost); + db.BlogTag.RemoveRange(db.BlogTag); + db.UploadedFiles.RemoveRange(db.UploadedFiles); + db.BlogSpot.RemoveRange(db.BlogSpot); + db.CircleMembers.RemoveRange(db.CircleMembers); + db.Circle.RemoveRange(db.Circle); + db.Users.RemoveRange(db.Users); + db.SaveChanges(); + return; + } + + db.Database.EnsureDeleted(); + db.Database.EnsureCreated(); + } + + /// Resolve a fresh service scope and the + /// from it, run + /// , and dispose the scope (and its + /// DbContext). Use this for seeding writes that should not share + /// a DbContext with the service call under test. + public T WithDb(Func body) + { + using var scope = _services.CreateScope(); + var db = scope.ServiceProvider + .GetRequiredService(); + return body(db); + } + + public void SeedUser(string userName, + Action? configure = null) + { + WithDb(db => + { + if (db.Users.Any(u => u.Id == userName)) return false; + var user = new ApplicationUser + { + Id = userName, + UserName = userName, + Email = $"{userName}@example.test", + }; + configure?.Invoke(user); + db.Users.Add(user); + db.SaveChanges(); + return true; + }); + } + + public long SeedBlogPost(string authorId, string title, + Action? configure = null) + { + return WithDb(db => + { + var post = new BlogPost + { + AuthorId = authorId, + Title = title, + Article = "Test article body.", + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + }; + configure?.Invoke(post); + db.BlogSpot.Add(post); + db.SaveChanges(); + return post.Id; + }); + } + + /// Insert a row for + /// — the existence of that row is what + /// makes a post "public" (readable by anyone) in + /// . This is the same write + /// SetPublishAsync performs in production. + public void Publish(long postId) + { + WithDb(db => + { + db.blogSpotPublications.Add( + new BlogSpotPublication { PostId = postId }); + db.SaveChanges(); + return true; + }); + } + + /// Seed a circle owned by and + /// return its server-assigned id. Sponsorship in + /// is "the viewer is a member of a + /// circle the post's author owns", so a circle + a + /// entry is what grants a non-owner + /// read access to a draft. + public long SeedCircle(string ownerId, string name) + { + return WithDb(db => + { + var circle = new Circle + { + OwnerId = ownerId, + Name = name, + Public = false, + }; + db.Circle.Add(circle); + db.SaveChanges(); + return circle.Id; + }); + } + + public void SeedCircleMember(long circleId, string memberId) + { + WithDb(db => + { + db.CircleMembers.Add( + new CircleMember { CircleId = circleId, MemberId = memberId }); + db.SaveChanges(); + return true; + }); + } + + /// Attach a ACL + /// entry to for . + /// The owner always sees the ACL; non-owners have it scrubbed by + /// 's ScrubAclForViewer. + public void SeedAcl(long postId, long circleId) + { + WithDb(db => + { + db.CircleAuthorizationToBlogPost.Add( + new CircleAuthorizationToBlogPost + { + BlogPostId = postId, + CircleId = circleId, + }); + db.SaveChanges(); + return true; + }); + } + + /// Seed a comment on authored by + /// . + /// loads each comment's Author navigation from + /// _context.Users; seeding the user first keeps the FK + /// valid (SQLite and PostgreSQL both enforce it). + public void SeedComment(long postId, string authorId, string article) + { + WithDb(db => + { + db.Comment.Add(new Comment + { + ReceiverId = postId, + AuthorId = authorId, + Article = article, + Visible = true, + UserModified = authorId, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + }); + db.SaveChanges(); + return true; + }); + } + + /// Resolve the real from the + /// fixture's DI container, run against it, + /// and dispose the request scope (and its scoped DbContext). The + /// service is scoped, so it must be resolved from a scope — and the + /// scope is what we dispose to avoid leaking a DbContext against + /// the shared connection across tests. + public async Task WithServiceAsync( + Func> body) + { + using var scope = _services.CreateScope(); + var service = scope.ServiceProvider + .GetRequiredService(); + return await body(service); + } + + private static string EnsureNpgsqlDatabaseCreated() + { + lock (_npgsqlLock) + { + if (!string.IsNullOrWhiteSpace(_sharedNpgsqlConnectionString)) + return _sharedNpgsqlConnectionString; + + var adminConnectionString = BuildAdminConnectionString(); + var databaseName = DedicatedNpgsqlDatabaseName; + + using (var adminConnection = new NpgsqlConnection(adminConnectionString)) + { + adminConnection.Open(); + using var existsCommand = adminConnection.CreateCommand(); + existsCommand.CommandText = + "SELECT 1 FROM pg_database WHERE datname = @databaseName"; + existsCommand.Parameters.AddWithValue("databaseName", databaseName); + + if (existsCommand.ExecuteScalar() is null) + { + using var createCommand = adminConnection.CreateCommand(); + createCommand.CommandText = + $"CREATE DATABASE \"{databaseName}\""; + createCommand.ExecuteNonQuery(); + } + } + + var testConnectionBuilder = new NpgsqlConnectionStringBuilder(adminConnectionString) + { + Database = databaseName, + Pooling = false, + IncludeErrorDetail = true + }; + + _sharedNpgsqlConnectionString = testConnectionBuilder.ToString(); + return _sharedNpgsqlConnectionString; + } + } + + private static string BuildAdminConnectionString() + { + // The admin connection string (host + credentials) is supplied + // via environment, never embedded in source — committing a + // real dev password would leak it into git history. The Npgsql + // path is opt-in, so requiring the env var is fine: a missing + // value produces a clear failure rather than a silent fallback + // onto someone's local dev database. + var configured = Environment.GetEnvironmentVariable(NpgsqlAdminConnectionEnvVar); + if (string.IsNullOrWhiteSpace(configured)) + throw new InvalidOperationException( + $"{NpgsqlAdminConnectionEnvVar} is not set. The Npgsql test " + + "provider requires an admin PostgreSQL connection string " + + "in that environment variable (e.g. " + + "'Server=localhost;Port=5432;Database=postgres;" + + "Username=yavscdev;Password=...;Include Error Detail=true')."); + + var builder = new NpgsqlConnectionStringBuilder(configured) + { + Pooling = false, + IncludeErrorDetail = true + }; + + // The admin connection targets the maintenance database + // (postgres) so we can CREATE/DROP the dedicated test database; + // whatever database the env var names is overridden here. + builder.Database = "postgres"; + + return builder.ToString(); + } + + public void Dispose() + { + _services.Dispose(); + _sqliteConnection?.Dispose(); + } + + private sealed class NoopFileSystemAuthManager : IFileSystemAuthManager + { + public FileAccessRight GetFilePathAccess( + System.Security.Claims.ClaimsPrincipal user, + string fileRelativePath) + => FileAccessRight.None; + + public void SetAccess(long circleId, string normalizedFullPath, + FileAccessRight access) { } + } +} \ No newline at end of file diff --git a/src/test/Yavsc.Server.Tests/Yavsc.Server.Tests.csproj b/src/test/Yavsc.Server.Tests/Yavsc.Server.Tests.csproj new file mode 100644 index 000000000..bb62ead21 --- /dev/null +++ b/src/test/Yavsc.Server.Tests/Yavsc.Server.Tests.csproj @@ -0,0 +1,32 @@ + + + net10.0 + enable + enable + false + Yavsc.Server.Tests + true + 1.1.0.0 + 1.1.0.0 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Yavsc.Tests.Shared/BlogHelpers.cs b/src/test/Yavsc.Tests.Shared/BlogHelpers.cs similarity index 100% rename from src/Yavsc.Tests.Shared/BlogHelpers.cs rename to src/test/Yavsc.Tests.Shared/BlogHelpers.cs diff --git a/src/Yavsc.Tests.Shared/IBackendFixture.cs b/src/test/Yavsc.Tests.Shared/IBackendFixture.cs similarity index 100% rename from src/Yavsc.Tests.Shared/IBackendFixture.cs rename to src/test/Yavsc.Tests.Shared/IBackendFixture.cs diff --git a/src/Yavsc.Tests.Shared/InMemoryDatabaseName.cs b/src/test/Yavsc.Tests.Shared/InMemoryDatabaseName.cs similarity index 100% rename from src/Yavsc.Tests.Shared/InMemoryDatabaseName.cs rename to src/test/Yavsc.Tests.Shared/InMemoryDatabaseName.cs diff --git a/src/Yavsc.Tests.Shared/README.md b/src/test/Yavsc.Tests.Shared/README.md similarity index 100% rename from src/Yavsc.Tests.Shared/README.md rename to src/test/Yavsc.Tests.Shared/README.md diff --git a/src/Yavsc.Tests.Shared/TestAuthPolicyProvider.cs b/src/test/Yavsc.Tests.Shared/TestAuthPolicyProvider.cs similarity index 100% rename from src/Yavsc.Tests.Shared/TestAuthPolicyProvider.cs rename to src/test/Yavsc.Tests.Shared/TestAuthPolicyProvider.cs diff --git a/src/Yavsc.Tests.Shared/TestTokenIssuer.cs b/src/test/Yavsc.Tests.Shared/TestTokenIssuer.cs similarity index 80% rename from src/Yavsc.Tests.Shared/TestTokenIssuer.cs rename to src/test/Yavsc.Tests.Shared/TestTokenIssuer.cs index 18aa2e00c..b1e0fc005 100644 --- a/src/Yavsc.Tests.Shared/TestTokenIssuer.cs +++ b/src/test/Yavsc.Tests.Shared/TestTokenIssuer.cs @@ -63,17 +63,32 @@ public static class TestTokenIssuer /// Value of the scope claim. The /// production BlogScope policy requires /// RequireClaim("scope", "blogs"). + /// Value of the name claim. Defaults to + /// (the test convention where the login + /// equals the subject). Pass a distinct login to mirror production, + /// where sub is a GUID and name is the user's + /// UserName — the file-system list/upload paths key the + /// personal-storage tree off the login, not the subject. /// Optional additional claims /// (e.g. a role for an admin-bypass test). public static string Issue( string subject, string scope = "blogs", + string? name = null, IEnumerable? extraClaims = null) { var now = DateTime.UtcNow; var claims = new List { new("sub", subject), + // Mirror production: IdentityServer's ProfileService emits the + // "name" claim as the user's login (UserName) when the profile + // scope is granted, and the file-system helpers resolve the + // per-user storage root via GetUserName() (FindFirstValue("name")) + // — not via User.Identity.Name. A test token without "name" would + // make ReceiveUserFile / EnsureDestinationDirectory compute a + // null owner root and 500 on upload, so stamp it here. + new("name", name ?? subject), new("scope", scope), }; if (extraClaims is not null) claims.AddRange(extraClaims); diff --git a/src/Yavsc.Tests.Shared/WebHostFixture.cs b/src/test/Yavsc.Tests.Shared/WebHostFixture.cs similarity index 100% rename from src/Yavsc.Tests.Shared/WebHostFixture.cs rename to src/test/Yavsc.Tests.Shared/WebHostFixture.cs diff --git a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj b/src/test/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj similarity index 78% rename from src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj rename to src/test/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj index dd93a8b34..b5819d758 100644 --- a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj +++ b/src/test/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj @@ -14,8 +14,8 @@ --> 1.1.0.0 1.1.0.0 - 1.1.0-feat-sign-estimate.1+306.Branch.feat-sign-estimate.Sha.e9a33695b38154859729b61d1280e596f1597acf - 1.1.0-feat-sign-estimate.1 + 1.1.0-estimate.1+364.Branch.feature-estimate.Sha.3d6ab6e72f9ba4ccfa250a1cce154b550281405e + 1.1.0-estimate.1 @@ -24,6 +24,6 @@ - + \ No newline at end of file diff --git a/yavsc.sln b/yavsc.sln index 6085a85fd..e6caef782 100644 --- a/yavsc.sln +++ b/yavsc.sln @@ -15,10 +15,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Api", "src\Yavsc.Api\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cli", "src\cli\cli.csproj", "{27DA6CEC-4266-4F78-BB32-8388F9A401D0}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{76A7FBA6-B7EC-4864-85C0-1F470794BA0F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Org.Tests", "src\Yavsc.Org.Tests\Yavsc.Org.Tests.csproj", "{CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Blogs", "src\Yavsc.Blogs\Yavsc.Blogs.csproj", "{5D9E161F-3CB3-4854-9D07-3DF6882EA66C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostIt", "PostIt", "{E13D107F-4053-D0DE-6394-453609595BFE}" @@ -31,15 +27,21 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostIt.Browser", "src\PostI EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostIt.Desktop", "src\PostIt\PostIt.Desktop\PostIt.Desktop.csproj", "{EFE24256-9335-44C5-8B77-E180C2DB3C0B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Blogs.Tests", "src\Yavsc.Blogs.Tests\Yavsc.Blogs.Tests.csproj", "{0E471075-DABF-40E9-98B7-1630BEF19145}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Tests.Shared", "src\Yavsc.Tests.Shared\Yavsc.Tests.Shared.csproj", "{34D1F73D-BF74-47CC-9358-9F4F221C75D7}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Api.Client", "src\Yavsc.Api.Client\Yavsc.Api.Client.csproj", "{59AF5DEA-D349-495A-BC44-FC7BD4E55099}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostIt.Tests", "src\PostIt\PostIt.Tests\PostIt.Tests.csproj", "{021E6F5A-B81D-42A7-9918-DFE39B424FC2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Api.Test", "src\Yavsc.Api.Test\Yavsc.Api.Test.csproj", "{49A42F0E-6E18-4617-9716-3E728966D96F}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6EA238F3-C35C-B265-FF02-A4695B9C69A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Api.Tests", "src\test\Yavsc.Api.Tests\Yavsc.Api.Tests.csproj", "{70141DC1-39E5-4350-B995-9625347D29AA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Tests.Shared", "src\test\Yavsc.Tests.Shared\Yavsc.Tests.Shared.csproj", "{1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Server.Tests", "src\test\Yavsc.Server.Tests\Yavsc.Server.Tests.csproj", "{8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Org.Tests", "src\test\Yavsc.Org.Tests\Yavsc.Org.Tests.csproj", "{7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Blogs.Tests", "src\test\Yavsc.Blogs.Tests\Yavsc.Blogs.Tests.csproj", "{22ECDDEE-EFD7-4501-9905-BF4E27C605C3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -111,18 +113,6 @@ Global {27DA6CEC-4266-4F78-BB32-8388F9A401D0}.Release|x64.Build.0 = Release|Any CPU {27DA6CEC-4266-4F78-BB32-8388F9A401D0}.Release|x86.ActiveCfg = Release|Any CPU {27DA6CEC-4266-4F78-BB32-8388F9A401D0}.Release|x86.Build.0 = Release|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Debug|x64.ActiveCfg = Debug|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Debug|x64.Build.0 = Debug|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Debug|x86.ActiveCfg = Debug|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Debug|x86.Build.0 = Debug|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Release|Any CPU.Build.0 = Release|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Release|x64.ActiveCfg = Release|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Release|x64.Build.0 = Release|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Release|x86.ActiveCfg = Release|Any CPU - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1}.Release|x86.Build.0 = Release|Any CPU {5D9E161F-3CB3-4854-9D07-3DF6882EA66C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5D9E161F-3CB3-4854-9D07-3DF6882EA66C}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D9E161F-3CB3-4854-9D07-3DF6882EA66C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -183,30 +173,6 @@ Global {EFE24256-9335-44C5-8B77-E180C2DB3C0B}.Release|x64.Build.0 = Release|Any CPU {EFE24256-9335-44C5-8B77-E180C2DB3C0B}.Release|x86.ActiveCfg = Release|Any CPU {EFE24256-9335-44C5-8B77-E180C2DB3C0B}.Release|x86.Build.0 = Release|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|x64.ActiveCfg = Debug|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|x64.Build.0 = Debug|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|x86.ActiveCfg = Debug|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|x86.Build.0 = Debug|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|Any CPU.Build.0 = Release|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|x64.ActiveCfg = Release|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|x64.Build.0 = Release|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|x86.ActiveCfg = Release|Any CPU - {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|x86.Build.0 = Release|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|x64.ActiveCfg = Debug|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|x64.Build.0 = Debug|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|x86.ActiveCfg = Debug|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|x86.Build.0 = Debug|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|Any CPU.Build.0 = Release|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|x64.ActiveCfg = Release|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|x64.Build.0 = Release|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|x86.ActiveCfg = Release|Any CPU - {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|x86.Build.0 = Release|Any CPU {59AF5DEA-D349-495A-BC44-FC7BD4E55099}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {59AF5DEA-D349-495A-BC44-FC7BD4E55099}.Debug|Any CPU.Build.0 = Debug|Any CPU {59AF5DEA-D349-495A-BC44-FC7BD4E55099}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -231,18 +197,66 @@ Global {021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x64.Build.0 = Release|Any CPU {021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x86.ActiveCfg = Release|Any CPU {021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x86.Build.0 = Release|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|x64.ActiveCfg = Debug|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|x64.Build.0 = Debug|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|x86.ActiveCfg = Debug|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|x86.Build.0 = Debug|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Release|Any CPU.Build.0 = Release|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Release|x64.ActiveCfg = Release|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Release|x64.Build.0 = Release|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Release|x86.ActiveCfg = Release|Any CPU - {49A42F0E-6E18-4617-9716-3E728966D96F}.Release|x86.Build.0 = Release|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Debug|x64.ActiveCfg = Debug|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Debug|x64.Build.0 = Debug|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Debug|x86.Build.0 = Debug|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Release|Any CPU.Build.0 = Release|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Release|x64.ActiveCfg = Release|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Release|x64.Build.0 = Release|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Release|x86.ActiveCfg = Release|Any CPU + {70141DC1-39E5-4350-B995-9625347D29AA}.Release|x86.Build.0 = Release|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Debug|x64.Build.0 = Debug|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Debug|x86.Build.0 = Debug|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Release|Any CPU.Build.0 = Release|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Release|x64.ActiveCfg = Release|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Release|x64.Build.0 = Release|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Release|x86.ActiveCfg = Release|Any CPU + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4}.Release|x86.Build.0 = Release|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Debug|x64.ActiveCfg = Debug|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Debug|x64.Build.0 = Debug|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Debug|x86.ActiveCfg = Debug|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Debug|x86.Build.0 = Debug|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Release|Any CPU.Build.0 = Release|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Release|x64.ActiveCfg = Release|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Release|x64.Build.0 = Release|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Release|x86.ActiveCfg = Release|Any CPU + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF}.Release|x86.Build.0 = Release|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Debug|x64.ActiveCfg = Debug|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Debug|x64.Build.0 = Debug|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Debug|x86.ActiveCfg = Debug|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Debug|x86.Build.0 = Debug|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Release|Any CPU.Build.0 = Release|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Release|x64.ActiveCfg = Release|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Release|x64.Build.0 = Release|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Release|x86.ActiveCfg = Release|Any CPU + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B}.Release|x86.Build.0 = Release|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Debug|x64.ActiveCfg = Debug|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Debug|x64.Build.0 = Debug|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Debug|x86.Build.0 = Debug|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Release|Any CPU.Build.0 = Release|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Release|x64.ActiveCfg = Release|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Release|x64.Build.0 = Release|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Release|x86.ActiveCfg = Release|Any CPU + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -253,17 +267,19 @@ Global {88142EC2-FEB2-4D69-AD3A-74F3364360F0} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} {95DAD2B5-4324-4E89-A337-5703E45CFBE8} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} {27DA6CEC-4266-4F78-BB32-8388F9A401D0} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} - {CDCF676A-2037-472E-8C8B-67C5E4FDFEA1} = {76A7FBA6-B7EC-4864-85C0-1F470794BA0F} {5D9E161F-3CB3-4854-9D07-3DF6882EA66C} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} {E13D107F-4053-D0DE-6394-453609595BFE} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} {38B554FD-2AAF-4278-B87B-729FA4DC0111} = {E13D107F-4053-D0DE-6394-453609595BFE} {4C092CF8-524A-494D-AAFC-69383DFBA31D} = {E13D107F-4053-D0DE-6394-453609595BFE} {AF96C1C4-D128-4CD7-A8BB-D194E6D270F0} = {E13D107F-4053-D0DE-6394-453609595BFE} {EFE24256-9335-44C5-8B77-E180C2DB3C0B} = {E13D107F-4053-D0DE-6394-453609595BFE} - {0E471075-DABF-40E9-98B7-1630BEF19145} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} - {34D1F73D-BF74-47CC-9358-9F4F221C75D7} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} {59AF5DEA-D349-495A-BC44-FC7BD4E55099} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} {021E6F5A-B81D-42A7-9918-DFE39B424FC2} = {E13D107F-4053-D0DE-6394-453609595BFE} - {49A42F0E-6E18-4617-9716-3E728966D96F} = {76A7FBA6-B7EC-4864-85C0-1F470794BA0F} + {6EA238F3-C35C-B265-FF02-A4695B9C69A9} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} + {70141DC1-39E5-4350-B995-9625347D29AA} = {6EA238F3-C35C-B265-FF02-A4695B9C69A9} + {1A563263-B5E7-4B44-9AE9-AD357C2FA1A4} = {6EA238F3-C35C-B265-FF02-A4695B9C69A9} + {8788BE1C-A4D5-4641-A426-31F4D8D1BBFF} = {6EA238F3-C35C-B265-FF02-A4695B9C69A9} + {7CC76BA8-D4C3-46DA-96A2-F028B6B0058B} = {6EA238F3-C35C-B265-FF02-A4695B9C69A9} + {22ECDDEE-EFD7-4501-9905-BF4E27C605C3} = {6EA238F3-C35C-B265-FF02-A4695B9C69A9} EndGlobalSection EndGlobal