Compare commits
85 Commits
f173d46c2e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f51f508ce | |||
| 62824549fc | |||
| 7d7f5750e3 | |||
| 789ba35c60 | |||
| 1226c42f19 | |||
| 5e083aeaf6 | |||
| 69bb19e4eb | |||
| db791a1183 | |||
| ad77c2fe2b | |||
| 87a0241f11 | |||
| 5956f27646 | |||
| 69901a295c | |||
| 527759eb7b | |||
| d4fff6741c | |||
| a10070b9c7 | |||
| d10d1a6fdb | |||
| 97a275478d | |||
| 731eab3898 | |||
| f018e62163 | |||
| 10e02b5e9b | |||
| c365af1d42 | |||
| 7ddc346e88 | |||
| 925293d626 | |||
| 9b024967e6 | |||
| 288d470c1b | |||
| 84a72a8557 | |||
| d4223ed38f | |||
| 9f2c5db825 | |||
| 18cbc2225f | |||
| 267c4165dd | |||
| ef1c1d8ba1 | |||
| 8d4ae30224 | |||
| 02e7ed7030 | |||
| 9595bedd8e | |||
| af661632cc | |||
| 5062887010 | |||
| b41d5c5d33 | |||
| 4b377ce9f4 | |||
| 5e084ab0a8 | |||
| 559804765b | |||
| 5da1e2fd75 | |||
| ab32be98a6 | |||
| 8681247e76 | |||
| f6dbf489ad | |||
| eaa06029bb | |||
| 9e16d6004a | |||
| 0df7449a99 | |||
| 7f61e011ed | |||
| 9c372b31a6 | |||
| fd7a8024a9 | |||
| 4a8e3d02e0 | |||
| f7af144275 | |||
| cb3c8c0d18 | |||
| a997a3b825 | |||
| c58f6fe364 | |||
| 69bc76cab4 | |||
| 4b1f9e78df | |||
| 4c00f868c7 | |||
| 8b9ccdc694 | |||
| b8d1fddd91 | |||
| 9246729edf | |||
| e13b5f2cdc | |||
| 63c7624a00 | |||
| f58613d661 | |||
| d71e523074 | |||
| 1c8e02b3fa | |||
| feadab4dff | |||
| 41c342bb0f | |||
| 2e3000c3fc | |||
| 92e4da4b93 | |||
| 5978a96dd7 | |||
| b9375d66b6 | |||
| b07b0c1f0f | |||
| fd9b9c7c2e | |||
| b6f9b5fb26 | |||
| 87d81f98e9 | |||
| c5555b3003 | |||
| d8f82bb2d1 | |||
| 390241aa53 | |||
| b323f7a29f | |||
| 8ca16936a8 | |||
| f0998c818a | |||
| 0cf9f3cd0f | |||
| b382446828 | |||
| 16318c70f7 |
10
.drone.yml
10
.drone.yml
@@ -42,9 +42,11 @@ steps:
|
|||||||
- name: docker build and push
|
- name: docker build and push
|
||||||
image: docker:24.0.7
|
image: docker:24.0.7
|
||||||
commands:
|
commands:
|
||||||
- docker build . -t $docker_registry$docker_repo:$DRONE_BRANCH
|
- dockerImageWithTag="$docker_registry$docker_repo:$DRONE_BRANCH"
|
||||||
|
- docker build . -t $dockerImageWithTag
|
||||||
- echo $docker_password | docker login --username $docker_username --password-stdin $docker_registry
|
- echo $docker_password | docker login --username $docker_username --password-stdin $docker_registry
|
||||||
- docker push $docker_registry$docker_repo:$DRONE_BRANCH
|
- docker push $dockerImageWithTag
|
||||||
|
- echo "Built and pushed $dockerImageWithTag"
|
||||||
environment:
|
environment:
|
||||||
docker_username:
|
docker_username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
@@ -60,6 +62,10 @@ steps:
|
|||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- main
|
- main
|
||||||
|
- production
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
depends_on:
|
depends_on:
|
||||||
- compile (.NET)
|
- compile (.NET)
|
||||||
- test
|
- test
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
dotnet ef migrations add $args[0] --project .\src\WebApi\WebApi.csproj --output-dir Persistence/Migrations
|
dotnet ef migrations add $args[0] --project .\src\Vegasco.Server.Api\Vegasco.Server.Api.csproj --output-dir Persistence/Migrations
|
||||||
dotnet ef migrations script --idempotent --project .\src\WebApi\WebApi.csproj --output migrations/migration.sql
|
dotnet ef migrations script --idempotent --project .\src\Vegasco.Server.Api\Vegasco.Server.Api.csproj --output ./src/Vegasco.Server.Api/migrations/migration.sql
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -1,17 +1,21 @@
|
|||||||
# Vegasco Server
|
# Vegasco Server
|
||||||
|
|
||||||
Backend for the vegasco (**VE**hicle **GAS** **CO**nsumption) application.
|
Vegasco (**VE**hicle **GAS** **CO**nsumption) application.
|
||||||
|
|
||||||
|
Includes the backend (`src/Vegasco.Server.Api`) and the frontend (`src/Vegasco-Web`). Uses [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview).
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
| Configuration | Description | Default | Required |
|
| Configuration | Description | Default | Required |
|
||||||
|--------------------------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|----------|
|
|------------------------------------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|----------|
|
||||||
| JWT:MetadataUrl | The oidc meta data url | - | true |
|
| JWT:MetadataUrl | The oidc meta data url | - | true |
|
||||||
| JWT:ValidAudience | The valid audience of the JWT token. | - | true |
|
| JWT:ValidAudience | The valid audience of the JWT token. | - | true |
|
||||||
| JWT:NameClaimType | The claim type of the user's name claim. For keycloak, using `preferred_username` is often the better choice. | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name | false |
|
| JWT:NameClaimType | The claim type of the user's name claim. For keycloak, using `preferred_username` is often the better choice. | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name | false |
|
||||||
| JWT:AllowHttpMetadataUrl | Whether to allow the meta data url to have http as protocol. Always true when `ASPNETCORE_ENVIRONMENT=true` | false | false |
|
| JWT:AllowHttpMetadataUrl | Whether to allow the meta data url to have http as protocol. Always true when `ASPNETCORE_ENVIRONMENT=true` | false | false |
|
||||||
|
| ConnectionStrings:seq | The seq http endpoint to send the logs and traces to. If not set, logs and traces will not be sent to seq. | - | false |
|
||||||
|
| ConnectionStrings:vegasco-database | The connection string to the postgres database. | - | true |
|
||||||
|
|
||||||
The application uses the prefix `Vegasco_` for environment variable names. The prefix is removed when the application reads the environment variables and duplicate entries are overwritten by the environment variables.
|
The application uses the prefix `Vegasco_` for environment variable names. The prefix is removed when the application reads the environment variables and duplicate entries are overwritten by the environment variables.
|
||||||
|
|
||||||
@@ -64,4 +68,18 @@ creates a Postgres database as a docker container, and starts the Api with the c
|
|||||||
|
|
||||||
Ensure you have an identity provider set up, for example Keycloak, and configured the relevant options described above.
|
Ensure you have an identity provider set up, for example Keycloak, and configured the relevant options described above.
|
||||||
|
|
||||||
Then, to run the application, ensure you have Docker running, then run the `Vegasco.Server.AppHost` launch profile.
|
Then, to run the application, ensure you have Docker running, then run either the `http` or `https` launch profile of the `Vegasco.Server.AppHost` project.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Build server by running in project root:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker build . -t docker.nuyken.dev/vegasco/api:main
|
||||||
|
```
|
||||||
|
|
||||||
|
Builder web client by running in `src/Vegasco-Web`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker build -t docker.nuyken.dev/vegasco/web:main --build-arg CONFIGURATION=production .
|
||||||
|
```
|
||||||
|
|||||||
10
src/Vegasco-Web/.dockerignore
Normal file
10
src/Vegasco-Web/.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
5
src/Vegasco-Web/.postcssrc.json
Normal file
5
src/Vegasco-Web/.postcssrc.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"@tailwindcss/postcss": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/Vegasco-Web/.vscode/launch.json
vendored
3
src/Vegasco-Web/.vscode/launch.json
vendored
@@ -3,10 +3,11 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Launch (Chrome)",
|
"name": "Launch Web (Chrome)",
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "npm: start",
|
"preLaunchTask": "npm: start",
|
||||||
|
"postDebugTask": "Terminate All Tasks",
|
||||||
"url": "http://localhost:44200/",
|
"url": "http://localhost:44200/",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
16
src/Vegasco-Web/.vscode/tasks.json
vendored
16
src/Vegasco-Web/.vscode/tasks.json
vendored
@@ -2,13 +2,19 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Terminate All Tasks",
|
||||||
|
"command": "echo ${input:terminate}",
|
||||||
|
"type": "shell",
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "start",
|
"script": "start",
|
||||||
"options": {
|
"options": {
|
||||||
"env": {
|
"env": {
|
||||||
"PORT": "44200",
|
"PORT": "44200",
|
||||||
"services__Vegasco-Server-Api__https__0": "https://localhost:7098",
|
"services__Api__https__0": "https://localhost:7098",
|
||||||
"NODE_ENV": "development"
|
"NODE_ENV": "development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -45,5 +51,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "terminate",
|
||||||
|
"type": "command",
|
||||||
|
"command": "workbench.action.tasks.terminate",
|
||||||
|
"args": "terminateAll"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/Vegasco-Web/Dockerfile
Normal file
19
src/Vegasco-Web/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:latest AS build
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
ARG CONFIGURATION=development
|
||||||
|
WORKDIR /usr/local/app
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm install
|
||||||
|
RUN pnpm "build:$CONFIGURATION"
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
RUN rm /etc/nginx/conf.d/*
|
||||||
|
RUN apk add --update dos2unix
|
||||||
|
ENV DOLLAR=$
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
COPY --from=build /usr/local/app/dist/Vegasco-Web/browser .
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
RUN dos2unix /etc/nginx/nginx.conf
|
||||||
|
COPY webserver.conf.template /etc/nginx/templates/webserver.conf.template
|
||||||
|
RUN dos2unix /etc/nginx/templates/webserver.conf.template
|
||||||
|
EXPOSE 80
|
||||||
@@ -12,6 +12,16 @@ ng serve
|
|||||||
|
|
||||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## API Proxy
|
||||||
|
|
||||||
|
Because the solution utilizes Aspire which injects endpoint references for the API as environment variables, this application uses a proxy to access the API. The proxy is configured in the `proxy.config.js` file which is used in the `serve` section of the `angular.json` file. This makes the dev server provide a proxy when serving the application.
|
||||||
|
|
||||||
|
The environment variables for the API endpoint are named `services__Api__https__0` and `services__Api__http__0` for the https and the http endpoints respectively. If the https endpoint is not configured, the http endpoint is used. At least one of them has to be configured.
|
||||||
|
|
||||||
|
To allow the dev proxy to accept otherwise untrusted server certificates, set `NODE_ENV` to `development`. Otherwise the dev proxy rejects untrusted certificates.
|
||||||
|
|
||||||
|
When deploying the application elsewhere, another proxy has to be configured to provide the same functionality to ensure the application works correctly.
|
||||||
|
|
||||||
## Code scaffolding
|
## Code scaffolding
|
||||||
|
|
||||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:application",
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/tmp",
|
"outputPath": "dist/Vegasco-Web",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
|
|||||||
8
src/Vegasco-Web/nginx.conf
Normal file
8
src/Vegasco-Web/nginx.conf
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
events { }
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
|
||||||
|
resolver 127.0.0.11;
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/webserver.conf;
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@
|
|||||||
"start": "run-script-os",
|
"start": "run-script-os",
|
||||||
"start:win32": "ng serve --port %PORT% --configuration development",
|
"start:win32": "ng serve --port %PORT% --configuration development",
|
||||||
"start:default": "ng serve --port $PORT --configuration development",
|
"start:default": "ng serve --port $PORT --configuration development",
|
||||||
"build": "ng build",
|
"build": "pnpm build:development",
|
||||||
|
"build:development": "ng build",
|
||||||
|
"build:production": "ng build --configuration production",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test"
|
||||||
},
|
},
|
||||||
@@ -18,10 +20,18 @@
|
|||||||
"@angular/forms": "^19.2.14",
|
"@angular/forms": "^19.2.14",
|
||||||
"@angular/platform-browser": "^19.2.14",
|
"@angular/platform-browser": "^19.2.14",
|
||||||
"@angular/router": "^19.2.14",
|
"@angular/router": "^19.2.14",
|
||||||
|
"@ng-icons/core": "^31.4.0",
|
||||||
|
"@ng-icons/material-file-icons": "^31.4.0",
|
||||||
|
"@ng-icons/material-icons": "^31.4.0",
|
||||||
"@primeng/themes": "^19.1.3",
|
"@primeng/themes": "^19.1.3",
|
||||||
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"keycloak-angular": "^19.0.2",
|
"keycloak-angular": "^19.0.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
"primeng": "^19.1.3",
|
"primeng": "^19.1.3",
|
||||||
"rxjs": "~7.8.2",
|
"rxjs": "~7.8.2",
|
||||||
|
"tailwindcss": "^4.1.10",
|
||||||
|
"tailwindcss-primeui": "^0.6.1",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"zone.js": "~0.15.1"
|
"zone.js": "~0.15.1"
|
||||||
},
|
},
|
||||||
|
|||||||
380
src/Vegasco-Web/pnpm-lock.yaml
generated
380
src/Vegasco-Web/pnpm-lock.yaml
generated
@@ -26,18 +26,42 @@ importers:
|
|||||||
'@angular/router':
|
'@angular/router':
|
||||||
specifier: ^19.2.14
|
specifier: ^19.2.14
|
||||||
version: 19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.14(@angular/animations@19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
|
version: 19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.14(@angular/animations@19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
|
||||||
|
'@ng-icons/core':
|
||||||
|
specifier: ^31.4.0
|
||||||
|
version: 31.4.0(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||||
|
'@ng-icons/material-file-icons':
|
||||||
|
specifier: ^31.4.0
|
||||||
|
version: 31.4.0
|
||||||
|
'@ng-icons/material-icons':
|
||||||
|
specifier: ^31.4.0
|
||||||
|
version: 31.4.0
|
||||||
'@primeng/themes':
|
'@primeng/themes':
|
||||||
specifier: ^19.1.3
|
specifier: ^19.1.3
|
||||||
version: 19.1.3
|
version: 19.1.3
|
||||||
|
'@tailwindcss/postcss':
|
||||||
|
specifier: ^4.1.10
|
||||||
|
version: 4.1.10
|
||||||
|
dayjs:
|
||||||
|
specifier: ^1.11.13
|
||||||
|
version: 1.11.13
|
||||||
keycloak-angular:
|
keycloak-angular:
|
||||||
specifier: ^19.0.2
|
specifier: ^19.0.2
|
||||||
version: 19.0.2(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(@angular/router@19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.14(@angular/animations@19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(keycloak-js@26.2.0)
|
version: 19.0.2(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(@angular/router@19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.14(@angular/animations@19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(keycloak-js@26.2.0)
|
||||||
|
postcss:
|
||||||
|
specifier: ^8.5.6
|
||||||
|
version: 8.5.6
|
||||||
primeng:
|
primeng:
|
||||||
specifier: ^19.1.3
|
specifier: ^19.1.3
|
||||||
version: 19.1.3(47ee1c247593ea8ad66380722e410532)
|
version: 19.1.3(47ee1c247593ea8ad66380722e410532)
|
||||||
rxjs:
|
rxjs:
|
||||||
specifier: ~7.8.2
|
specifier: ~7.8.2
|
||||||
version: 7.8.2
|
version: 7.8.2
|
||||||
|
tailwindcss:
|
||||||
|
specifier: ^4.1.10
|
||||||
|
version: 4.1.10
|
||||||
|
tailwindcss-primeui:
|
||||||
|
specifier: ^0.6.1
|
||||||
|
version: 0.6.1(tailwindcss@4.1.10)
|
||||||
tslib:
|
tslib:
|
||||||
specifier: ^2.8.1
|
specifier: ^2.8.1
|
||||||
version: 2.8.1
|
version: 2.8.1
|
||||||
@@ -47,7 +71,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@angular-devkit/build-angular':
|
'@angular-devkit/build-angular':
|
||||||
specifier: ^19.2.15
|
specifier: ^19.2.15
|
||||||
version: 19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(@angular/compiler@19.2.14)(@types/node@24.0.3)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(typescript@5.8.3)(vite@6.2.7(@types/node@24.0.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))
|
version: 19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(@angular/compiler@19.2.14)(@types/node@24.0.3)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(lightningcss@1.30.1)(tailwindcss@4.1.10)(typescript@5.8.3)(vite@6.2.7(@types/node@24.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0))
|
||||||
'@angular/cli':
|
'@angular/cli':
|
||||||
specifier: ^19.2.15
|
specifier: ^19.2.15
|
||||||
version: 19.2.15(@types/node@24.0.3)(chokidar@4.0.3)
|
version: 19.2.15(@types/node@24.0.3)(chokidar@4.0.3)
|
||||||
@@ -84,6 +108,10 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@alloc/quick-lru@5.2.0':
|
||||||
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -1289,6 +1317,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==}
|
resolution: {integrity: sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
'@ng-icons/core@31.4.0':
|
||||||
|
resolution: {integrity: sha512-JfLiJGDX/ihWmawcnLGXtwyCqMi2qXz7gMJyXXWdUN5JA18EAnt3JnyuxDAGkoU/u7wRlcOI7irlXHU4spAKOg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@angular/common': '>=18.0.0'
|
||||||
|
'@angular/core': '>=18.0.0'
|
||||||
|
rxjs: ^6.5.3 || ^7.4.0
|
||||||
|
|
||||||
|
'@ng-icons/material-file-icons@31.4.0':
|
||||||
|
resolution: {integrity: sha512-Ffh61ghuuDRxelfTe/rHQ5IFCqUget/JeZ/NLq6QWLBycxUC6PjiEIIAXQvnVmYwCHNgxjBIRExP1/+vdHriNQ==}
|
||||||
|
|
||||||
|
'@ng-icons/material-icons@31.4.0':
|
||||||
|
resolution: {integrity: sha512-JCxwM0LXwOgT5LD99p5TwPM6dPQ5x1BGieNzAstz7vk5+aiASg3fqs3rjNx7CbN3c2QjJ8+KuKrCCBzT9DCkOQ==}
|
||||||
|
|
||||||
'@ngtools/webpack@19.2.15':
|
'@ngtools/webpack@19.2.15':
|
||||||
resolution: {integrity: sha512-H37nop/wWMkSgoU2VvrMzanHePdLRRrX52nC5tT2ZhH3qP25+PrnMyw11PoLDLv3iWXC68uB1AiKNIT+jiQbuQ==}
|
resolution: {integrity: sha512-H37nop/wWMkSgoU2VvrMzanHePdLRRrX52nC5tT2ZhH3qP25+PrnMyw11PoLDLv3iWXC68uB1AiKNIT+jiQbuQ==}
|
||||||
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
||||||
@@ -1573,6 +1614,94 @@ packages:
|
|||||||
'@socket.io/component-emitter@3.1.2':
|
'@socket.io/component-emitter@3.1.2':
|
||||||
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||||
|
|
||||||
|
'@tailwindcss/node@4.1.10':
|
||||||
|
resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==}
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-android-arm64@4.1.10':
|
||||||
|
resolution: {integrity: sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-arm64@4.1.10':
|
||||||
|
resolution: {integrity: sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-x64@4.1.10':
|
||||||
|
resolution: {integrity: sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-freebsd-x64@4.1.10':
|
||||||
|
resolution: {integrity: sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10':
|
||||||
|
resolution: {integrity: sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.10':
|
||||||
|
resolution: {integrity: sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.10':
|
||||||
|
resolution: {integrity: sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.10':
|
||||||
|
resolution: {integrity: sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl@4.1.10':
|
||||||
|
resolution: {integrity: sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi@4.1.10':
|
||||||
|
resolution: {integrity: sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
cpu: [wasm32]
|
||||||
|
bundledDependencies:
|
||||||
|
- '@napi-rs/wasm-runtime'
|
||||||
|
- '@emnapi/core'
|
||||||
|
- '@emnapi/runtime'
|
||||||
|
- '@tybys/wasm-util'
|
||||||
|
- '@emnapi/wasi-threads'
|
||||||
|
- tslib
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc@4.1.10':
|
||||||
|
resolution: {integrity: sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc@4.1.10':
|
||||||
|
resolution: {integrity: sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@tailwindcss/oxide@4.1.10':
|
||||||
|
resolution: {integrity: sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
'@tailwindcss/postcss@4.1.10':
|
||||||
|
resolution: {integrity: sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ==}
|
||||||
|
|
||||||
'@tufjs/canonical-json@2.0.0':
|
'@tufjs/canonical-json@2.0.0':
|
||||||
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
|
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
|
||||||
engines: {node: ^16.14.0 || >=18.0.0}
|
engines: {node: ^16.14.0 || >=18.0.0}
|
||||||
@@ -2103,6 +2232,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
|
resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
dayjs@1.11.13:
|
||||||
|
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2787,6 +2919,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jiti@2.4.2:
|
||||||
|
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -2904,6 +3040,70 @@ packages:
|
|||||||
webpack:
|
webpack:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
|
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.30.1:
|
||||||
|
resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.1:
|
||||||
|
resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||||
|
resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.1:
|
||||||
|
resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.1:
|
||||||
|
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.1:
|
||||||
|
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.1:
|
||||||
|
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.1:
|
||||||
|
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.1:
|
||||||
|
resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss@1.30.1:
|
||||||
|
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
lines-and-columns@1.2.4:
|
lines-and-columns@1.2.4:
|
||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
@@ -3865,6 +4065,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
|
|
||||||
|
tailwindcss-primeui@0.6.1:
|
||||||
|
resolution: {integrity: sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A==}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '>=3.1.0'
|
||||||
|
|
||||||
|
tailwindcss@4.1.10:
|
||||||
|
resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==}
|
||||||
|
|
||||||
tapable@2.2.2:
|
tapable@2.2.2:
|
||||||
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
|
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -4249,6 +4457,8 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/gen-mapping': 0.3.8
|
'@jridgewell/gen-mapping': 0.3.8
|
||||||
@@ -4261,13 +4471,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- chokidar
|
||||||
|
|
||||||
'@angular-devkit/build-angular@19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(@angular/compiler@19.2.14)(@types/node@24.0.3)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(typescript@5.8.3)(vite@6.2.7(@types/node@24.0.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))':
|
'@angular-devkit/build-angular@19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(@angular/compiler@19.2.14)(@types/node@24.0.3)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(lightningcss@1.30.1)(tailwindcss@4.1.10)(typescript@5.8.3)(vite@6.2.7(@types/node@24.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@angular-devkit/architect': 0.1902.15(chokidar@4.0.3)
|
'@angular-devkit/architect': 0.1902.15(chokidar@4.0.3)
|
||||||
'@angular-devkit/build-webpack': 0.1902.15(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.4))
|
'@angular-devkit/build-webpack': 0.1902.15(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.98.0))(webpack@5.98.0(esbuild@0.25.4))
|
||||||
'@angular-devkit/core': 19.2.15(chokidar@4.0.3)
|
'@angular-devkit/core': 19.2.15(chokidar@4.0.3)
|
||||||
'@angular/build': 19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(@angular/compiler@19.2.14)(@types/node@24.0.3)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(terser@5.39.0)(typescript@5.8.3)
|
'@angular/build': 19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(@angular/compiler@19.2.14)(@types/node@24.0.3)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(less@4.2.2)(lightningcss@1.30.1)(postcss@8.5.2)(tailwindcss@4.1.10)(terser@5.39.0)(typescript@5.8.3)
|
||||||
'@angular/compiler-cli': 19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3)
|
'@angular/compiler-cli': 19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3)
|
||||||
'@babel/core': 7.26.10
|
'@babel/core': 7.26.10
|
||||||
'@babel/generator': 7.26.10
|
'@babel/generator': 7.26.10
|
||||||
@@ -4280,7 +4490,7 @@ snapshots:
|
|||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.26.10
|
||||||
'@discoveryjs/json-ext': 0.6.3
|
'@discoveryjs/json-ext': 0.6.3
|
||||||
'@ngtools/webpack': 19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.4))
|
'@ngtools/webpack': 19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.4))
|
||||||
'@vitejs/plugin-basic-ssl': 1.2.0(vite@6.2.7(@types/node@24.0.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))
|
'@vitejs/plugin-basic-ssl': 1.2.0(vite@6.2.7(@types/node@24.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0))
|
||||||
ansi-colors: 4.1.3
|
ansi-colors: 4.1.3
|
||||||
autoprefixer: 10.4.20(postcss@8.5.2)
|
autoprefixer: 10.4.20(postcss@8.5.2)
|
||||||
babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.98.0(esbuild@0.25.4))
|
babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.98.0(esbuild@0.25.4))
|
||||||
@@ -4323,6 +4533,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
karma: 6.4.4
|
karma: 6.4.4
|
||||||
|
tailwindcss: 4.1.10
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@angular/compiler'
|
- '@angular/compiler'
|
||||||
- '@rspack/core'
|
- '@rspack/core'
|
||||||
@@ -4382,7 +4593,7 @@ snapshots:
|
|||||||
'@angular/core': 19.2.14(rxjs@7.8.2)(zone.js@0.15.1)
|
'@angular/core': 19.2.14(rxjs@7.8.2)(zone.js@0.15.1)
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@angular/build@19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(@angular/compiler@19.2.14)(@types/node@24.0.3)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(terser@5.39.0)(typescript@5.8.3)':
|
'@angular/build@19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(@angular/compiler@19.2.14)(@types/node@24.0.3)(chokidar@4.0.3)(jiti@2.4.2)(karma@6.4.4)(less@4.2.2)(lightningcss@1.30.1)(postcss@8.5.2)(tailwindcss@4.1.10)(terser@5.39.0)(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@angular-devkit/architect': 0.1902.15(chokidar@4.0.3)
|
'@angular-devkit/architect': 0.1902.15(chokidar@4.0.3)
|
||||||
@@ -4393,7 +4604,7 @@ snapshots:
|
|||||||
'@babel/helper-split-export-declaration': 7.24.7
|
'@babel/helper-split-export-declaration': 7.24.7
|
||||||
'@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.10)
|
'@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.10)
|
||||||
'@inquirer/confirm': 5.1.6(@types/node@24.0.3)
|
'@inquirer/confirm': 5.1.6(@types/node@24.0.3)
|
||||||
'@vitejs/plugin-basic-ssl': 1.2.0(vite@6.2.7(@types/node@24.0.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))
|
'@vitejs/plugin-basic-ssl': 1.2.0(vite@6.2.7(@types/node@24.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0))
|
||||||
beasties: 0.3.2
|
beasties: 0.3.2
|
||||||
browserslist: 4.25.0
|
browserslist: 4.25.0
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
@@ -4411,13 +4622,14 @@ snapshots:
|
|||||||
semver: 7.7.1
|
semver: 7.7.1
|
||||||
source-map-support: 0.5.21
|
source-map-support: 0.5.21
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
vite: 6.2.7(@types/node@24.0.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)
|
vite: 6.2.7(@types/node@24.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)
|
||||||
watchpack: 2.4.2
|
watchpack: 2.4.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
karma: 6.4.4
|
karma: 6.4.4
|
||||||
less: 4.2.2
|
less: 4.2.2
|
||||||
lmdb: 3.2.6
|
lmdb: 3.2.6
|
||||||
postcss: 8.5.2
|
postcss: 8.5.2
|
||||||
|
tailwindcss: 4.1.10
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- chokidar
|
- chokidar
|
||||||
@@ -5613,6 +5825,21 @@ snapshots:
|
|||||||
'@napi-rs/nice-win32-x64-msvc': 1.0.1
|
'@napi-rs/nice-win32-x64-msvc': 1.0.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@ng-icons/core@31.4.0(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@angular/common': 19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
|
||||||
|
'@angular/core': 19.2.14(rxjs@7.8.2)(zone.js@0.15.1)
|
||||||
|
rxjs: 7.8.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@ng-icons/material-file-icons@31.4.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@ng-icons/material-icons@31.4.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@ngtools/webpack@19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.4))':
|
'@ngtools/webpack@19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.4))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular/compiler-cli': 19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3)
|
'@angular/compiler-cli': 19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3)
|
||||||
@@ -5865,6 +6092,78 @@ snapshots:
|
|||||||
|
|
||||||
'@socket.io/component-emitter@3.1.2': {}
|
'@socket.io/component-emitter@3.1.2': {}
|
||||||
|
|
||||||
|
'@tailwindcss/node@4.1.10':
|
||||||
|
dependencies:
|
||||||
|
'@ampproject/remapping': 2.3.0
|
||||||
|
enhanced-resolve: 5.18.1
|
||||||
|
jiti: 2.4.2
|
||||||
|
lightningcss: 1.30.1
|
||||||
|
magic-string: 0.30.17
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
tailwindcss: 4.1.10
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-android-arm64@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-arm64@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-darwin-x64@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-freebsd-x64@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc@4.1.10':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tailwindcss/oxide@4.1.10':
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.0.4
|
||||||
|
tar: 7.4.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@tailwindcss/oxide-android-arm64': 4.1.10
|
||||||
|
'@tailwindcss/oxide-darwin-arm64': 4.1.10
|
||||||
|
'@tailwindcss/oxide-darwin-x64': 4.1.10
|
||||||
|
'@tailwindcss/oxide-freebsd-x64': 4.1.10
|
||||||
|
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.10
|
||||||
|
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.10
|
||||||
|
'@tailwindcss/oxide-linux-arm64-musl': 4.1.10
|
||||||
|
'@tailwindcss/oxide-linux-x64-gnu': 4.1.10
|
||||||
|
'@tailwindcss/oxide-linux-x64-musl': 4.1.10
|
||||||
|
'@tailwindcss/oxide-wasm32-wasi': 4.1.10
|
||||||
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.10
|
||||||
|
'@tailwindcss/oxide-win32-x64-msvc': 4.1.10
|
||||||
|
|
||||||
|
'@tailwindcss/postcss@4.1.10':
|
||||||
|
dependencies:
|
||||||
|
'@alloc/quick-lru': 5.2.0
|
||||||
|
'@tailwindcss/node': 4.1.10
|
||||||
|
'@tailwindcss/oxide': 4.1.10
|
||||||
|
postcss: 8.5.6
|
||||||
|
tailwindcss: 4.1.10
|
||||||
|
|
||||||
'@tufjs/canonical-json@2.0.0': {}
|
'@tufjs/canonical-json@2.0.0': {}
|
||||||
|
|
||||||
'@tufjs/models@3.0.1':
|
'@tufjs/models@3.0.1':
|
||||||
@@ -5969,9 +6268,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.0.3
|
'@types/node': 24.0.3
|
||||||
|
|
||||||
'@vitejs/plugin-basic-ssl@1.2.0(vite@6.2.7(@types/node@24.0.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0))':
|
'@vitejs/plugin-basic-ssl@1.2.0(vite@6.2.7(@types/node@24.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 6.2.7(@types/node@24.0.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)
|
vite: 6.2.7(@types/node@24.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)
|
||||||
|
|
||||||
'@webassemblyjs/ast@1.14.1':
|
'@webassemblyjs/ast@1.14.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6470,6 +6769,8 @@ snapshots:
|
|||||||
|
|
||||||
date-format@4.0.14: {}
|
date-format@4.0.14: {}
|
||||||
|
|
||||||
|
dayjs@1.11.13: {}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.0.0
|
ms: 2.0.0
|
||||||
@@ -6504,8 +6805,7 @@ snapshots:
|
|||||||
detect-libc@1.0.3:
|
detect-libc@1.0.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
detect-libc@2.0.4:
|
detect-libc@2.0.4: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
detect-node@2.1.0: {}
|
detect-node@2.1.0: {}
|
||||||
|
|
||||||
@@ -7182,6 +7482,8 @@ snapshots:
|
|||||||
|
|
||||||
jiti@1.21.7: {}
|
jiti@1.21.7: {}
|
||||||
|
|
||||||
|
jiti@2.4.2: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@4.1.0:
|
js-yaml@4.1.0:
|
||||||
@@ -7315,6 +7617,51 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
webpack: 5.98.0(esbuild@0.25.4)
|
webpack: 5.98.0(esbuild@0.25.4)
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss@1.30.1:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.0.4
|
||||||
|
optionalDependencies:
|
||||||
|
lightningcss-darwin-arm64: 1.30.1
|
||||||
|
lightningcss-darwin-x64: 1.30.1
|
||||||
|
lightningcss-freebsd-x64: 1.30.1
|
||||||
|
lightningcss-linux-arm-gnueabihf: 1.30.1
|
||||||
|
lightningcss-linux-arm64-gnu: 1.30.1
|
||||||
|
lightningcss-linux-arm64-musl: 1.30.1
|
||||||
|
lightningcss-linux-x64-gnu: 1.30.1
|
||||||
|
lightningcss-linux-x64-musl: 1.30.1
|
||||||
|
lightningcss-win32-arm64-msvc: 1.30.1
|
||||||
|
lightningcss-win32-x64-msvc: 1.30.1
|
||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
listr2@8.2.5:
|
listr2@8.2.5:
|
||||||
@@ -8385,6 +8732,12 @@ snapshots:
|
|||||||
|
|
||||||
symbol-observable@4.0.0: {}
|
symbol-observable@4.0.0: {}
|
||||||
|
|
||||||
|
tailwindcss-primeui@0.6.1(tailwindcss@4.1.10):
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 4.1.10
|
||||||
|
|
||||||
|
tailwindcss@4.1.10: {}
|
||||||
|
|
||||||
tapable@2.2.2: {}
|
tapable@2.2.2: {}
|
||||||
|
|
||||||
tar@6.2.1:
|
tar@6.2.1:
|
||||||
@@ -8523,7 +8876,7 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite@6.2.7(@types/node@24.0.3)(jiti@1.21.7)(less@4.2.2)(sass@1.85.0)(terser@5.39.0):
|
vite@6.2.7(@types/node@24.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@@ -8531,8 +8884,9 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.0.3
|
'@types/node': 24.0.3
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 1.21.7
|
jiti: 2.4.2
|
||||||
less: 4.2.2
|
less: 4.2.2
|
||||||
|
lightningcss: 1.30.1
|
||||||
sass: 1.85.0
|
sass: 1.85.0
|
||||||
terser: 5.39.0
|
terser: 5.39.0
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"/api": {
|
"/api": {
|
||||||
target:
|
target:
|
||||||
process.env["services__Vegasco-Server-Api__https__0"] ||
|
process.env["services__Api__https__0"] ||
|
||||||
process.env["services__Vegasco-Server-Api__http__0"],
|
process.env["services__Api__http__0"],
|
||||||
secure: process.env["NODE_ENV"] !== "development",
|
secure: process.env["NODE_ENV"] !== "development",
|
||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
"^/api": "",
|
"^/api": "",
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export class CarClient {
|
|||||||
return this.http.post<Car>(`${this.apiBasePath}/v1/cars`, request);
|
return this.http.post<Car>(`${this.apiBasePath}/v1/cars`, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(request: UpdateCarRequest): Observable<Car> {
|
update(id: string, request: UpdateCarRequest): Observable<Car> {
|
||||||
return this.http.put<Car>(`${this.apiBasePath}/v1/cars`, request);
|
return this.http.put<Car>(`${this.apiBasePath}/v1/cars/${id}`, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id: string): Observable<void> {
|
delete(id: string): Observable<void> {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {inject, Injectable} from '@angular/core';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import {API_BASE_PATH} from '../api-base-path';
|
|
||||||
import { map, Observable } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { API_BASE_PATH } from '../api-base-path';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -18,12 +18,12 @@ export class ConsumptionClient {
|
|||||||
return this.http.get<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions/${id}`);
|
return this.http.get<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
create(request: CreateCarRequest): Observable<ConsumptionEntry> {
|
create(request: CreateConsumptionEntry): Observable<ConsumptionEntry> {
|
||||||
return this.http.post<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions`, request);
|
return this.http.post<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions`, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(request: UpdateCarRequest): Observable<ConsumptionEntry> {
|
update(id: string, request: UpdateConsumptionEntry): Observable<ConsumptionEntry> {
|
||||||
return this.http.put<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions`, request);
|
return this.http.put<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions/${id}`, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id: string): Observable<void> {
|
delete(id: string): Observable<void> {
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ interface ConsumptionEntry {
|
|||||||
dateTime: string;
|
dateTime: string;
|
||||||
distance: number;
|
distance: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
ignoreInCalculation: boolean;
|
|
||||||
carId: string;
|
carId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,5 @@ interface CreateConsumptionEntry {
|
|||||||
dateTime: string;
|
dateTime: string;
|
||||||
distance: number;
|
distance: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
ignoreInCalculation: boolean;
|
|
||||||
carId: string;
|
carId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
interface GetConsumptionEntriesEntry {
|
||||||
|
id: string;
|
||||||
|
dateTime: string;
|
||||||
|
distance: number;
|
||||||
|
amount: number;
|
||||||
|
car: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
literPer100Km: number | null;
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
interface GetConsumptionEntriesResponse {
|
interface GetConsumptionEntriesResponse {
|
||||||
consumptions: ConsumptionEntry[];
|
consumptions: GetConsumptionEntriesEntry[];
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,5 @@ interface UpdateConsumptionEntry {
|
|||||||
dateTime: string;
|
dateTime: string;
|
||||||
distance: number;
|
distance: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
ignoreInCalculation: boolean;
|
|
||||||
carId: string;
|
carId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="content">
|
<header class="h-12 bg-primary text-primary-contrast">
|
||||||
<router-outlet/>
|
<div class="header max-content-width mx-auto flex items-center justify-between">
|
||||||
|
<a routerLink="/" class="reset cursor-pointer">
|
||||||
|
Vegasco
|
||||||
|
</a>
|
||||||
|
<span class="flex items-center gap-4">
|
||||||
|
<a routerLink="/entries" class="reset cursor-pointer">
|
||||||
|
Einträge
|
||||||
|
</a>
|
||||||
|
<a routerLink="/cars" class="reset cursor-pointer">
|
||||||
|
Autos
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="content max-content-width mx-auto">
|
||||||
|
<p-toast />
|
||||||
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -9,5 +9,9 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'entries',
|
path: 'entries',
|
||||||
loadChildren: () => import('./modules/entries/entries.routes').then(m => m.routes)
|
loadChildren: () => import('./modules/entries/entries.routes').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'cars',
|
||||||
|
loadChildren: () => import('./modules/cars/cars.routes').then(m => m.routes)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterLink, RouterOutlet } from '@angular/router';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { ToastModule } from 'primeng/toast';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet],
|
imports: [RouterLink, RouterOutlet, ToastModule],
|
||||||
|
providers: [MessageService],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss'
|
styleUrl: './app.scss'
|
||||||
})
|
})
|
||||||
export class App {
|
export class App {
|
||||||
protected title = 'Vegasco-Web';
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/Vegasco-Web/src/app/modules/cars/cars.routes.ts
Normal file
17
src/Vegasco-Web/src/app/modules/cars/cars.routes.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Routes } from "@angular/router";
|
||||||
|
import { CarsComponent } from "./cars/cars.component";
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CarsComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
loadComponent: () => import('./edit-car/edit-car.component').then(m => m.EditCarComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
loadComponent: () => import('./edit-car/edit-car.component').then(m => m.EditCarComponent)
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<section>
|
||||||
|
<p-scrollTop />
|
||||||
|
<div class="mb-4 flex gap-2 md:justify-end">
|
||||||
|
<div>
|
||||||
|
<p-button label="Erstellen" routerLink="/cars/create">
|
||||||
|
<ng-icon name="matAddSharp"></ng-icon>
|
||||||
|
</p-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (nonDeletedCars$ | async; as cars) {
|
||||||
|
<p-dataView
|
||||||
|
[value]="cars"
|
||||||
|
[paginator]="true"
|
||||||
|
[rows]="25"
|
||||||
|
[rowsPerPageOptions]="[10, 25, 50, 100]"
|
||||||
|
[pageLinks]="0"
|
||||||
|
[showCurrentPageReport]="true"
|
||||||
|
currentPageReportTemplate="{currentPage} / {totalPages}"
|
||||||
|
layout="list">
|
||||||
|
<ng-template #list let-cars>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
@for (car of cars; track car.id) {
|
||||||
|
<app-car-card [car]="car"
|
||||||
|
(carDeleted)="onCarDeleted($event)" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</p-dataView>
|
||||||
|
} @else {
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
@for (_ of skeletonsIterationSource; track $index) {
|
||||||
|
<p-skeleton height="4rem" styleClass="mb-2" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
th, td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
127
src/Vegasco-Web/src/app/modules/cars/cars/cars.component.ts
Normal file
127
src/Vegasco-Web/src/app/modules/cars/cars/cars.component.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, DestroyRef, inject } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
matAddSharp,
|
||||||
|
} from '@ng-icons/material-icons/sharp';
|
||||||
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { DataViewModule } from 'primeng/dataview';
|
||||||
|
import { ScrollTopModule } from 'primeng/scrolltop';
|
||||||
|
import { SelectModule } from 'primeng/select';
|
||||||
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
catchError,
|
||||||
|
combineLatest,
|
||||||
|
EMPTY,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
throwError
|
||||||
|
} from 'rxjs';
|
||||||
|
import { CarCardComponent } from './components/car-card/car-card.component';
|
||||||
|
import { SelectedCarService } from '@vegasco-web/modules/entries/services/selected-car.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-entries',
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
ButtonModule,
|
||||||
|
CommonModule,
|
||||||
|
DataViewModule,
|
||||||
|
CarCardComponent,
|
||||||
|
NgIconComponent,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterLink,
|
||||||
|
ScrollTopModule,
|
||||||
|
SelectModule,
|
||||||
|
SkeletonModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
matAddSharp,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
templateUrl: './cars.component.html',
|
||||||
|
styleUrl: './cars.component.scss'
|
||||||
|
})
|
||||||
|
export class CarsComponent {
|
||||||
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly selectedCarService = inject(SelectedCarService);
|
||||||
|
|
||||||
|
protected readonly nonDeletedCars$: Observable<Car[]>;
|
||||||
|
|
||||||
|
protected readonly skeletonsIterationSource = Array(10).fill(0);
|
||||||
|
|
||||||
|
private readonly deletedCars$ = new BehaviorSubject(<string[]>[]);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const cars$ = this.carClient.getAll()
|
||||||
|
.pipe(
|
||||||
|
map(response => response.cars),
|
||||||
|
map((cars) => cars
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.nonDeletedCars$ = combineLatest([
|
||||||
|
cars$,
|
||||||
|
this.deletedCars$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
map(([cars, deletedCars]) => cars.filter(car => !deletedCars.includes(car.id))),
|
||||||
|
catchError((error) => this.handleGetCarsError(error)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCarDeleted(car: Car): void {
|
||||||
|
this.deletedCars$.next([...this.deletedCars$.value, car.id]);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Auto gelöscht',
|
||||||
|
detail: 'Das Auto wurde erfolgreich gelöscht.',
|
||||||
|
});
|
||||||
|
this.resetSelectedCarIfDeleted(car);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetSelectedCarIfDeleted(car: Car) {
|
||||||
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||||
|
if (selectedCarId === car.id) {
|
||||||
|
this.selectedCarService.setSelectedCarId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetCarsError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => new Error('An unexpected error occurred'));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<p-confirmDialog></p-confirmDialog>
|
||||||
|
|
||||||
|
<div class="flex rounded-border shadow">
|
||||||
|
<div class="grow p-4 pos-relative edit-button" (click)="navigateToEdit()" role="button"
|
||||||
|
aria-roledescription="Bearbeite diesen Eintrag">
|
||||||
|
<div class="grid grid-cols-4 gap-4">
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matDirectionsCarOutline" />
|
||||||
|
<div>{{ car().name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-red-500 text-white rounded-r text-center flex flex-col justify-center">
|
||||||
|
<button type="button" title="Löschen" class="reset cursor-pointer primary-color-text p-4 h-full rounded-r"
|
||||||
|
(click)="confirmDeleteCar()">
|
||||||
|
<ng-icon name="matDeleteSharp"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.edit-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, DestroyRef, inject, input, output } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
matDirectionsCarOutline,
|
||||||
|
} from '@ng-icons/material-icons/outline';
|
||||||
|
import {
|
||||||
|
matDeleteSharp
|
||||||
|
} from '@ng-icons/material-icons/sharp';
|
||||||
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { CardModule } from 'primeng/card';
|
||||||
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
|
import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-car-card',
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CardModule,
|
||||||
|
ConfirmDialogModule,
|
||||||
|
NgIconComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
matDeleteSharp,
|
||||||
|
matDirectionsCarOutline,
|
||||||
|
}),
|
||||||
|
ConfirmationService,
|
||||||
|
],
|
||||||
|
templateUrl: './car-card.component.html',
|
||||||
|
styleUrl: './car-card.component.scss'
|
||||||
|
})
|
||||||
|
export class CarCardComponent {
|
||||||
|
readonly car = input.required<Car>();
|
||||||
|
|
||||||
|
readonly carDeleted = output<Car>();
|
||||||
|
|
||||||
|
private readonly routingService = inject(RoutingService);
|
||||||
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly confirmationService = inject(ConfirmationService);
|
||||||
|
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
async navigateToEdit(): Promise<void> {
|
||||||
|
await this.routingService.navigateToEditCar(this.car().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteCar(): void {
|
||||||
|
this.confirmationService.confirm({
|
||||||
|
closeOnEscape: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
header: 'Bist du sicher?',
|
||||||
|
message: `Möchtest du das Auto "${this.car().name}" wirklich löschen?`,
|
||||||
|
acceptButtonProps: {
|
||||||
|
label: 'Löschen',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
rejectButtonProps: {
|
||||||
|
label: 'Abbrechen',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
accept: () => this.deleteCar(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCar(): void {
|
||||||
|
this.carClient.delete(this.car().id)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
tap(() => this.carDeleted.emit(this.car())),
|
||||||
|
catchError((error) => this.handleError(error)),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Löschen des Autos ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 400:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Clientfehler',
|
||||||
|
detail:
|
||||||
|
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Löschen des Autos hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<span class="required">*</span>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-required-marker',
|
||||||
|
imports: [
|
||||||
|
],
|
||||||
|
templateUrl: './required-marker.component.html',
|
||||||
|
styleUrl: './required-marker.component.scss'
|
||||||
|
})
|
||||||
|
export class RequiredMarkerComponent {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@if (!isCarDataLoaded()) {
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<form [formGroup]="formGroup" class="flex flex-col gap-4" (ngSubmit)="onSubmit()">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label [for]="formFieldNames.name">
|
||||||
|
Name
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
placeholder="Name eingeben"
|
||||||
|
type="text"
|
||||||
|
pInputText
|
||||||
|
[formControlName]="formFieldNames.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<p-button type="button" label="Abbrechen" (click)="navigateToOverviewPage()" severity="warn" />
|
||||||
|
<p-button type="submit" label="Abschicken" severity="success" [disabled]="formGroup.invalid" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, DestroyRef, inject, input, OnInit, signal } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { ChipModule } from 'primeng/chip';
|
||||||
|
import { DatePickerModule } from 'primeng/datepicker';
|
||||||
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
|
import { InputGroupModule } from 'primeng/inputgroup';
|
||||||
|
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
|
||||||
|
import { InputNumberModule } from 'primeng/inputnumber';
|
||||||
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
|
import { MultiSelectModule } from 'primeng/multiselect';
|
||||||
|
import { SelectModule } from 'primeng/select';
|
||||||
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
|
import { catchError, EMPTY, Observable, switchMap, tap, throwError } from 'rxjs';
|
||||||
|
import { RequiredMarkerComponent } from './components/required-marker.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-entry',
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
ChipModule,
|
||||||
|
DatePickerModule,
|
||||||
|
FloatLabelModule,
|
||||||
|
InputGroupAddonModule,
|
||||||
|
InputGroupModule,
|
||||||
|
InputNumberModule,
|
||||||
|
InputTextModule,
|
||||||
|
MultiSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RequiredMarkerComponent,
|
||||||
|
SelectModule,
|
||||||
|
SkeletonModule,
|
||||||
|
],
|
||||||
|
templateUrl: './edit-car.component.html',
|
||||||
|
styleUrl: './edit-car.component.scss'
|
||||||
|
})
|
||||||
|
export class EditCarComponent implements OnInit {
|
||||||
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly routingService = inject(RoutingService);
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
|
||||||
|
protected readonly id = input<string | undefined>(undefined);
|
||||||
|
|
||||||
|
protected readonly today = new Date();
|
||||||
|
|
||||||
|
protected readonly formFieldNames = {
|
||||||
|
name: 'name',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
protected readonly formGroup = new FormGroup({
|
||||||
|
[this.formFieldNames.name]: new FormControl<string | null>({ value: null, disabled: true }, [Validators.required]),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected readonly isCarDataLoaded = signal(false);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadEntryDetailsAndEnableControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadEntryDetailsAndEnableControls() {
|
||||||
|
const carId = this.id();
|
||||||
|
|
||||||
|
if (carId === undefined || carId === null) {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isCarDataLoaded.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.carClient
|
||||||
|
.getSingle(carId)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleGetError(error)),
|
||||||
|
tap((car) => {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
[this.formFieldNames.name]: car.name,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isCarDataLoaded.set(true);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private enableFormControls(): void {
|
||||||
|
for (const controlName of Object.values(this.formFieldNames)) {
|
||||||
|
const control = this.formGroup.get(controlName);
|
||||||
|
if (control) {
|
||||||
|
control.enable();
|
||||||
|
} else {
|
||||||
|
console.warn(`Form control '${controlName}' not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToOverviewPage(): Promise<void> {
|
||||||
|
await this.routingService.navigateToCars();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var carId = this.id();
|
||||||
|
if (carId === undefined || carId === null) {
|
||||||
|
this.createCar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCar(carId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormData() {
|
||||||
|
return {
|
||||||
|
name: this.formGroup.controls[this.formFieldNames.name].value!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createCar() {
|
||||||
|
var request: CreateCarRequest = this.getFormData();
|
||||||
|
this.carClient.create(request)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleCreateOrUpdateError(error, false)),
|
||||||
|
switchMap(() => this.routingService.navigateToCars())
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCar(id: string) {
|
||||||
|
var request: UpdateCarRequest = this.getFormData();
|
||||||
|
this.carClient.update(id, request)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleCreateOrUpdateError(error, true)),
|
||||||
|
switchMap(() => this.routingService.navigateToCars())
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen des Autos ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen des Autos hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreateOrUpdateError(error: unknown, isUpdate: boolean): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = isUpdate ? 'Aktualisieren' : 'Erstellen';
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
`Beim ${action} des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 400:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Clientfehler',
|
||||||
|
detail:
|
||||||
|
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 409:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Konflikt',
|
||||||
|
detail:
|
||||||
|
'Es existiert bereits ein Auto mit diesem Namen. Bitte wähle einen anderen Namen.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
`Beim ${action} des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<span class="required">*</span>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-required-marker',
|
||||||
|
imports: [
|
||||||
|
],
|
||||||
|
templateUrl: './required-marker.component.html',
|
||||||
|
styleUrl: './required-marker.component.scss'
|
||||||
|
})
|
||||||
|
export class RequiredMarkerComponent {
|
||||||
|
}
|
||||||
@@ -1,27 +1,46 @@
|
|||||||
@if (isLoading()) {
|
@if (isLoading()) {
|
||||||
<p-skeleton height="4rem" styleClass="mb-2" />
|
<div class="flex flex-col gap-6">
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<form [formGroup]="formGroup" class="flex flex-col gap-4" (ngSubmit)="onSubmit()">
|
<form [formGroup]="formGroup" class="flex flex-col gap-4" (ngSubmit)="onSubmit()">
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label [for]="formFieldNames.car"> Auto </label>
|
<label [for]="formFieldNames.car">
|
||||||
|
Auto
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
@if (cars(); as cars) {
|
@if (cars(); as cars) {
|
||||||
<p-select
|
<p-select
|
||||||
[options]="cars"
|
[options]="cars"
|
||||||
placeholder="Auto auswählen"
|
placeholder="Auto auswählen"
|
||||||
[formControlName]="formFieldNames.car"
|
[formControlName]="formFieldNames.car"
|
||||||
[optionLabel]="formFieldNames.car"
|
optionLabel="name"
|
||||||
[inputId]="formFieldNames.car"
|
[inputId]="formFieldNames.car"
|
||||||
styleClass="w-full" />
|
styleClass="w-full" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label [for]="formFieldNames.date"> Datum </label>
|
<div class="flex gap-2 items-center">
|
||||||
|
<label [for]="formFieldNames.date">
|
||||||
|
Datum
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<p-datepicker [iconDisplay]="'input'"
|
<p-datepicker [iconDisplay]="'input'"
|
||||||
[firstDayOfWeek]="1"
|
[firstDayOfWeek]="1"
|
||||||
placeholder="Datum auswählen"
|
placeholder="Datum auswählen"
|
||||||
[showIcon]="true"
|
[showIcon]="true"
|
||||||
|
[maxDate]="today"
|
||||||
|
[defaultDate]="today"
|
||||||
[inputId]="formFieldNames.date"
|
[inputId]="formFieldNames.date"
|
||||||
[formControlName]="formFieldNames.date"
|
[formControlName]="formFieldNames.date"
|
||||||
styleClass="w-full"
|
styleClass="w-full"
|
||||||
@@ -29,7 +48,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label [for]="formFieldNames.mileage"> Kilometerstand </label>
|
<label [for]="formFieldNames.mileage">
|
||||||
|
Kilometerstand
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
<p-inputGroup>
|
<p-inputGroup>
|
||||||
<input
|
<input
|
||||||
id="mileage"
|
id="mileage"
|
||||||
@@ -43,7 +65,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label [for]="formFieldNames.amount"> Menge </label>
|
<label [for]="formFieldNames.amount">
|
||||||
|
Menge
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
<p-inputGroup>
|
<p-inputGroup>
|
||||||
<input
|
<input
|
||||||
id="amount"
|
id="amount"
|
||||||
@@ -52,7 +77,7 @@
|
|||||||
min="1"
|
min="1"
|
||||||
pInputText
|
pInputText
|
||||||
[formControlName]="formFieldNames.amount" />
|
[formControlName]="formFieldNames.amount" />
|
||||||
<p-inputGroupAddon>l</p-inputGroupAddon>
|
<p-inputGroupAddon>ℓ</p-inputGroupAddon>
|
||||||
</p-inputGroup>
|
</p-inputGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { Component, computed, inject, Signal } from '@angular/core';
|
import dayjs from 'dayjs';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, computed, DestroyRef, inject, input, OnInit, signal, Signal } from '@angular/core';
|
||||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
|
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
||||||
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { ChipModule } from 'primeng/chip';
|
import { ChipModule } from 'primeng/chip';
|
||||||
import { DatePickerModule } from 'primeng/datepicker';
|
import { DatePickerModule } from 'primeng/datepicker';
|
||||||
@@ -14,7 +18,9 @@ import { InputTextModule } from 'primeng/inputtext';
|
|||||||
import { MultiSelectModule } from 'primeng/multiselect';
|
import { MultiSelectModule } from 'primeng/multiselect';
|
||||||
import { SelectModule } from 'primeng/select';
|
import { SelectModule } from 'primeng/select';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
import { map } from 'rxjs';
|
import { catchError, combineLatest, EMPTY, filter, map, Observable, switchMap, tap, throwError } from 'rxjs';
|
||||||
|
import { RequiredMarkerComponent } from './components/required-marker.component';
|
||||||
|
import { SelectedCarService } from '../services/selected-car.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-edit-entry',
|
selector: 'app-edit-entry',
|
||||||
@@ -29,141 +35,259 @@ import { map } from 'rxjs';
|
|||||||
InputTextModule,
|
InputTextModule,
|
||||||
MultiSelectModule,
|
MultiSelectModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
RequiredMarkerComponent,
|
||||||
SelectModule,
|
SelectModule,
|
||||||
SkeletonModule,
|
SkeletonModule,
|
||||||
],
|
],
|
||||||
templateUrl: './edit-entry.component.html',
|
templateUrl: './edit-entry.component.html',
|
||||||
styleUrl: './edit-entry.component.scss'
|
styleUrl: './edit-entry.component.scss'
|
||||||
})
|
})
|
||||||
export class EditEntryComponent {
|
export class EditEntryComponent implements OnInit {
|
||||||
private readonly formBuilder = inject(FormBuilder);
|
|
||||||
private readonly carClient = inject(CarClient);
|
private readonly carClient = inject(CarClient);
|
||||||
private readonly router = inject(Router);
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
|
private readonly routingService = inject(RoutingService);
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly selectedCarService = inject(SelectedCarService);
|
||||||
|
|
||||||
|
protected readonly id = input<string | undefined>(undefined);
|
||||||
|
|
||||||
|
protected readonly today = new Date();
|
||||||
|
|
||||||
protected readonly formFieldNames = {
|
protected readonly formFieldNames = {
|
||||||
car: 'car',
|
car: 'car',
|
||||||
date: 'date',
|
date: 'date',
|
||||||
mileage: 'mileage',
|
mileage: 'mileage',
|
||||||
amount: 'amount',
|
amount: 'amount',
|
||||||
}
|
} as const;
|
||||||
|
|
||||||
protected readonly formGroup = this.formBuilder.group({
|
protected readonly formGroup = new FormGroup({
|
||||||
[this.formFieldNames.car]: [<Car | null>null, Validators.required],
|
[this.formFieldNames.car]: new FormControl<Car | null>({ value: null, disabled: true }, [Validators.required]),
|
||||||
[this.formFieldNames.date]: [<Date | null>null],
|
[this.formFieldNames.date]: new FormControl<Date>({ value: new Date(), disabled: true }, [Validators.required, this.dateTimeGreaterThanOrEqualToTodayValidator]),
|
||||||
[this.formFieldNames.mileage]: [
|
[this.formFieldNames.mileage]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
||||||
<number | null>null,
|
[this.formFieldNames.amount]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
||||||
[Validators.required, Validators.min(1)],
|
|
||||||
],
|
|
||||||
[this.formFieldNames.amount]: [
|
|
||||||
<number | null>null,
|
|
||||||
[Validators.required, Validators.min(1)],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private readonly cars$: Observable<Car[]>;
|
||||||
protected readonly cars: Signal<Car[] | undefined>;
|
protected readonly cars: Signal<Car[] | undefined>;
|
||||||
|
|
||||||
|
private readonly isEntryDataLoaded = signal(false);
|
||||||
|
|
||||||
protected readonly isLoading = computed(() => {
|
protected readonly isLoading = computed(() => {
|
||||||
return this.cars() === undefined;
|
var cars = this.cars();
|
||||||
})
|
var isEntryDataLoaded = this.isEntryDataLoaded();
|
||||||
|
return cars === undefined || !isEntryDataLoaded;
|
||||||
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cars = toSignal(
|
this.cars$ = this.carClient
|
||||||
this.carClient
|
|
||||||
.getAll()
|
.getAll()
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
map(response => response.cars)
|
map(response => response.cars
|
||||||
),
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
|
tap(cars => {
|
||||||
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||||
|
|
||||||
|
if (selectedCarId === null) {
|
||||||
|
const firstCar = cars[0];
|
||||||
|
this.formGroup.controls[this.formFieldNames.car].setValue(firstCar);
|
||||||
|
this.selectedCarService.setSelectedCarId(firstCar?.id ?? null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCar = cars.find(car => car.id === selectedCarId);
|
||||||
|
this.formGroup.controls[this.formFieldNames.car].setValue(selectedCar ?? null);
|
||||||
|
this.selectedCarService.setSelectedCarId(selectedCar?.id ?? null);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
this.cars = toSignal(this.cars$);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadEntryDetailsAndEnableControls();
|
||||||
|
|
||||||
|
this.formGroup.controls[this.formFieldNames.car]
|
||||||
|
.valueChanges
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
tap((car) => {
|
||||||
|
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadEntryDetailsAndEnableControls() {
|
||||||
|
const entryId = this.id();
|
||||||
|
|
||||||
|
if (entryId === undefined || entryId === null) {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isEntryDataLoaded.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumption$ = this.consumptionClient
|
||||||
|
.getSingle(entryId);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
consumption$, this.cars$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
filter(([_, cars]) => cars !== undefined),
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleGetError(error)),
|
||||||
|
tap(([consumption, cars]) => {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
[this.formFieldNames.car]: cars!.find(c => c.id === consumption.carId) ?? null,
|
||||||
|
[this.formFieldNames.date]: new Date(consumption.dateTime),
|
||||||
|
[this.formFieldNames.mileage]: consumption.distance,
|
||||||
|
[this.formFieldNames.amount]: consumption.amount,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isEntryDataLoaded.set(true);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enableFormControls(): void {
|
||||||
|
for (const controlName of Object.values(this.formFieldNames)) {
|
||||||
|
const control = this.formGroup.get(controlName);
|
||||||
|
if (control) {
|
||||||
|
control.enable();
|
||||||
|
} else {
|
||||||
|
console.warn(`Form control '${controlName}' not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateToOverviewPage(): Promise<void> {
|
async navigateToOverviewPage(): Promise<void> {
|
||||||
await this.router.navigateByUrl(`/entries`);
|
await this.routingService.navigateToEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
// if (!this.entryId) {
|
if (this.formGroup.invalid) {
|
||||||
// this.createEntry();
|
this.formGroup.markAllAsTouched();
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// this.updateEntry();
|
var entryId = this.id();
|
||||||
|
if (entryId === undefined || entryId === null) {
|
||||||
|
this.createEntry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateEntry(entryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormData() {
|
||||||
|
var dateTime = new Date((this.formGroup.controls[this.formFieldNames.date].value ?? new Date).setHours(0, 0, 0, 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
carId: this.formGroup.controls[this.formFieldNames.car].value!.id,
|
||||||
|
dateTime: dateTime.toISOString(),
|
||||||
|
distance: this.formGroup.controls[this.formFieldNames.mileage].value!,
|
||||||
|
amount: this.formGroup.controls[this.formFieldNames.amount].value!,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createEntry() {
|
createEntry() {
|
||||||
// this.api
|
var request: CreateConsumptionEntry = this.getFormData();
|
||||||
// .createWeightEntry(
|
this.consumptionClient.create(request)
|
||||||
// this.getWeighedAt(),
|
.pipe(
|
||||||
// this.formGroup.controls['weight'].value,
|
takeUntilDestroyed(this.destroyRef),
|
||||||
// this.formGroup.controls['comment'].value,
|
catchError((error) => this.handleCreateOrUpdateError(error)),
|
||||||
// this.formGroup.controls['rabbit'].value!.id,
|
switchMap(() => this.routingService.navigateToEntries())
|
||||||
// this.formGroup.controls['medicines'].value?.map((x) => x.id) ?? [],
|
)
|
||||||
// )
|
.subscribe();
|
||||||
// .subscribe({
|
|
||||||
// next: (_) => {
|
|
||||||
// this.router.navigateByUrl('/weight-entries');
|
|
||||||
// },
|
|
||||||
// error: (error: HttpErrorResponse) => {
|
|
||||||
// switch (true) {
|
|
||||||
// case error.status >= 500 && error.status <= 599:
|
|
||||||
// this.messageService.add({
|
|
||||||
// severity: 'error',
|
|
||||||
// summary: 'Serverfehler',
|
|
||||||
// detail:
|
|
||||||
// 'Beim Erstellen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
|
||||||
// });
|
|
||||||
// break;
|
|
||||||
// case error.status == 400:
|
|
||||||
// this.messageService.add({
|
|
||||||
// severity: 'error',
|
|
||||||
// summary: 'Clientfehler',
|
|
||||||
// detail:
|
|
||||||
// 'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
|
||||||
// });
|
|
||||||
// break;
|
|
||||||
// default:
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEntry() {
|
updateEntry(id: string) {
|
||||||
// this.api
|
var request: UpdateConsumptionEntry = this.getFormData();
|
||||||
// .updateWeightEntry(
|
this.consumptionClient.update(id, request)
|
||||||
// this.entryId!,
|
.pipe(
|
||||||
// this.getWeighedAt(),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
// this.formGroup.controls['weight'].value,
|
catchError((error) => this.handleCreateOrUpdateError(error)),
|
||||||
// this.formGroup.controls['comment'].value,
|
switchMap(() => this.routingService.navigateToEntries())
|
||||||
// this.formGroup.controls['rabbit'].value!.id,
|
)
|
||||||
// this.formGroup.controls['medicines'].value?.map((x) => x.id) ?? [],
|
.subscribe();
|
||||||
// )
|
}
|
||||||
// .subscribe({
|
|
||||||
// next: (_) => {
|
private handleGetError(error: unknown): Observable<never> {
|
||||||
// this.router.navigateByUrl('/weight-entries');
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
// },
|
return throwError(() => error);
|
||||||
// error: (error: HttpErrorResponse) => {
|
}
|
||||||
// switch (true) {
|
|
||||||
// case error.status >= 500 && error.status <= 599:
|
switch (true) {
|
||||||
// this.messageService.add({
|
case error.status >= 500 && error.status <= 599:
|
||||||
// severity: 'error',
|
this.messageService.add({
|
||||||
// summary: 'Serverfehler',
|
severity: 'error',
|
||||||
// detail:
|
summary: 'Serverfehler',
|
||||||
// 'Beim Aktualisieren des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
detail:
|
||||||
// });
|
'Beim Erstellen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
// break;
|
});
|
||||||
// case error.status == 400:
|
break;
|
||||||
// this.messageService.add({
|
default:
|
||||||
// severity: 'error',
|
console.error(error);
|
||||||
// summary: 'Clientfehler',
|
this.messageService.add({
|
||||||
// detail:
|
severity: 'error',
|
||||||
// 'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
summary: 'Unerwarteter Fehler',
|
||||||
// });
|
detail:
|
||||||
// break;
|
'Beim Erstellen des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
// default:
|
});
|
||||||
// break;
|
break;
|
||||||
// }
|
}
|
||||||
// },
|
|
||||||
// });
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreateOrUpdateError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Erstellen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 400:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Clientfehler',
|
||||||
|
detail:
|
||||||
|
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Erstellen des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private dateTimeGreaterThanOrEqualToTodayValidator(control: FormControl<Date>): ValidationErrors | null {
|
||||||
|
const tomorrowStartOfDay = dayjs().add(1, 'day').startOf('day');
|
||||||
|
const controlDate = dayjs(control.value);
|
||||||
|
|
||||||
|
if (controlDate.isBefore(tomorrowStartOfDay)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dateTimeGreaterThanOrEqualToToday: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,13 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: EntriesComponent
|
component: EntriesComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
loadComponent: () => import('./edit-entry/edit-entry.component').then(m => m.EditEntryComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
loadComponent: () => import('./edit-entry/edit-entry.component').then(m => m.EditEntryComponent)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<p-confirmDialog></p-confirmDialog>
|
||||||
|
|
||||||
|
<div class="flex rounded-border shadow">
|
||||||
|
<div class="grow p-4 pos-relative edit-button" (click)="navigateToEdit()" role="button"
|
||||||
|
aria-roledescription="Bearbeite diesen Eintrag">
|
||||||
|
<div class="grid grid-cols-4 gap-4">
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matCalendarMonthSharp" />
|
||||||
|
<div>{{ entry().dateTime | date:"dd.MM.yyyy" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matStraightenSharp" />
|
||||||
|
<div>{{entry().distance }} km</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matLocalGasStationSharp" />
|
||||||
|
<div>{{entry().amount }} ℓ</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
@if (formattedLiterPer100Km(); as formattedLiterPer100Km) {
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matSpeedSharp" />
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{{ formattedLiterPer100Km }}
|
||||||
|
<app-fraction numerator="ℓ" [denominator]="'100km'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-red-500 text-white rounded-r text-center flex flex-col justify-center">
|
||||||
|
|
||||||
|
<button type="button" title="Löschen" class="reset cursor-pointer primary-color-text p-4 h-full rounded-r"
|
||||||
|
(click)="confirmDeleteEntry()">
|
||||||
|
<ng-icon name="matDeleteSharp"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.edit-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { DatePipe } from '@angular/common';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, computed, DestroyRef, inject, input, output } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
matCalendarMonthSharp,
|
||||||
|
matDeleteSharp,
|
||||||
|
matLocalGasStationSharp,
|
||||||
|
matSpeedSharp,
|
||||||
|
matStraightenSharp,
|
||||||
|
} from '@ng-icons/material-icons/sharp';
|
||||||
|
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
||||||
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { CardModule } from 'primeng/card';
|
||||||
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
|
import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
|
||||||
|
import { FractionComponent } from "../fraction/fraction.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-entry-card',
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CardModule,
|
||||||
|
ConfirmDialogModule,
|
||||||
|
DatePipe,
|
||||||
|
NgIconComponent,
|
||||||
|
FractionComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
matDeleteSharp,
|
||||||
|
matCalendarMonthSharp,
|
||||||
|
matSpeedSharp,
|
||||||
|
matStraightenSharp,
|
||||||
|
matLocalGasStationSharp,
|
||||||
|
}),
|
||||||
|
ConfirmationService,
|
||||||
|
],
|
||||||
|
templateUrl: './entry-card.component.html',
|
||||||
|
styleUrl: './entry-card.component.scss'
|
||||||
|
})
|
||||||
|
export class EntryCardComponent {
|
||||||
|
readonly entry = input.required<GetConsumptionEntriesEntry>();
|
||||||
|
|
||||||
|
protected readonly formattedLiterPer100Km = computed(() => {
|
||||||
|
const entry = this.entry();
|
||||||
|
|
||||||
|
const formatted = entry.literPer100Km
|
||||||
|
?.toFixed(2)
|
||||||
|
.replace('.', ',');
|
||||||
|
return formatted;
|
||||||
|
})
|
||||||
|
|
||||||
|
readonly entryDeleted = output<GetConsumptionEntriesEntry>();
|
||||||
|
|
||||||
|
private readonly routingService = inject(RoutingService);
|
||||||
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly confirmationService = inject(ConfirmationService);
|
||||||
|
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
async navigateToEdit(): Promise<void> {
|
||||||
|
await this.routingService.navigateToEditEntry(this.entry().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteEntry(): void {
|
||||||
|
const weighedAt = new Date(
|
||||||
|
Date.parse(this.entry().dateTime),
|
||||||
|
).toLocaleString('de-DE', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.confirmationService.confirm({
|
||||||
|
closeOnEscape: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
header: 'Bist du sicher?',
|
||||||
|
message: `Möchtest du diesen Eintrag (${weighedAt} für ${this.entry().car.name}) wirklich löschen?`,
|
||||||
|
acceptButtonProps: {
|
||||||
|
label: 'Löschen',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
rejectButtonProps: {
|
||||||
|
label: 'Abbrechen',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
accept: () => this.deleteEntry(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEntry(): void {
|
||||||
|
this.consumptionClient.delete(this.entry().id)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
tap(() => this.entryDeleted.emit(this.entry())),
|
||||||
|
catchError((error) => this.handleError(error)),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Löschen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 400:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Clientfehler',
|
||||||
|
detail:
|
||||||
|
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Löschen des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="flex flex-col items-center text-half-size">
|
||||||
|
<span>
|
||||||
|
{{ numerator() }}
|
||||||
|
</span>
|
||||||
|
<span class="separator"></span>
|
||||||
|
<span>
|
||||||
|
{{ denominator() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.separator {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-half-size {
|
||||||
|
// Specifically use em here to allow the parent to control the font size
|
||||||
|
// The font size here should be smaller because it is a fraction and would
|
||||||
|
// otherwise look too large
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-fraction',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './fraction.component.html',
|
||||||
|
styleUrl: './fraction.component.scss'
|
||||||
|
})
|
||||||
|
export class FractionComponent {
|
||||||
|
readonly numerator = input.required<number | string>();
|
||||||
|
readonly denominator = input.required<number | string>();
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
<p-scrollTop />
|
<p-scrollTop />
|
||||||
<div class="mb-4 flex gap-2 md:justify-between">
|
<div class="mb-4 flex gap-2 md:justify-between">
|
||||||
<div class="basis-full lg:basis-1/4 md:basis-1/2 p-0">
|
<div class="basis-full lg:basis-1/4 md:basis-1/2 p-0">
|
||||||
<!-- <p-select styleClass="w-full" [formControl]="selectedRabbit" placeholder="Kaninchen" [showClear]="true"
|
<p-select styleClass="w-full" [formControl]="selectedCar" placeholder="Auto" [showClear]="false"
|
||||||
[options]="(rabbits$ | async)!" optionLabel="name" /> -->
|
[options]="(cars$ | async)!" optionLabel="name" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p-button label="Erstellen" routerLink="/weight-entries/create">
|
<p-button label="Erstellen" routerLink="/entries/create">
|
||||||
<!-- <ng-icon name="matAddSharp"></ng-icon> -->
|
<ng-icon name="matAddSharp"></ng-icon>
|
||||||
</p-button>
|
</p-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,9 +26,8 @@
|
|||||||
<ng-template #list let-entries>
|
<ng-template #list let-entries>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@for (entry of entries; track entry.id) {
|
@for (entry of entries; track entry.id) {
|
||||||
{{ entry | json }}
|
<app-entry-card [entry]="entry"
|
||||||
<!-- <app-weight-entry-card [weightEntry]="weightEntry"
|
(entryDeleted)="onEntryDeleted($event)" />
|
||||||
(entryDeleted)="onEntryDeleted($event)"></app-weight-entry-card> -->
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
import { AsyncPipe, CommonModule } from '@angular/common';
|
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||||
import { Component, inject } from '@angular/core';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
matAddSharp,
|
||||||
|
} from '@ng-icons/material-icons/sharp';
|
||||||
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DataViewModule } from 'primeng/dataview';
|
import { DataViewModule } from 'primeng/dataview';
|
||||||
import { ScrollTopModule } from 'primeng/scrolltop';
|
import { ScrollTopModule } from 'primeng/scrolltop';
|
||||||
import { SelectModule } from 'primeng/select';
|
import { SelectModule } from 'primeng/select';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
import {
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
catchError,
|
||||||
|
combineLatest,
|
||||||
|
EMPTY,
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
tap
|
startWith,
|
||||||
|
tap,
|
||||||
|
throwError
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
import { SelectedCarService } from '../services/selected-car.service';
|
||||||
|
import { EntryCardComponent } from './components/entry-card/entry-card.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-entries',
|
selector: 'app-entries',
|
||||||
@@ -23,41 +37,135 @@ import {
|
|||||||
ButtonModule,
|
ButtonModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
DataViewModule,
|
DataViewModule,
|
||||||
SkeletonModule,
|
EntryCardComponent,
|
||||||
SelectModule,
|
NgIconComponent,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
ScrollTopModule,
|
ScrollTopModule,
|
||||||
|
SelectModule,
|
||||||
|
SkeletonModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
matAddSharp,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
templateUrl: './entries.component.html',
|
templateUrl: './entries.component.html',
|
||||||
styleUrl: './entries.component.scss'
|
styleUrl: './entries.component.scss'
|
||||||
})
|
})
|
||||||
export class EntriesComponent {
|
export class EntriesComponent implements OnInit {
|
||||||
private readonly consumptionClient = inject(ConsumptionClient);
|
|
||||||
private readonly carClient = inject(CarClient);
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly selectedCarService = inject(SelectedCarService);
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
protected readonly consumptionEntries$: Observable<ConsumptionEntry[]>;
|
protected readonly consumptionEntries$: Observable<GetConsumptionEntriesEntry[]>;
|
||||||
protected readonly cars$: Observable<Car[]>;
|
protected readonly cars$: Observable<Car[]>;
|
||||||
|
|
||||||
protected readonly skeletonsIterationSource = Array(10).fill(0);
|
protected readonly skeletonsIterationSource = Array(10).fill(0);
|
||||||
|
|
||||||
|
protected readonly selectedCar = new FormControl<Car | null | undefined>(null);
|
||||||
|
|
||||||
|
private readonly deletedEntries$ = new BehaviorSubject(<string[]>[]);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.consumptionEntries$ = this.consumptionClient.getAll()
|
const entries = this.consumptionClient.getAll()
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
tap((response) => {
|
map(response => response.consumptions.sort((a, b) => b.dateTime.localeCompare(a.dateTime))),
|
||||||
console.log('Entries response:', response);
|
catchError((error) => this.handleGetEntriesError(error))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.consumptionEntries$ = combineLatest([
|
||||||
|
entries,
|
||||||
|
this.selectedCar.valueChanges.pipe(startWith(null)),
|
||||||
|
this.deletedEntries$,
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
map(([entries, selectedCar, deletedEntries]) => {
|
||||||
|
const nonDeletedEntries =
|
||||||
|
deletedEntries.length === 0
|
||||||
|
? entries
|
||||||
|
: entries.filter(entry => !deletedEntries.includes(entry.id));
|
||||||
|
|
||||||
|
if (!selectedCar) {
|
||||||
|
return nonDeletedEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonDeletedEntries.filter(entry => entry.car.id === selectedCar.id);
|
||||||
}),
|
}),
|
||||||
map(response => response.consumptions)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cars$ = this.carClient.getAll()
|
this.cars$ = this.carClient.getAll()
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
tap((response) => {
|
map(response => response.cars),
|
||||||
console.log('Cars response:', response);
|
map((cars) => cars
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
|
tap((cars) => {
|
||||||
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||||
|
|
||||||
|
if (selectedCarId === null) {
|
||||||
|
const firstCar = cars[0];
|
||||||
|
this.selectedCar.setValue(firstCar);
|
||||||
|
this.selectedCarService.setSelectedCarId(firstCar?.id ?? null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCar = cars.find(car => car.id === selectedCarId);
|
||||||
|
this.selectedCar.setValue(selectedCar ?? null);
|
||||||
|
this.selectedCarService.setSelectedCarId(selectedCar?.id ?? null);
|
||||||
}),
|
}),
|
||||||
map(response => response.cars)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.selectedCar.valueChanges
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
tap((car) => {
|
||||||
|
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntryDeleted(entry: GetConsumptionEntriesEntry): void {
|
||||||
|
this.deletedEntries$.next([...this.deletedEntries$.value, entry.id]);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Eintrag gelöscht',
|
||||||
|
detail: 'Der Eintrag wurde erfolgreich gelöscht.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetEntriesError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => new Error('An unexpected error occurred'));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { BehaviorSubject, tap } from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class SelectedCarService {
|
||||||
|
static readonly SELECTED_CAR_ID_KEY = "SELECTED_CAR_ID";
|
||||||
|
|
||||||
|
private selectedCarId: string | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadStoredCarId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadStoredCarId(): void {
|
||||||
|
this.selectedCarId = localStorage.getItem(SelectedCarService.SELECTED_CAR_ID_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedCarId() {
|
||||||
|
return this.selectedCarId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedCarId(carId: string | null): void {
|
||||||
|
this.selectedCarId = carId;
|
||||||
|
if (carId === null) {
|
||||||
|
localStorage.removeItem(SelectedCarService.SELECTED_CAR_ID_KEY);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(SelectedCarService.SELECTED_CAR_ID_KEY, carId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/Vegasco-Web/src/app/services/routing.service.ts
Normal file
33
src/Vegasco-Web/src/app/services/routing.service.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RoutingService {
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
async navigateToEntries(): Promise<void> {
|
||||||
|
await this.router.navigateByUrl('/entries');
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToEditEntry(entryId: string): Promise<void> {
|
||||||
|
await this.router.navigate(['entries', 'edit', entryId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToCreateEntry(): Promise<void> {
|
||||||
|
await this.router.navigate(['entries', 'create']);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToCars(): Promise<void> {
|
||||||
|
await this.router.navigateByUrl('/cars');
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToEditCar(entryId: string): Promise<void> {
|
||||||
|
await this.router.navigate(['cars', 'edit', entryId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToCreateCar(): Promise<void> {
|
||||||
|
await this.router.navigate(['cars', 'create']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,39 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
@use "tailwindcss";
|
||||||
|
@plugin "tailwindcss-primeui";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-content-width {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trbl-0 {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-color-text {
|
||||||
|
color: var(--primary-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|||||||
12
src/Vegasco-Web/webserver.conf.template
Normal file
12
src/Vegasco-Web/webserver.conf.template
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location ~ ^/api/(.*) {
|
||||||
|
proxy_pass ${apiUrl}/${DOLLAR}1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files ${DOLLAR}uri ${DOLLAR}uri/ /index.html =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Vegasco.Server.Api.Authentication;
|
using Vegasco.Server.Api.Authentication;
|
||||||
using Vegasco.Server.Api.Common;
|
using Vegasco.Server.Api.Common;
|
||||||
using Vegasco.Server.Api.Persistence;
|
using Vegasco.Server.Api.Persistence;
|
||||||
@@ -10,6 +11,7 @@ namespace Vegasco.Server.Api.Cars;
|
|||||||
public static class CreateCar
|
public static class CreateCar
|
||||||
{
|
{
|
||||||
public record Request(string Name);
|
public record Request(string Name);
|
||||||
|
|
||||||
public record Response(Guid Id, string Name);
|
public record Response(Guid Id, string Name);
|
||||||
|
|
||||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
@@ -19,7 +21,8 @@ public static class CreateCar
|
|||||||
.WithTags("Cars")
|
.WithTags("Cars")
|
||||||
.WithDescription("Creates a new car")
|
.WithDescription("Creates a new car")
|
||||||
.Produces<Response>(201)
|
.Produces<Response>(201)
|
||||||
.ProducesValidationProblem();
|
.ProducesValidationProblem()
|
||||||
|
.Produces(409);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Validator : AbstractValidator<Request>
|
public class Validator : AbstractValidator<Request>
|
||||||
@@ -32,40 +35,61 @@ public static class CreateCar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
Request request,
|
Request request,
|
||||||
IEnumerable<IValidator<Request>> validators,
|
IEnumerable<IValidator<Request>> validators,
|
||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
UserAccessor userAccessor,
|
UserAccessor userAccessor,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
|
ILogger logger = loggerFactory.CreateLogger(typeof(CreateCar));
|
||||||
|
|
||||||
|
List<ValidationResult> failedValidations =
|
||||||
|
await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
|
||||||
if (failedValidations.Count > 0)
|
if (failedValidations.Count > 0)
|
||||||
{
|
{
|
||||||
|
string[] errors = failedValidations
|
||||||
|
.Where(x => !x.IsValid)
|
||||||
|
.SelectMany(x => x.Errors)
|
||||||
|
.Select(x => x.ErrorMessage)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
logger.LogDebug(
|
||||||
|
"Validation failed for request {@Request} with errors {@Errors}",
|
||||||
|
request,
|
||||||
|
errors);
|
||||||
|
|
||||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isDuplicate = await dbContext.Cars
|
||||||
|
.AnyAsync(x => x.Name.ToUpper() == request.Name.ToUpper(), cancellationToken);
|
||||||
|
|
||||||
|
if (isDuplicate)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Car with name '{CarName}' (case insensitive) already exists", request.Name);
|
||||||
|
return TypedResults.Conflict();
|
||||||
|
}
|
||||||
|
|
||||||
string userId = userAccessor.GetUserId();
|
string userId = userAccessor.GetUserId();
|
||||||
|
|
||||||
User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken);
|
User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken);
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
user = new User
|
logger.LogDebug("User with ID '{UserId}' not found, creating new user", userId);
|
||||||
{
|
|
||||||
Id = userId
|
user = new User { Id = userId };
|
||||||
};
|
|
||||||
await dbContext.Users.AddAsync(user, cancellationToken);
|
await dbContext.Users.AddAsync(user, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Car car = new()
|
Car car = new() { Name = request.Name.Trim(), UserId = userId };
|
||||||
{
|
|
||||||
Name = request.Name,
|
|
||||||
UserId = userId
|
|
||||||
};
|
|
||||||
|
|
||||||
await dbContext.Cars.AddAsync(car, cancellationToken);
|
await dbContext.Cars.AddAsync(car, cancellationToken);
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogTrace("Created new car: {@Car}", car);
|
||||||
|
|
||||||
Response response = new(car.Id.Value, car.Name);
|
Response response = new(car.Id.Value, car.Name);
|
||||||
return TypedResults.Created($"/v1/cars/{car.Id}", response);
|
return TypedResults.Created($"/v1/cars/{car.Id}", response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Vegasco.Server.Api.Persistence;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Cars;
|
namespace Vegasco.Server.Api.Cars;
|
||||||
|
|
||||||
@@ -14,20 +16,29 @@ public static class DeleteCar
|
|||||||
.Produces(404);
|
.Produces(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
Guid id,
|
Guid id,
|
||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Car? car = await dbContext.Cars.FindAsync([new CarId(id)], cancellationToken: cancellationToken);
|
Activity? activity = Activity.Current;
|
||||||
|
activity?.SetTag("id", id);
|
||||||
|
|
||||||
if (car is null)
|
int rows = await dbContext.Cars
|
||||||
|
.Where(x => x.Id == new CarId(id))
|
||||||
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (rows == 0)
|
||||||
{
|
{
|
||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.Cars.Remove(car);
|
if (rows > 1)
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
{
|
||||||
|
ILogger logger = loggerFactory.CreateLogger(typeof(DeleteCar));
|
||||||
|
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{CarId}'", rows, id);
|
||||||
|
}
|
||||||
|
|
||||||
return TypedResults.NoContent();
|
return TypedResults.NoContent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public static class GetCar
|
|||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = new Response(car.Id.Value, car.Name);
|
Response response = new Response(car.Id.Value, car.Name);
|
||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Diagnostics;
|
||||||
using Vegasco.Server.Api.Persistence;
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Cars;
|
namespace Vegasco.Server.Api.Cars;
|
||||||
@@ -34,11 +35,15 @@ public static class GetCars
|
|||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Activity? activity = Activity.Current;
|
||||||
|
|
||||||
List<ResponseDto> cars = await dbContext.Cars
|
List<ResponseDto> cars = await dbContext.Cars
|
||||||
.Select(x => new ResponseDto(x.Id.Value, x.Name))
|
.Select(x => new ResponseDto(x.Id.Value, x.Name))
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var response = new ApiResponse
|
activity?.SetTag("carCount", cars.Count);
|
||||||
|
|
||||||
|
ApiResponse response = new()
|
||||||
{
|
{
|
||||||
Cars = cars
|
Cars = cars
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Vegasco.Server.Api.Authentication;
|
using Vegasco.Server.Api.Authentication;
|
||||||
using Vegasco.Server.Api.Common;
|
using Vegasco.Server.Api.Common;
|
||||||
using Vegasco.Server.Api.Persistence;
|
using Vegasco.Server.Api.Persistence;
|
||||||
@@ -9,6 +10,7 @@ namespace Vegasco.Server.Api.Cars;
|
|||||||
public static class UpdateCar
|
public static class UpdateCar
|
||||||
{
|
{
|
||||||
public record Request(string Name);
|
public record Request(string Name);
|
||||||
|
|
||||||
public record Response(Guid Id, string Name);
|
public record Response(Guid Id, string Name);
|
||||||
|
|
||||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
@@ -19,7 +21,8 @@ public static class UpdateCar
|
|||||||
.WithDescription("Updates a car by ID")
|
.WithDescription("Updates a car by ID")
|
||||||
.Produces<Response>()
|
.Produces<Response>()
|
||||||
.ProducesValidationProblem()
|
.ProducesValidationProblem()
|
||||||
.Produces(404);
|
.Produces(404)
|
||||||
|
.Produces(409);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Validator : AbstractValidator<Request>
|
public class Validator : AbstractValidator<Request>
|
||||||
@@ -32,17 +35,31 @@ public static class UpdateCar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
Guid id,
|
Guid id,
|
||||||
Request request,
|
Request request,
|
||||||
IEnumerable<IValidator<Request>> validators,
|
IEnumerable<IValidator<Request>> validators,
|
||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
UserAccessor userAccessor,
|
UserAccessor userAccessor,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
ILogger logger = loggerFactory.CreateLogger(typeof(UpdateCar));
|
||||||
|
|
||||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||||
if (failedValidations.Count > 0)
|
if (failedValidations.Count > 0)
|
||||||
{
|
{
|
||||||
|
string[] errors = failedValidations
|
||||||
|
.Where(x => !x.IsValid)
|
||||||
|
.SelectMany(x => x.Errors)
|
||||||
|
.Select(x => x.ErrorMessage)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
logger.LogDebug(
|
||||||
|
"Validation failed for request {@Request} with errors {@Errors}",
|
||||||
|
request,
|
||||||
|
errors);
|
||||||
|
|
||||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +70,20 @@ public static class UpdateCar
|
|||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
car.Name = request.Name;
|
bool isDuplicate = await dbContext.Cars
|
||||||
|
.AnyAsync(x => x.Name.ToUpper() == request.Name.ToUpper(), cancellationToken);
|
||||||
|
|
||||||
|
if (isDuplicate)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Car with name '{CarName}' (case insensitive) already exists", request.Name);
|
||||||
|
return TypedResults.Conflict();
|
||||||
|
}
|
||||||
|
|
||||||
|
car.Name = request.Name.Trim();
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogTrace("Updated car: {@Car}", car);
|
||||||
|
|
||||||
Response response = new(car.Id.Value, car.Name);
|
Response response = new(car.Id.Value, car.Name);
|
||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ public static class DependencyInjectionExtensions
|
|||||||
/// <param name="builder"></param>
|
/// <param name="builder"></param>
|
||||||
public static void AddApiServices(this IHostApplicationBuilder builder)
|
public static void AddApiServices(this IHostApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
|
builder.AddBuilderServices();
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddMiscellaneousServices()
|
.AddMiscellaneousServices()
|
||||||
.AddCustomOpenApi()
|
.AddCustomOpenApi()
|
||||||
@@ -27,6 +29,24 @@ public static class DependencyInjectionExtensions
|
|||||||
builder.AddDbContext();
|
builder.AddDbContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IHostApplicationBuilder AddBuilderServices(this IHostApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
string? seqHost = builder.Configuration.GetConnectionString("seq");
|
||||||
|
if (!string.IsNullOrEmpty(seqHost))
|
||||||
|
{
|
||||||
|
builder.AddSeqEndpoint("seq", o =>
|
||||||
|
{
|
||||||
|
var apiKey = builder.Configuration.GetValue<string>("seq-api-key");
|
||||||
|
if (!string.IsNullOrEmpty(apiKey))
|
||||||
|
{
|
||||||
|
o.ApiKey = apiKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
private static IServiceCollection AddMiscellaneousServices(this IServiceCollection services)
|
private static IServiceCollection AddMiscellaneousServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton(() =>
|
services.AddSingleton(() =>
|
||||||
@@ -121,7 +141,7 @@ public static class DependencyInjectionExtensions
|
|||||||
.ValidateFluently()
|
.ValidateFluently()
|
||||||
.ValidateOnStart();
|
.ValidateOnStart();
|
||||||
|
|
||||||
var jwtOptions = services.BuildServiceProvider().GetRequiredService<IOptions<JwtOptions>>();
|
IOptions<JwtOptions> jwtOptions = services.BuildServiceProvider().GetRequiredService<IOptions<JwtOptions>>();
|
||||||
|
|
||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
|
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ public class Consumption
|
|||||||
|
|
||||||
public double Amount { get; set; }
|
public double Amount { get; set; }
|
||||||
|
|
||||||
public bool IgnoreInCalculation { get; set; }
|
|
||||||
|
|
||||||
public CarId CarId { get; set; }
|
public CarId CarId { get; set; }
|
||||||
|
|
||||||
public virtual Car Car { get; set; } = null!;
|
public virtual Car Car { get; set; } = null!;
|
||||||
@@ -39,9 +37,6 @@ public class ConsumptionTableConfiguration : IEntityTypeConfiguration<Consumptio
|
|||||||
builder.Property(x => x.Amount)
|
builder.Property(x => x.Amount)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
builder.Property(x => x.IgnoreInCalculation)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
builder.Property(x => x.CarId)
|
builder.Property(x => x.CarId)
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasConversion<CarId.EfCoreValueConverter>();
|
.HasConversion<CarId.EfCoreValueConverter>();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
|
using System.Diagnostics;
|
||||||
using Vegasco.Server.Api.Cars;
|
using Vegasco.Server.Api.Cars;
|
||||||
using Vegasco.Server.Api.Common;
|
using Vegasco.Server.Api.Common;
|
||||||
using Vegasco.Server.Api.Persistence;
|
using Vegasco.Server.Api.Persistence;
|
||||||
@@ -8,9 +9,9 @@ namespace Vegasco.Server.Api.Consumptions;
|
|||||||
|
|
||||||
public static class CreateConsumption
|
public static class CreateConsumption
|
||||||
{
|
{
|
||||||
public record Request(DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation, Guid CarId);
|
public record Request(DateTimeOffset DateTime, double Distance, double Amount, Guid CarId);
|
||||||
|
|
||||||
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation, Guid CarId);
|
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, Guid CarId);
|
||||||
|
|
||||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -25,8 +26,13 @@ public static class CreateConsumption
|
|||||||
{
|
{
|
||||||
public Validator(TimeProvider timeProvider)
|
public Validator(TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
|
Func<DateTimeOffset> getTodayEndOfDay = () => timeProvider.GetUtcNow()
|
||||||
|
.Date
|
||||||
|
.AddDays(1)
|
||||||
|
.AddTicks(-1);
|
||||||
|
|
||||||
RuleFor(x => x.DateTime.ToUniversalTime())
|
RuleFor(x => x.DateTime.ToUniversalTime())
|
||||||
.LessThanOrEqualTo(timeProvider.GetUtcNow())
|
.LessThanOrEqualTo(_ => getTodayEndOfDay())
|
||||||
.WithName(nameof(Request.DateTime));
|
.WithName(nameof(Request.DateTime));
|
||||||
|
|
||||||
RuleFor(x => x.Distance)
|
RuleFor(x => x.Distance)
|
||||||
@@ -44,11 +50,25 @@ public static class CreateConsumption
|
|||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
Request request,
|
Request request,
|
||||||
IEnumerable<IValidator<Request>> validators,
|
IEnumerable<IValidator<Request>> validators,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
ILogger logger = loggerFactory.CreateLogger(typeof(CreateConsumption));
|
||||||
|
|
||||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||||
if (failedValidations.Count > 0)
|
if (failedValidations.Count > 0)
|
||||||
{
|
{
|
||||||
|
string[] errors = failedValidations
|
||||||
|
.Where(x => !x.IsValid)
|
||||||
|
.SelectMany(x => x.Errors)
|
||||||
|
.Select(x => x.ErrorMessage)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
logger.LogDebug(
|
||||||
|
"Validation failed for request {@Request} with errors {@Errors}",
|
||||||
|
request,
|
||||||
|
errors);
|
||||||
|
|
||||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,19 +78,21 @@ public static class CreateConsumption
|
|||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var consumption = new Consumption
|
Consumption consumption = new()
|
||||||
{
|
{
|
||||||
DateTime = request.DateTime.ToUniversalTime(),
|
DateTime = request.DateTime.ToUniversalTime(),
|
||||||
Distance = request.Distance,
|
Distance = request.Distance,
|
||||||
Amount = request.Amount,
|
Amount = request.Amount,
|
||||||
IgnoreInCalculation = request.IgnoreInCalculation,
|
|
||||||
CarId = new CarId(request.CarId)
|
CarId = new CarId(request.CarId)
|
||||||
};
|
};
|
||||||
|
|
||||||
dbContext.Consumptions.Add(consumption);
|
dbContext.Consumptions.Add(consumption);
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogTrace("Created new consumption: {@Consumption}", consumption);
|
||||||
|
|
||||||
return TypedResults.Created($"consumptions/{consumption.Id.Value}",
|
return TypedResults.Created($"consumptions/{consumption.Id.Value}",
|
||||||
new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, consumption.IgnoreInCalculation, consumption.CarId.Value));
|
new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount,
|
||||||
|
consumption.CarId.Value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using Vegasco.Server.Api.Persistence;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Consumptions;
|
namespace Vegasco.Server.Api.Consumptions;
|
||||||
|
|
||||||
@@ -15,18 +17,28 @@ public static class DeleteConsumption
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
ApplicationDbContext dbContext,
|
|
||||||
Guid id,
|
Guid id,
|
||||||
|
ApplicationDbContext dbContext,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Consumption? consumption = await dbContext.Consumptions.FindAsync([new ConsumptionId(id)], cancellationToken);
|
Activity? activity = Activity.Current;
|
||||||
if (consumption is null)
|
activity?.SetTag("id", id);
|
||||||
|
|
||||||
|
int rows = await dbContext.Consumptions
|
||||||
|
.Where(x => x.Id == new ConsumptionId(id))
|
||||||
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (rows == 0)
|
||||||
{
|
{
|
||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.Consumptions.Remove(consumption);
|
if (rows > 1)
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
{
|
||||||
|
ILogger logger = loggerFactory.CreateLogger(typeof(DeleteConsumption));
|
||||||
|
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{ConsumptionId}'", rows, id);
|
||||||
|
}
|
||||||
|
|
||||||
return TypedResults.NoContent();
|
return TypedResults.NoContent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace Vegasco.Server.Api.Consumptions;
|
|||||||
|
|
||||||
public static class GetConsumption
|
public static class GetConsumption
|
||||||
{
|
{
|
||||||
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation, Guid CarId);
|
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, Guid CarId);
|
||||||
|
|
||||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -28,8 +28,12 @@ public static class GetConsumption
|
|||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance,
|
Response response = new(
|
||||||
consumption.Amount, consumption.IgnoreInCalculation, consumption.CarId.Value);
|
consumption.Id.Value,
|
||||||
|
consumption.DateTime,
|
||||||
|
consumption.Distance,
|
||||||
|
consumption.Amount,
|
||||||
|
consumption.CarId.Value);
|
||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Vegasco.Server.Api.Cars;
|
||||||
using Vegasco.Server.Api.Persistence;
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Consumptions;
|
namespace Vegasco.Server.Api.Consumptions;
|
||||||
@@ -17,8 +19,18 @@ public static class GetConsumptions
|
|||||||
DateTimeOffset DateTime,
|
DateTimeOffset DateTime,
|
||||||
double Distance,
|
double Distance,
|
||||||
double Amount,
|
double Amount,
|
||||||
bool IgnoreInCalculation,
|
CarDto Car,
|
||||||
Guid CarId);
|
double? LiterPer100Km);
|
||||||
|
|
||||||
|
public record CarDto(
|
||||||
|
Guid Id,
|
||||||
|
string Name)
|
||||||
|
{
|
||||||
|
public static CarDto FromCar(Car car)
|
||||||
|
{
|
||||||
|
return new CarDto(car.Id.Value, car.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class Request
|
public class Request
|
||||||
{
|
{
|
||||||
@@ -38,16 +50,52 @@ public static class GetConsumptions
|
|||||||
private static async Task<Ok<ApiResponse>> Endpoint(
|
private static async Task<Ok<ApiResponse>> Endpoint(
|
||||||
[AsParameters] Request request,
|
[AsParameters] Request request,
|
||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ResponseDto> consumptions = await dbContext.Consumptions
|
ILogger logger = loggerFactory.CreateLogger(typeof(GetConsumptions));
|
||||||
.Select(x =>
|
|
||||||
new ResponseDto(x.Id.Value, x.DateTime, x.Distance, x.Amount, x.IgnoreInCalculation, x.CarId.Value))
|
logger.LogTrace("Received request to get consumptions with parameters: {@Request}", request);
|
||||||
.ToListAsync(cancellationToken);
|
Activity? activity = Activity.Current;
|
||||||
|
|
||||||
|
Dictionary<CarId, List<Consumption>> consumptionsByCar = await dbContext.Consumptions
|
||||||
|
.Include(x => x.Car)
|
||||||
|
.GroupBy(x => x.CarId)
|
||||||
|
.ToDictionaryAsync(x => x.Key, x => x.OrderByDescending(x => x.DateTime).ToList(), cancellationToken);
|
||||||
|
|
||||||
|
List<ResponseDto> responses = [];
|
||||||
|
|
||||||
|
foreach (List<Consumption> consumptions in consumptionsByCar.Select(x => x.Value))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < consumptions.Count; i++)
|
||||||
|
{
|
||||||
|
Consumption consumption = consumptions[i];
|
||||||
|
|
||||||
|
double? literPer100Km = null;
|
||||||
|
|
||||||
|
bool isLast = i == consumptions.Count - 1;
|
||||||
|
if (!isLast)
|
||||||
|
{
|
||||||
|
Consumption previousConsumption = consumptions[i + 1];
|
||||||
|
double distanceDiff = consumption.Distance - previousConsumption.Distance;
|
||||||
|
literPer100Km = consumption.Amount / (distanceDiff / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.Add(new ResponseDto(
|
||||||
|
consumption.Id.Value,
|
||||||
|
consumption.DateTime,
|
||||||
|
consumption.Distance,
|
||||||
|
consumption.Amount,
|
||||||
|
CarDto.FromCar(consumption.Car),
|
||||||
|
literPer100Km));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activity?.SetTag("consumptionCount", responses.Count);
|
||||||
|
|
||||||
ApiResponse apiResponse = new()
|
ApiResponse apiResponse = new()
|
||||||
{
|
{
|
||||||
Consumptions = consumptions
|
Consumptions = responses
|
||||||
};
|
};
|
||||||
return TypedResults.Ok(apiResponse);
|
return TypedResults.Ok(apiResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ namespace Vegasco.Server.Api.Consumptions;
|
|||||||
|
|
||||||
public static class UpdateConsumption
|
public static class UpdateConsumption
|
||||||
{
|
{
|
||||||
public record Request(DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation);
|
public record Request(DateTimeOffset DateTime, double Distance, double Amount);
|
||||||
|
|
||||||
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, bool IgnoreInCalculation, Guid CarId);
|
public record Response(Guid Id, DateTimeOffset DateTime, double Distance, double Amount, Guid CarId);
|
||||||
|
|
||||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -26,8 +26,13 @@ public static class UpdateConsumption
|
|||||||
{
|
{
|
||||||
public Validator(TimeProvider timeProvider)
|
public Validator(TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
|
Func<DateTimeOffset> getTodayEndOfDay = () => timeProvider.GetUtcNow()
|
||||||
|
.Date
|
||||||
|
.AddDays(1)
|
||||||
|
.AddTicks(-1);
|
||||||
|
|
||||||
RuleFor(x => x.DateTime.ToUniversalTime())
|
RuleFor(x => x.DateTime.ToUniversalTime())
|
||||||
.LessThanOrEqualTo(timeProvider.GetUtcNow())
|
.LessThanOrEqualTo(_ => getTodayEndOfDay())
|
||||||
.WithName(nameof(Request.DateTime));
|
.WithName(nameof(Request.DateTime));
|
||||||
|
|
||||||
RuleFor(x => x.Distance)
|
RuleFor(x => x.Distance)
|
||||||
@@ -43,11 +48,25 @@ public static class UpdateConsumption
|
|||||||
Guid id,
|
Guid id,
|
||||||
Request request,
|
Request request,
|
||||||
IEnumerable<IValidator<Request>> validators,
|
IEnumerable<IValidator<Request>> validators,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
ILogger logger = loggerFactory.CreateLogger(typeof(UpdateConsumption));
|
||||||
|
|
||||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken);
|
||||||
if (failedValidations.Count > 0)
|
if (failedValidations.Count > 0)
|
||||||
{
|
{
|
||||||
|
string[] errors = failedValidations
|
||||||
|
.Where(x => !x.IsValid)
|
||||||
|
.SelectMany(x => x.Errors)
|
||||||
|
.Select(x => x.ErrorMessage)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
logger.LogDebug(
|
||||||
|
"Validation failed for request {@Request} with errors {@Errors}",
|
||||||
|
request,
|
||||||
|
errors);
|
||||||
|
|
||||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,10 +79,12 @@ public static class UpdateConsumption
|
|||||||
consumption.DateTime = request.DateTime.ToUniversalTime();
|
consumption.DateTime = request.DateTime.ToUniversalTime();
|
||||||
consumption.Distance = request.Distance;
|
consumption.Distance = request.Distance;
|
||||||
consumption.Amount = request.Amount;
|
consumption.Amount = request.Amount;
|
||||||
consumption.IgnoreInCalculation = request.IgnoreInCalculation;
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return TypedResults.Ok(new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, consumption.IgnoreInCalculation, consumption.CarId.Value));
|
logger.LogTrace("Updated consumption: {@Consumption}", consumption);
|
||||||
|
|
||||||
|
return TypedResults.Ok(new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance,
|
||||||
|
consumption.Amount, consumption.CarId.Value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,5 +41,6 @@ public static class EndpointExtensions
|
|||||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||||
|
|
||||||
GetServerInfo.MapEndpoint(versionedApis);
|
GetServerInfo.MapEndpoint(versionedApis);
|
||||||
|
GetCurrentTime.MapEndpoint(versionedApis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/Vegasco.Server.Api/Info/GetCurrentTime.cs
Normal file
21
src/Vegasco.Server.Api/Info/GetCurrentTime.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
|
||||||
|
namespace Vegasco.Server.Api.Info;
|
||||||
|
|
||||||
|
public static class GetCurrentTime
|
||||||
|
{
|
||||||
|
public record Response(DateTimeOffset CurrentTime);
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
return builder
|
||||||
|
.MapGet("info/time", Endpoint)
|
||||||
|
.WithTags("Info");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Ok<Response> Endpoint(
|
||||||
|
TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
return TypedResults.Ok(new Response(timeProvider.GetUtcNow()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Vegasco.Server.Api.Info;
|
namespace Vegasco.Server.Api.Info;
|
||||||
|
|
||||||
public class GetServerInfo
|
public static class GetServerInfo
|
||||||
{
|
{
|
||||||
public record Response(
|
public record Response(
|
||||||
string FullVersion,
|
string FullVersion,
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ public class ApplyMigrationsService(
|
|||||||
{
|
{
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var activity = activitySource.StartActivity("ApplyMigrations");
|
using Activity? activity = activitySource.StartActivity("ApplyMigrations");
|
||||||
|
|
||||||
logger.LogInformation("Starting migrations");
|
logger.LogInformation("Starting migrations");
|
||||||
|
|
||||||
using IServiceScope scope = scopeFactory.CreateScope();
|
using IServiceScope scope = scopeFactory.CreateScope();
|
||||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
await using ApplicationDbContext dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
117
src/Vegasco.Server.Api/Persistence/Migrations/20250622085121_DropIgnoreInCalculation.Designer.cs
generated
Normal file
117
src/Vegasco.Server.Api/Persistence/Migrations/20250622085121_DropIgnoreInCalculation.Designer.cs
generated
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Vegasco.Server.Api.Persistence.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20250622085121_DropIgnoreInCalculation")]
|
||||||
|
partial class DropIgnoreInCalculation
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Cars");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.Server.Api.Consumptions.Consumption", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<double>("Amount")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<Guid>("CarId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("DateTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<double>("Distance")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CarId");
|
||||||
|
|
||||||
|
b.ToTable("Consumptions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.Server.Api.Users.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Vegasco.Server.Api.Users.User", "User")
|
||||||
|
.WithMany("Cars")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.Server.Api.Consumptions.Consumption", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Vegasco.Server.Api.Cars.Car", "Car")
|
||||||
|
.WithMany("Consumptions")
|
||||||
|
.HasForeignKey("CarId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Car");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.Server.Api.Cars.Car", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Consumptions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.Server.Api.Users.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Cars");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Vegasco.Server.Api.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DropIgnoreInCalculation : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IgnoreInCalculation",
|
||||||
|
table: "Consumptions");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IgnoreInCalculation",
|
||||||
|
table: "Consumptions",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
using Vegasco.Server.Api.Persistence;
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Persistence.Migrations
|
namespace Vegasco.Server.Api.Persistence.Migrations
|
||||||
@@ -18,7 +17,7 @@ namespace Vegasco.Server.Api.Persistence.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "8.0.8")
|
.HasAnnotation("ProductVersion", "9.0.5")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -61,9 +60,6 @@ namespace Vegasco.Server.Api.Persistence.Migrations
|
|||||||
b.Property<double>("Distance")
|
b.Property<double>("Distance")
|
||||||
.HasColumnType("double precision");
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
b.Property<bool>("IgnoreInCalculation")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("CarId");
|
b.HasIndex("CarId");
|
||||||
|
|||||||
@@ -13,23 +13,24 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
|
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
|
||||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||||
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.3.0" />
|
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.5.1" />
|
||||||
|
<PackageReference Include="Aspire.Seq" Version="9.5.1" />
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="OpenTelemetry" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry" Version="1.13.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.4.16" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.9.0" />
|
||||||
<PackageReference Include="StronglyTypedId" Version="1.0.0-beta08" PrivateAssets="all" ExcludeAssets="runtime" />
|
<PackageReference Include="StronglyTypedId" Version="1.0.0-beta08" PrivateAssets="all" ExcludeAssets="runtime" />
|
||||||
<PackageReference Include="StronglyTypedId.Templates" Version="1.0.0-beta08" />
|
<PackageReference Include="StronglyTypedId.Templates" Version="1.0.0-beta08" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.7.115" />
|
<PackageReference Update="Nerdbank.GitVersioning" Version="3.8.118" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
85
src/Vegasco.Server.Api/migrations/migration.sql
Normal file
85
src/Vegasco.Server.Api/migrations/migration.sql
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||||
|
"MigrationId" character varying(150) NOT NULL,
|
||||||
|
"ProductVersion" character varying(32) NOT NULL,
|
||||||
|
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
||||||
|
);
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
DO $EF$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240818105918_Initial') THEN
|
||||||
|
CREATE TABLE "Users" (
|
||||||
|
"Id" text NOT NULL,
|
||||||
|
CONSTRAINT "PK_Users" PRIMARY KEY ("Id")
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END $EF$;
|
||||||
|
|
||||||
|
DO $EF$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240818105918_Initial') THEN
|
||||||
|
CREATE TABLE "Cars" (
|
||||||
|
"Id" uuid NOT NULL,
|
||||||
|
"Name" character varying(50) NOT NULL,
|
||||||
|
"UserId" text NOT NULL,
|
||||||
|
CONSTRAINT "PK_Cars" PRIMARY KEY ("Id"),
|
||||||
|
CONSTRAINT "FK_Cars_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users" ("Id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END $EF$;
|
||||||
|
|
||||||
|
DO $EF$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240818105918_Initial') THEN
|
||||||
|
CREATE TABLE "Consumptions" (
|
||||||
|
"Id" uuid NOT NULL,
|
||||||
|
"DateTime" timestamp with time zone NOT NULL,
|
||||||
|
"Distance" double precision NOT NULL,
|
||||||
|
"Amount" double precision NOT NULL,
|
||||||
|
"IgnoreInCalculation" boolean NOT NULL,
|
||||||
|
"CarId" uuid NOT NULL,
|
||||||
|
CONSTRAINT "PK_Consumptions" PRIMARY KEY ("Id"),
|
||||||
|
CONSTRAINT "FK_Consumptions_Cars_CarId" FOREIGN KEY ("CarId") REFERENCES "Cars" ("Id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END $EF$;
|
||||||
|
|
||||||
|
DO $EF$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240818105918_Initial') THEN
|
||||||
|
CREATE INDEX "IX_Cars_UserId" ON "Cars" ("UserId");
|
||||||
|
END IF;
|
||||||
|
END $EF$;
|
||||||
|
|
||||||
|
DO $EF$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240818105918_Initial') THEN
|
||||||
|
CREATE INDEX "IX_Consumptions_CarId" ON "Consumptions" ("CarId");
|
||||||
|
END IF;
|
||||||
|
END $EF$;
|
||||||
|
|
||||||
|
DO $EF$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240818105918_Initial') THEN
|
||||||
|
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||||
|
VALUES ('20240818105918_Initial', '9.0.5');
|
||||||
|
END IF;
|
||||||
|
END $EF$;
|
||||||
|
|
||||||
|
DO $EF$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20250622085121_DropIgnoreInCalculation') THEN
|
||||||
|
ALTER TABLE "Consumptions" DROP COLUMN "IgnoreInCalculation";
|
||||||
|
END IF;
|
||||||
|
END $EF$;
|
||||||
|
|
||||||
|
DO $EF$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20250622085121_DropIgnoreInCalculation') THEN
|
||||||
|
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||||
|
VALUES ('20250622085121_DropIgnoreInCalculation', '9.0.5');
|
||||||
|
END IF;
|
||||||
|
END $EF$;
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ public static class Constants
|
|||||||
{
|
{
|
||||||
public static class Projects
|
public static class Projects
|
||||||
{
|
{
|
||||||
public const string Api = "Vegasco-Server-Api";
|
public const string Api = "Api";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Database
|
public static class Database
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Nerdbank.GitVersioning">
|
<PackageReference Update="Nerdbank.GitVersioning">
|
||||||
<Version>3.7.115</Version>
|
<Version>3.8.118</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Vegasco.Server.AppHost.Shared;
|
using Vegasco.Server.AppHost.Shared;
|
||||||
|
|
||||||
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args);
|
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args);
|
||||||
|
|
||||||
IResourceBuilder<PostgresDatabaseResource> postgres = builder.AddPostgres(Constants.Database.ServiceName)
|
IResourceBuilder<PostgresServerResource> postgresBuilder = builder.AddPostgres(Constants.Database.ServiceName)
|
||||||
|
.WithLifetime(ContainerLifetime.Persistent)
|
||||||
|
.WithDataVolume();
|
||||||
|
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
postgresBuilder = postgresBuilder
|
||||||
|
.WithPgWeb()
|
||||||
|
.WithPgAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
IResourceBuilder<SeqResource> seq = builder.AddSeq("seq")
|
||||||
.WithLifetime(ContainerLifetime.Persistent)
|
.WithLifetime(ContainerLifetime.Persistent)
|
||||||
.WithDataVolume()
|
.WithDataVolume()
|
||||||
|
.WithExternalHttpEndpoints()
|
||||||
|
.WithImageTag("latest");
|
||||||
|
|
||||||
|
IResourceBuilder<PostgresDatabaseResource> postgres = postgresBuilder
|
||||||
.AddDatabase(Constants.Database.Name);
|
.AddDatabase(Constants.Database.Name);
|
||||||
|
|
||||||
IResourceBuilder<ProjectResource> api = builder
|
IResourceBuilder<ProjectResource> api = builder
|
||||||
.AddProject<Projects.Vegasco_Server_Api>(Constants.Projects.Api)
|
.AddProject<Projects.Vegasco_Server_Api>(Constants.Projects.Api)
|
||||||
.WithReference(postgres)
|
.WithReference(postgres)
|
||||||
.WaitFor(postgres);
|
.WaitFor(postgres)
|
||||||
|
.WithReference(seq)
|
||||||
|
.WaitFor(seq);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.AddNpmApp("Vegasco-Web", "../Vegasco-Web")
|
.AddNpmApp("Vegasco-Web", "../Vegasco-Web")
|
||||||
.WithReference(api)
|
.WithReference(api)
|
||||||
.WaitFor(api)
|
.WaitFor(api)
|
||||||
.WithHttpEndpoint(port: 44200, env: "PORT", isProxied: false)
|
.WithHttpEndpoint(port: 44200, env: "PORT")
|
||||||
.WithExternalHttpEndpoints()
|
.WithExternalHttpEndpoints()
|
||||||
.WithHttpHealthCheck("/", 200);
|
.WithHttpHealthCheck("/", 200);
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.3.0" />
|
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.1" />
|
||||||
<PackageReference Include="Aspire.Hosting.NodeJs" Version="9.3.1" />
|
<PackageReference Include="Aspire.Hosting.NodeJs" Version="9.5.1" />
|
||||||
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.3.0" />
|
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.5.1" />
|
||||||
<PackageReference Update="Nerdbank.GitVersioning">
|
<PackageReference Update="Nerdbank.GitVersioning">
|
||||||
<Version>3.7.115</Version>
|
<Version>3.8.118</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Aspire.Hosting.Seq" Version="9.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -25,4 +26,14 @@
|
|||||||
<ProjectReference Include="..\Vegasco.Server.Api\Vegasco.Server.Api.csproj" />
|
<ProjectReference Include="..\Vegasco.Server.Api\Vegasco.Server.Api.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="RestoreNpm" BeforeTargets="Build" Condition=" '$(DesignTimeBuild)' != 'true' ">
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageJsons Include="..\*\package.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Install npm packages if node_modules is missing -->
|
||||||
|
<Message Importance="Normal" Text="Installing npm packages for %(PackageJsons.RelativeDir)" Condition="!Exists('%(PackageJsons.RootDir)%(PackageJsons.Directory)/node_modules')" />
|
||||||
|
<Exec Command="pnpm install" WorkingDirectory="%(PackageJsons.RootDir)%(PackageJsons.Directory)" Condition="!Exists('%(PackageJsons.RootDir)%(PackageJsons.Directory)/node_modules')" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -10,15 +10,15 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.5.0" />
|
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.10.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.3.0" />
|
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.5.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||||
<PackageReference Update="Nerdbank.GitVersioning">
|
<PackageReference Update="Nerdbank.GitVersioning">
|
||||||
<Version>3.7.115</Version>
|
<Version>3.8.118</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ namespace Vegasco.Server.Api.Tests.Integration;
|
|||||||
|
|
||||||
internal class CarFaker
|
internal class CarFaker
|
||||||
{
|
{
|
||||||
private readonly Faker _faker = new();
|
|
||||||
|
|
||||||
internal CreateCar.Request CreateCarRequest()
|
internal CreateCar.Request CreateCarRequest()
|
||||||
{
|
{
|
||||||
return new CreateCar.Request(_faker.Vehicle.Model());
|
Faker faker = new();
|
||||||
|
return new CreateCar.Request(faker.Person.FirstName);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal UpdateCar.Request UpdateCarRequest()
|
internal UpdateCar.Request UpdateCarRequest()
|
||||||
{
|
{
|
||||||
return new UpdateCar.Request(_faker.Vehicle.Model());
|
Faker faker = new();
|
||||||
|
return new UpdateCar.Request(faker.Person.FirstName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ public class CreateCarTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||||
var createdCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
createdCar.Should().BeEquivalentTo(createCarRequest, o => o.ExcludingMissingMembers());
|
createdCar.Should().BeEquivalentTo(createCarRequest, o => o.ExcludingMissingMembers());
|
||||||
|
|
||||||
_dbContext.Cars.Should().ContainEquivalentOf(createdCar, o => o.Excluding(x => x!.Id))
|
_dbContext.Cars.Should().ContainEquivalentOf(createdCar, o => o.Excluding(x => x!.Id))
|
||||||
@@ -46,14 +46,14 @@ public class CreateCarTests : IAsyncLifetime
|
|||||||
public async Task CreateCar_ShouldReturnValidationProblems_WhenRequestIsNotValid()
|
public async Task CreateCar_ShouldReturnValidationProblems_WhenRequestIsNotValid()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var createCarRequest = new CreateCar.Request("");
|
CreateCar.Request createCarRequest = new CreateCar.Request("");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
ValidationProblemDetails? validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||||
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
||||||
x.Equals(nameof(CreateCar.Request.Name), StringComparison.OrdinalIgnoreCase));
|
x.Equals(nameof(CreateCar.Request.Name), StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class DeleteCarTests : IAsyncLifetime
|
|||||||
public async Task DeleteCar_ShouldReturnNotFound_WhenCarDoesNotExist()
|
public async Task DeleteCar_ShouldReturnNotFound_WhenCarDoesNotExist()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var randomCarId = Guid.NewGuid();
|
Guid randomCarId = Guid.NewGuid();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/cars/{randomCarId}");
|
HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/cars/{randomCarId}");
|
||||||
@@ -43,7 +43,7 @@ public class DeleteCarTests : IAsyncLifetime
|
|||||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/cars/{createdCar!.Id}");
|
HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/cars/{createdCar!.Id}");
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class GetCarTests : IAsyncLifetime
|
|||||||
public async Task GetCar_ShouldReturnNotFound_WhenCarDoesNotExist()
|
public async Task GetCar_ShouldReturnNotFound_WhenCarDoesNotExist()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var randomCarId = Guid.NewGuid();
|
Guid randomCarId = Guid.NewGuid();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/cars/{randomCarId}");
|
HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/cars/{randomCarId}");
|
||||||
@@ -37,14 +37,14 @@ public class GetCarTests : IAsyncLifetime
|
|||||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/cars/{createdCar!.Id}");
|
HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/cars/{createdCar!.Id}");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
var car = await response.Content.ReadFromJsonAsync<GetCar.Response>();
|
GetCar.Response? car = await response.Content.ReadFromJsonAsync<GetCar.Response>();
|
||||||
car.Should().BeEquivalentTo(createdCar);
|
car.Should().BeEquivalentTo(createdCar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class GetCarsTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
var apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
|
GetCars.ApiResponse? apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
|
||||||
apiResponse!.Cars.Should().BeEmpty();
|
apiResponse!.Cars.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,13 +38,13 @@ public class GetCarsTests : IAsyncLifetime
|
|||||||
List<CreateCar.Response> createdCars = [];
|
List<CreateCar.Response> createdCars = [];
|
||||||
|
|
||||||
const int numberOfCars = 5;
|
const int numberOfCars = 5;
|
||||||
for (var i = 0; i < numberOfCars; i++)
|
for (int i = 0; i < numberOfCars; i++)
|
||||||
{
|
{
|
||||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
createdCars.Add(createdCar!);
|
createdCars.Add(createdCar!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ public class GetCarsTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
var apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
|
GetCars.ApiResponse? apiResponse = await response.Content.ReadFromJsonAsync<GetCars.ApiResponse>();
|
||||||
apiResponse!.Cars.Should().BeEquivalentTo(createdCars);
|
apiResponse!.Cars.Should().BeEquivalentTo(createdCars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class UpdateCarTests : IAsyncLifetime
|
|||||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
|
|
||||||
UpdateCar.Request updateCarRequest = _carFaker.UpdateCarRequest();
|
UpdateCar.Request updateCarRequest = _carFaker.UpdateCarRequest();
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ public class UpdateCarTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
var updatedCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? updatedCar = await response.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
updatedCar!.Id.Should().Be(createdCar.Id);
|
updatedCar!.Id.Should().Be(createdCar.Id);
|
||||||
updatedCar.Should().BeEquivalentTo(updateCarRequest, o => o.ExcludingMissingMembers());
|
updatedCar.Should().BeEquivalentTo(updateCarRequest, o => o.ExcludingMissingMembers());
|
||||||
|
|
||||||
@@ -57,16 +57,16 @@ public class UpdateCarTests : IAsyncLifetime
|
|||||||
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
CreateCar.Request createCarRequest = _carFaker.CreateCarRequest();
|
||||||
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCar = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
|
|
||||||
var updateCarRequest = new UpdateCar.Request("");
|
UpdateCar.Request updateCarRequest = new UpdateCar.Request("");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/cars/{createdCar!.Id}", updateCarRequest);
|
HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/cars/{createdCar!.Id}", updateCarRequest);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
ValidationProblemDetails? validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||||
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
||||||
x.Equals(nameof(CreateCar.Request.Name), StringComparison.OrdinalIgnoreCase));
|
x.Equals(nameof(CreateCar.Request.Name), StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ public class UpdateCarTests : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
UpdateCar.Request updateCarRequest = _carFaker.UpdateCarRequest();
|
UpdateCar.Request updateCarRequest = _carFaker.UpdateCarRequest();
|
||||||
var randomCarId = Guid.NewGuid();
|
Guid randomCarId = Guid.NewGuid();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/cars/{randomCarId}", updateCarRequest);
|
HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/cars/{randomCarId}", updateCarRequest);
|
||||||
|
|||||||
@@ -5,25 +5,22 @@ namespace Vegasco.Server.Api.Tests.Integration;
|
|||||||
|
|
||||||
internal class ConsumptionFaker
|
internal class ConsumptionFaker
|
||||||
{
|
{
|
||||||
private readonly Faker _faker = new();
|
|
||||||
|
|
||||||
internal CreateConsumption.Request CreateConsumptionRequest(Guid carId)
|
internal CreateConsumption.Request CreateConsumptionRequest(Guid carId)
|
||||||
{
|
{
|
||||||
|
Faker faker = new();
|
||||||
return new CreateConsumption.Request(
|
return new CreateConsumption.Request(
|
||||||
_faker.Date.RecentOffset(),
|
faker.Date.RecentOffset(),
|
||||||
_faker.Random.Int(1, 1_000),
|
faker.Random.Int(1, 1_000),
|
||||||
_faker.Random.Int(20, 70),
|
faker.Random.Int(20, 70),
|
||||||
_faker.Random.Bool(),
|
|
||||||
carId);
|
carId);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal UpdateConsumption.Request UpdateConsumptionRequest()
|
internal UpdateConsumption.Request UpdateConsumptionRequest()
|
||||||
{
|
{
|
||||||
CreateConsumption.Request createRequest = CreateConsumptionRequest(default);
|
CreateConsumption.Request createRequest = CreateConsumptionRequest(Guid.Empty);
|
||||||
return new UpdateConsumption.Request(
|
return new UpdateConsumption.Request(
|
||||||
createRequest.DateTime,
|
createRequest.DateTime,
|
||||||
createRequest.Distance,
|
createRequest.Distance,
|
||||||
createRequest.Amount,
|
createRequest.Amount);
|
||||||
createRequest.IgnoreInCalculation);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ public class CreateConsumptionTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||||
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
CreateConsumption.Response? createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||||
createdConsumption.Should().BeEquivalentTo(createConsumptionRequest, o => o.ExcludingMissingMembers());
|
createdConsumption.Should().BeEquivalentTo(createConsumptionRequest, o => o.ExcludingMissingMembers());
|
||||||
|
|
||||||
_dbContext.Consumptions.Should().HaveCount(1)
|
_dbContext.Consumptions.Should().HaveCount(1)
|
||||||
@@ -64,7 +64,7 @@ public class CreateConsumptionTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
ValidationProblemDetails? validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||||
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
||||||
x.Equals(nameof(createConsumptionRequest.CarId), StringComparison.OrdinalIgnoreCase));
|
x.Equals(nameof(createConsumptionRequest.CarId), StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ public class CreateConsumptionTests : IAsyncLifetime
|
|||||||
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
||||||
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
return createdCarResponse!;
|
return createdCarResponse!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class DeleteConsumptionTests : IAsyncLifetime
|
|||||||
public async Task DeleteConsumption_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
|
public async Task DeleteConsumption_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var consumptionId = Guid.NewGuid();
|
Guid consumptionId = Guid.NewGuid();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
using HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/consumptions/{consumptionId}");
|
using HttpResponseMessage response = await _factory.HttpClient.DeleteAsync($"v1/consumptions/{consumptionId}");
|
||||||
@@ -58,7 +58,7 @@ public class DeleteConsumptionTests : IAsyncLifetime
|
|||||||
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
|
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
|
||||||
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
|
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
CreateConsumption.Response? createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||||
return createdConsumption!;
|
return createdConsumption!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ public class DeleteConsumptionTests : IAsyncLifetime
|
|||||||
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
||||||
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
return createdCarResponse!;
|
return createdCarResponse!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public class GetConsumptionTests : IAsyncLifetime
|
|||||||
// Assert
|
// Assert
|
||||||
string content = await response.Content.ReadAsStringAsync();
|
string content = await response.Content.ReadAsStringAsync();
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
var consumption = await response.Content.ReadFromJsonAsync<GetConsumption.Response>();
|
GetConsumption.Response? consumption = await response.Content.ReadFromJsonAsync<GetConsumption.Response>();
|
||||||
consumption.Should().BeEquivalentTo(createdConsumption);
|
consumption.Should().BeEquivalentTo(createdConsumption);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ public class GetConsumptionTests : IAsyncLifetime
|
|||||||
public async Task GetConsumptions_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
|
public async Task GetConsumptions_ShouldReturnNotFound_WhenConsumptionDoesNotExist()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var consumptionId = Guid.NewGuid();
|
Guid consumptionId = Guid.NewGuid();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
using HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/consumptions{consumptionId}");
|
using HttpResponseMessage response = await _factory.HttpClient.GetAsync($"v1/consumptions{consumptionId}");
|
||||||
@@ -60,7 +60,7 @@ public class GetConsumptionTests : IAsyncLifetime
|
|||||||
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
|
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
|
||||||
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
|
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
CreateConsumption.Response? createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||||
return createdConsumption!;
|
return createdConsumption!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ public class GetConsumptionTests : IAsyncLifetime
|
|||||||
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
||||||
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
return createdCarResponse!;
|
return createdCarResponse!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class GetConsumptionsTests : IAsyncLifetime
|
|||||||
// Arrange
|
// Arrange
|
||||||
List<CreateConsumption.Response> createdConsumptions = [];
|
List<CreateConsumption.Response> createdConsumptions = [];
|
||||||
const int numberOfConsumptions = 3;
|
const int numberOfConsumptions = 3;
|
||||||
for (var i = 0; i < numberOfConsumptions; i++)
|
for (int i = 0; i < numberOfConsumptions; i++)
|
||||||
{
|
{
|
||||||
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
|
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
|
||||||
createdConsumptions.Add(createdConsumption);
|
createdConsumptions.Add(createdConsumption);
|
||||||
@@ -42,8 +42,16 @@ public class GetConsumptionsTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
var apiResponse = await response.Content.ReadFromJsonAsync<GetConsumptions.ApiResponse>();
|
GetConsumptions.ApiResponse? apiResponse =
|
||||||
apiResponse!.Consumptions.Should().BeEquivalentTo(createdConsumptions);
|
await response.Content.ReadFromJsonAsync<GetConsumptions.ApiResponse>();
|
||||||
|
|
||||||
|
apiResponse.Should().NotBeNull();
|
||||||
|
apiResponse.Consumptions.Should().HaveCount(createdConsumptions.Count);
|
||||||
|
apiResponse.Consumptions.Should().BeEquivalentTo(createdConsumptions, o => o.ExcludingMissingMembers());
|
||||||
|
apiResponse.Consumptions
|
||||||
|
.Select(x => x.Car.Id)
|
||||||
|
.Should()
|
||||||
|
.BeEquivalentTo(createdConsumptions, o => o.ExcludingMissingMembers());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -56,26 +64,32 @@ public class GetConsumptionsTests : IAsyncLifetime
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
var apiResponse = await response.Content.ReadFromJsonAsync<GetConsumptions.ApiResponse>();
|
GetConsumptions.ApiResponse? apiResponse =
|
||||||
|
await response.Content.ReadFromJsonAsync<GetConsumptions.ApiResponse>();
|
||||||
apiResponse!.Consumptions.Should().BeEmpty();
|
apiResponse!.Consumptions.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CreateConsumption.Response> CreateConsumptionAsync()
|
private async Task<CreateConsumption.Response> CreateConsumptionAsync()
|
||||||
{
|
{
|
||||||
CreateCar.Response createdCarResponse = await CreateCarAsync();
|
CreateCar.Response createdCarResponse = await CreateCarAsync();
|
||||||
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
|
CreateConsumption.Request createConsumptionRequest =
|
||||||
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
|
_consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
|
||||||
|
using HttpResponseMessage response =
|
||||||
|
await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
CreateConsumption.Response? createdConsumption =
|
||||||
|
await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||||
return createdConsumption!;
|
return createdConsumption!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CreateCar.Response> CreateCarAsync()
|
private async Task<CreateCar.Response> CreateCarAsync()
|
||||||
{
|
{
|
||||||
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
||||||
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
using HttpResponseMessage createCarResponse =
|
||||||
|
await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCarResponse =
|
||||||
|
await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
return createdCarResponse!;
|
return createdCarResponse!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
|||||||
// Assert
|
// Assert
|
||||||
string content = await response.Content.ReadAsStringAsync();
|
string content = await response.Content.ReadAsStringAsync();
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
var updatedConsumption = await response.Content.ReadFromJsonAsync<UpdateConsumption.Response>();
|
UpdateConsumption.Response? updatedConsumption = await response.Content.ReadFromJsonAsync<UpdateConsumption.Response>();
|
||||||
updatedConsumption.Should().BeEquivalentTo(updateConsumptionRequest, o => o.ExcludingMissingMembers());
|
updatedConsumption.Should().BeEquivalentTo(updateConsumptionRequest, o => o.ExcludingMissingMembers());
|
||||||
|
|
||||||
_dbContext.Consumptions.Should().HaveCount(1)
|
_dbContext.Consumptions.Should().HaveCount(1)
|
||||||
@@ -59,7 +59,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
|||||||
// Arrange
|
// Arrange
|
||||||
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
|
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
|
||||||
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest() with { Distance = -42 };
|
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest() with { Distance = -42 };
|
||||||
var randomGuid = Guid.NewGuid();
|
Guid randomGuid = Guid.NewGuid();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{randomGuid}", updateConsumptionRequest);
|
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{randomGuid}", updateConsumptionRequest);
|
||||||
@@ -67,7 +67,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
|||||||
// Assert
|
// Assert
|
||||||
string content = await response.Content.ReadAsStringAsync();
|
string content = await response.Content.ReadAsStringAsync();
|
||||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||||
var validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
ValidationProblemDetails? validationProblemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||||
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
validationProblemDetails!.Errors.Keys.Should().Contain(x =>
|
||||||
x.Equals(nameof(updateConsumptionRequest.Distance), StringComparison.OrdinalIgnoreCase));
|
x.Equals(nameof(updateConsumptionRequest.Distance), StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
|||||||
// Arrange
|
// Arrange
|
||||||
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
|
CreateConsumption.Response createdConsumption = await CreateConsumptionAsync();
|
||||||
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest();
|
UpdateConsumption.Request updateConsumptionRequest = _consumptionFaker.UpdateConsumptionRequest();
|
||||||
var randomGuid = Guid.NewGuid();
|
Guid randomGuid = Guid.NewGuid();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{randomGuid}", updateConsumptionRequest);
|
using HttpResponseMessage response = await _factory.HttpClient.PutAsJsonAsync($"v1/consumptions/{randomGuid}", updateConsumptionRequest);
|
||||||
@@ -98,7 +98,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
|||||||
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
|
CreateConsumption.Request createConsumptionRequest = _consumptionFaker.CreateConsumptionRequest(createdCarResponse.Id);
|
||||||
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
|
using HttpResponseMessage response = await _factory.HttpClient.PostAsJsonAsync("v1/consumptions", createConsumptionRequest);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
CreateConsumption.Response? createdConsumption = await response.Content.ReadFromJsonAsync<CreateConsumption.Response>();
|
||||||
return createdConsumption!;
|
return createdConsumption!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ public class UpdateConsumptionTests : IAsyncLifetime
|
|||||||
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
CreateCar.Request createCarRequest = new CarFaker().CreateCarRequest();
|
||||||
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
using HttpResponseMessage createCarResponse = await _factory.HttpClient.PostAsJsonAsync("v1/cars", createCarRequest);
|
||||||
createCarResponse.EnsureSuccessStatusCode();
|
createCarResponse.EnsureSuccessStatusCode();
|
||||||
var createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
CreateCar.Response? createdCarResponse = await createCarResponse.Content.ReadFromJsonAsync<CreateCar.Response>();
|
||||||
return createdCarResponse!;
|
return createdCarResponse!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using FluentAssertions.Extensions;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Vegasco.Server.Api.Info;
|
||||||
|
|
||||||
|
namespace Vegasco.Server.Api.Tests.Integration.Info;
|
||||||
|
|
||||||
|
[Collection(SharedTestCollection.Name)]
|
||||||
|
public sealed class GetCurrentTimeTests
|
||||||
|
{
|
||||||
|
private readonly WebAppFactory _factory;
|
||||||
|
|
||||||
|
public GetCurrentTimeTests(WebAppFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetServerInfo_ShouldReturnServerInfo_WhenCalled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act
|
||||||
|
using HttpResponseMessage response = await _factory.HttpClient.GetAsync("/v1/info/time");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
response.IsSuccessStatusCode.Should().BeTrue();
|
||||||
|
GetCurrentTime.Response? timeInfo = await response.Content.ReadFromJsonAsync<GetCurrentTime.Response>();
|
||||||
|
timeInfo.Should().NotBeNull();
|
||||||
|
timeInfo.CurrentTime.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ public class GetServerInfoTests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
response.IsSuccessStatusCode.Should().BeTrue();
|
response.IsSuccessStatusCode.Should().BeTrue();
|
||||||
var serverInfo = await response.Content.ReadFromJsonAsync<GetServerInfo.Response>();
|
GetServerInfo.Response? serverInfo = await response.Content.ReadFromJsonAsync<GetServerInfo.Response>();
|
||||||
serverInfo!.Environment.Should().NotBeEmpty();
|
serverInfo!.Environment.Should().NotBeEmpty();
|
||||||
serverInfo.CommitDate.Should().BeAfter(23.August(2024))
|
serverInfo.CommitDate.Should().BeAfter(23.August(2024))
|
||||||
.And.NotBeAfter(DateTime.Now);
|
.And.NotBeAfter(DateTime.Now);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ internal sealed class PostgresRespawner : IDisposable
|
|||||||
DbConnection connection = new NpgsqlConnection(connectionString);
|
DbConnection connection = new NpgsqlConnection(connectionString);
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
|
|
||||||
var respawner = await Respawner.CreateAsync(connection,
|
Respawner respawner = await Respawner.CreateAsync(connection,
|
||||||
new RespawnerOptions
|
new RespawnerOptions
|
||||||
{
|
{
|
||||||
SchemasToInclude = ["public"],
|
SchemasToInclude = ["public"],
|
||||||
|
|||||||
@@ -10,21 +10,21 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Azure.Identity" Version="1.14.0" />
|
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
||||||
<PackageReference Include="Bogus" Version="35.6.3" />
|
<PackageReference Include="Bogus" Version="35.6.4" />
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="FluentAssertions" Version="[7.2.0,8.0.0)" />
|
<PackageReference Include="FluentAssertions" Version="[7.2.0,8.0.0)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||||
<PackageReference Include="Respawn" Version="6.2.1" />
|
<PackageReference Include="Respawn" Version="6.2.1" />
|
||||||
<PackageReference Include="System.Formats.Asn1" Version="9.0.5" />
|
<PackageReference Include="System.Formats.Asn1" Version="9.0.10" />
|
||||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.5.0" />
|
<PackageReference Include="Testcontainers.PostgreSql" Version="4.7.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Nerdbank.GitVersioning" Version="3.7.115" />
|
<PackageReference Update="Nerdbank.GitVersioning" Version="3.8.118" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ public class CreateConsumptionRequestValidatorTests
|
|||||||
_sut = new CreateConsumption.Validator(_timeProvider);
|
_sut = new CreateConsumption.Validator(_timeProvider);
|
||||||
|
|
||||||
_validRequest = new CreateConsumption.Request(
|
_validRequest = new CreateConsumption.Request(
|
||||||
_utcNow.AddDays(-1),
|
_utcNow.Date.AddDays(1).AddTicks(-1),
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
false,
|
|
||||||
Guid.NewGuid());
|
Guid.NewGuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +38,10 @@ public class CreateConsumptionRequestValidatorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAsync_ShouldBeInvalid_WhenDateTimeIsGreaterThanUtcNow()
|
public async Task ValidateAsync_ShouldBeInvalid_WhenDateTimeIsGreaterThanUtcToday()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
CreateConsumption.Request request = _validRequest with { DateTime = _utcNow.AddDays(1) };
|
CreateConsumption.Request request = _validRequest with { DateTime = _utcNow.Date.AddDays(1) };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
ValidationResult? result = await _sut.ValidateAsync(request);
|
ValidationResult? result = await _sut.ValidateAsync(request);
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ public class UpdateConsumptionRequestValidatorTests
|
|||||||
_sut = new UpdateConsumption.Validator(_timeProvider);
|
_sut = new UpdateConsumption.Validator(_timeProvider);
|
||||||
|
|
||||||
_validRequest = new UpdateConsumption.Request(
|
_validRequest = new UpdateConsumption.Request(
|
||||||
_utcNow.AddDays(-1),
|
_utcNow.Date.AddDays(1).AddTicks(-1),
|
||||||
1,
|
1,
|
||||||
1,
|
1);
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -39,10 +38,10 @@ public class UpdateConsumptionRequestValidatorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ValidateAsync_ShouldBeInvalid_WhenDateTimeIsGreaterThanUtcNow()
|
public async Task ValidateAsync_ShouldBeInvalid_WhenDateTimeIsGreaterThanUtcToday()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
UpdateConsumption.Request request = _validRequest with { DateTime = _utcNow.AddDays(1) };
|
UpdateConsumption.Request request = _validRequest with { DateTime = _utcNow.Date.AddDays(1) };
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
ValidationResult? result = await _sut.ValidateAsync(request);
|
ValidationResult? result = await _sut.ValidateAsync(request);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user