Compare commits
62 Commits
63c7624a00
...
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 |
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
|
||||||
|
|||||||
34
README.md
34
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
|
||||||
2
src/Vegasco-Web/.vscode/tasks.json
vendored
2
src/Vegasco-Web/.vscode/tasks.json
vendored
@@ -14,7 +14,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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"
|
||||||
},
|
},
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
"@ng-icons/material-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",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"keycloak-angular": "^19.0.2",
|
"keycloak-angular": "^19.0.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"primeng": "^19.1.3",
|
"primeng": "^19.1.3",
|
||||||
|
|||||||
8
src/Vegasco-Web/pnpm-lock.yaml
generated
8
src/Vegasco-Web/pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ importers:
|
|||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.10
|
specifier: ^4.1.10
|
||||||
version: 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)
|
||||||
@@ -2229,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:
|
||||||
@@ -6763,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
|
||||||
|
|||||||
@@ -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": "",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
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',
|
||||||
})
|
})
|
||||||
export class ConsumptionClient {
|
export class ConsumptionClient {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly apiBasePath = inject(API_BASE_PATH, {optional: true});
|
private readonly apiBasePath = inject(API_BASE_PATH, { optional: true });
|
||||||
|
|
||||||
getAll(): Observable<GetConsumptionEntriesResponse> {
|
getAll(): Observable<GetConsumptionEntriesResponse> {
|
||||||
return this.http.get<GetConsumptionEntriesResponse>(`${this.apiBasePath}/v1/consumptions`);
|
return this.http.get<GetConsumptionEntriesResponse>(`${this.apiBasePath}/v1/consumptions`);
|
||||||
|
|||||||
@@ -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,9 +0,0 @@
|
|||||||
export interface Consumption {
|
|
||||||
id: string;
|
|
||||||
dateTime: string;
|
|
||||||
distance: number;
|
|
||||||
amount: number;
|
|
||||||
ignoreInCalculation: boolean;
|
|
||||||
carId: string;
|
|
||||||
car: Car;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
<main class="main">
|
<main class="main">
|
||||||
<header class="h-12 bg-primary text-primary-contrast">
|
<header class="h-12 bg-primary text-primary-contrast">
|
||||||
<div class="header max-content-width mx-auto">
|
<div class="header max-content-width mx-auto flex items-center justify-between">
|
||||||
<a routerLink="/" class="reset cursor-pointer">
|
<a routerLink="/" class="reset cursor-pointer">
|
||||||
Vegasco
|
Vegasco
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="content max-content-width mx-auto">
|
<div class="content max-content-width mx-auto">
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 { MessageService } from 'primeng/api';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet, ToastModule],
|
imports: [RouterLink, RouterOutlet, ToastModule],
|
||||||
providers: [MessageService],
|
providers: [MessageService],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss'
|
styleUrl: './app.scss'
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@
|
|||||||
[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"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Component, computed, DestroyRef, inject, input, OnInit, signal, Signal } from '@angular/core';
|
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 { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
|
||||||
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 { RoutingService } from '@vegasco-web/services/routing.service';
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
@@ -51,6 +52,8 @@ export class EditEntryComponent implements OnInit {
|
|||||||
|
|
||||||
protected readonly id = input<string | undefined>(undefined);
|
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',
|
||||||
@@ -60,7 +63,7 @@ export class EditEntryComponent implements OnInit {
|
|||||||
|
|
||||||
protected readonly formGroup = new FormGroup({
|
protected readonly formGroup = new FormGroup({
|
||||||
[this.formFieldNames.car]: new FormControl<Car | null>({ value: null, disabled: true }, [Validators.required]),
|
[this.formFieldNames.car]: new FormControl<Car | null>({ value: null, disabled: true }, [Validators.required]),
|
||||||
[this.formFieldNames.date]: new FormControl<Date>({ value: new Date(), disabled: true }, [Validators.required]),
|
[this.formFieldNames.date]: new FormControl<Date>({ value: new Date(), disabled: true }, [Validators.required, this.dateTimeGreaterThanOrEqualToTodayValidator]),
|
||||||
[this.formFieldNames.mileage]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
[this.formFieldNames.mileage]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
||||||
[this.formFieldNames.amount]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
[this.formFieldNames.amount]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
||||||
});
|
});
|
||||||
@@ -81,7 +84,8 @@ export class EditEntryComponent implements OnInit {
|
|||||||
.getAll()
|
.getAll()
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
map(response => response.cars),
|
map(response => response.cars
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
tap(cars => {
|
tap(cars => {
|
||||||
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||||
|
|
||||||
@@ -108,7 +112,6 @@ export class EditEntryComponent implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(this.destroyRef),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
tap((car) => {
|
tap((car) => {
|
||||||
console.log('Selected car changed (edit entry):', car);
|
|
||||||
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -166,6 +169,11 @@ export class EditEntryComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var entryId = this.id();
|
var entryId = this.id();
|
||||||
if (entryId === undefined || entryId === null) {
|
if (entryId === undefined || entryId === null) {
|
||||||
this.createEntry();
|
this.createEntry();
|
||||||
@@ -183,7 +191,6 @@ export class EditEntryComponent implements OnInit {
|
|||||||
dateTime: dateTime.toISOString(),
|
dateTime: dateTime.toISOString(),
|
||||||
distance: this.formGroup.controls[this.formFieldNames.mileage].value!,
|
distance: this.formGroup.controls[this.formFieldNames.mileage].value!,
|
||||||
amount: this.formGroup.controls[this.formFieldNames.amount].value!,
|
amount: this.formGroup.controls[this.formFieldNames.amount].value!,
|
||||||
ignoreInCalculation: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,4 +279,15 @@ export class EditEntryComponent implements OnInit {
|
|||||||
|
|
||||||
return EMPTY;
|
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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,6 @@
|
|||||||
</div>
|
</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="matDirectionsCarOutline" />
|
|
||||||
<div>{{ entry().car.name }}</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="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">
|
<div class="flex gap-2 items-center">
|
||||||
<ng-icon name="matStraightenSharp" />
|
<ng-icon name="matStraightenSharp" />
|
||||||
@@ -32,6 +25,18 @@
|
|||||||
<div>{{entry().amount }} ℓ</div>
|
<div>{{entry().amount }} ℓ</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="bg-red-500 text-white rounded-r text-center flex flex-col justify-center">
|
<div class="bg-red-500 text-white rounded-r text-center flex flex-col justify-center">
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Component, DestroyRef, inject, input, output } from '@angular/core';
|
import { Component, computed, DestroyRef, inject, input, output } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
import {
|
import {
|
||||||
matCalendarMonthSharp,
|
matCalendarMonthSharp,
|
||||||
matDeleteSharp,
|
matDeleteSharp,
|
||||||
matStraightenSharp,
|
|
||||||
matLocalGasStationSharp,
|
matLocalGasStationSharp,
|
||||||
|
matSpeedSharp,
|
||||||
|
matStraightenSharp,
|
||||||
} from '@ng-icons/material-icons/sharp';
|
} from '@ng-icons/material-icons/sharp';
|
||||||
import {
|
|
||||||
matDirectionsCarOutline,
|
|
||||||
} from '@ng-icons/material-icons/outline';
|
|
||||||
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
||||||
import { Consumption } from '@vegasco-web/api/models/consumption';
|
|
||||||
import { RoutingService } from '@vegasco-web/services/routing.service';
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { CardModule } from 'primeng/card';
|
import { CardModule } from 'primeng/card';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
|
import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
|
||||||
|
import { FractionComponent } from "../fraction/fraction.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-entry-card',
|
selector: 'app-entry-card',
|
||||||
@@ -30,12 +27,13 @@ import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
|
|||||||
ConfirmDialogModule,
|
ConfirmDialogModule,
|
||||||
DatePipe,
|
DatePipe,
|
||||||
NgIconComponent,
|
NgIconComponent,
|
||||||
|
FractionComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideIcons({
|
provideIcons({
|
||||||
matDeleteSharp,
|
matDeleteSharp,
|
||||||
matCalendarMonthSharp,
|
matCalendarMonthSharp,
|
||||||
matDirectionsCarOutline,
|
matSpeedSharp,
|
||||||
matStraightenSharp,
|
matStraightenSharp,
|
||||||
matLocalGasStationSharp,
|
matLocalGasStationSharp,
|
||||||
}),
|
}),
|
||||||
@@ -45,9 +43,18 @@ import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
|
|||||||
styleUrl: './entry-card.component.scss'
|
styleUrl: './entry-card.component.scss'
|
||||||
})
|
})
|
||||||
export class EntryCardComponent {
|
export class EntryCardComponent {
|
||||||
readonly entry = input.required<Consumption>();
|
readonly entry = input.required<GetConsumptionEntriesEntry>();
|
||||||
|
|
||||||
readonly entryDeleted = output<Consumption>();
|
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 routingService = inject(RoutingService);
|
||||||
private readonly consumptionClient = inject(ConsumptionClient);
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
@@ -67,8 +74,6 @@ export class EntryCardComponent {
|
|||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.confirmationService.confirm({
|
this.confirmationService.confirm({
|
||||||
|
|||||||
@@ -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,7 +2,7 @@
|
|||||||
<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]="selectedCar" placeholder="Auto" [showClear]="true"
|
<p-select styleClass="w-full" [formControl]="selectedCar" placeholder="Auto" [showClear]="false"
|
||||||
[options]="(cars$ | async)!" optionLabel="name" />
|
[options]="(cars$ | async)!" optionLabel="name" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { AsyncPipe, CommonModule } from '@angular/common';
|
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { FormControl, 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 { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
||||||
import { MessageService } from 'primeng/api';
|
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';
|
||||||
@@ -20,14 +27,8 @@ import {
|
|||||||
tap,
|
tap,
|
||||||
throwError
|
throwError
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { EntriesOverviewService } from './services/entries-overview.service';
|
|
||||||
import { EntryCardComponent } from './components/entry-card/entry-card.component';
|
|
||||||
import {
|
|
||||||
matAddSharp,
|
|
||||||
} from '@ng-icons/material-icons/sharp';
|
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
|
||||||
import { SelectedCarService } from '../services/selected-car.service';
|
import { SelectedCarService } from '../services/selected-car.service';
|
||||||
|
import { EntryCardComponent } from './components/entry-card/entry-card.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-entries',
|
selector: 'app-entries',
|
||||||
@@ -48,18 +49,18 @@ import { SelectedCarService } from '../services/selected-car.service';
|
|||||||
provideIcons({
|
provideIcons({
|
||||||
matAddSharp,
|
matAddSharp,
|
||||||
}),
|
}),
|
||||||
EntriesOverviewService,
|
|
||||||
],
|
],
|
||||||
templateUrl: './entries.component.html',
|
templateUrl: './entries.component.html',
|
||||||
styleUrl: './entries.component.scss'
|
styleUrl: './entries.component.scss'
|
||||||
})
|
})
|
||||||
export class EntriesComponent implements OnInit {
|
export class EntriesComponent implements OnInit {
|
||||||
private readonly entriesOverviewService = inject(EntriesOverviewService);
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
private readonly messageService = inject(MessageService);
|
private readonly messageService = inject(MessageService);
|
||||||
private readonly selectedCarService = inject(SelectedCarService);
|
private readonly selectedCarService = inject(SelectedCarService);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
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);
|
||||||
@@ -69,9 +70,10 @@ export class EntriesComponent implements OnInit {
|
|||||||
private readonly deletedEntries$ = new BehaviorSubject(<string[]>[]);
|
private readonly deletedEntries$ = new BehaviorSubject(<string[]>[]);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const entries = this.entriesOverviewService.getEntries()
|
const entries = this.consumptionClient.getAll()
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
|
map(response => response.consumptions.sort((a, b) => b.dateTime.localeCompare(a.dateTime))),
|
||||||
catchError((error) => this.handleGetEntriesError(error))
|
catchError((error) => this.handleGetEntriesError(error))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -92,13 +94,16 @@ export class EntriesComponent implements OnInit {
|
|||||||
return nonDeletedEntries;
|
return nonDeletedEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nonDeletedEntries.filter(entry => entry.carId === selectedCar.id);
|
return nonDeletedEntries.filter(entry => entry.car.id === selectedCar.id);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cars$ = this.entriesOverviewService.getCars()
|
this.cars$ = this.carClient.getAll()
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
|
map(response => response.cars),
|
||||||
|
map((cars) => cars
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
tap((cars) => {
|
tap((cars) => {
|
||||||
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||||
|
|
||||||
@@ -121,14 +126,13 @@ export class EntriesComponent implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(this.destroyRef),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
tap((car) => {
|
tap((car) => {
|
||||||
console.log('Selected car changed (entries):', car);
|
|
||||||
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntryDeleted(entry: ConsumptionEntry): void {
|
onEntryDeleted(entry: GetConsumptionEntriesEntry): void {
|
||||||
this.deletedEntries$.next([...this.deletedEntries$.value, entry.id]);
|
this.deletedEntries$.next([...this.deletedEntries$.value, entry.id]);
|
||||||
this.messageService.add({
|
this.messageService.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { inject, Injectable } from "@angular/core";
|
|
||||||
import { CarClient } from "@vegasco-web/api/cars/car-client";
|
|
||||||
import { ConsumptionClient } from "@vegasco-web/api/consumptions/consumption-client";
|
|
||||||
import { Consumption } from "@vegasco-web/api/models/consumption";
|
|
||||||
import { RoutingService } from "@vegasco-web/services/routing.service";
|
|
||||||
import { combineLatest, map, Observable, shareReplay } from "rxjs";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class EntriesOverviewService {
|
|
||||||
private readonly carClient = inject(CarClient);
|
|
||||||
private readonly consumptionClient = inject(ConsumptionClient);
|
|
||||||
|
|
||||||
private cachedCars$: Observable<Car[]> | null = null;
|
|
||||||
|
|
||||||
private ensureCarsAreCached(): void {
|
|
||||||
if (this.cachedCars$ !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachedCars$ = this.carClient.getAll()
|
|
||||||
.pipe(
|
|
||||||
map(response => response.cars),
|
|
||||||
shareReplay(1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getEntries(): Observable<Consumption[]> {
|
|
||||||
this.ensureCarsAreCached();
|
|
||||||
|
|
||||||
const entries$ = this.consumptionClient.getAll()
|
|
||||||
.pipe(map(response => response.consumptions));
|
|
||||||
|
|
||||||
return combineLatest([this.cachedCars$!, entries$])
|
|
||||||
.pipe(
|
|
||||||
map(([cars, entries]) => {
|
|
||||||
return entries
|
|
||||||
.sort((a, b) => b.dateTime.localeCompare(a.dateTime))
|
|
||||||
.map((entry): Consumption => ({
|
|
||||||
...entry,
|
|
||||||
car: cars.find(car => car.id === entry.carId)!
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getCars(): Observable<Car[]> {
|
|
||||||
this.ensureCarsAreCached();
|
|
||||||
return this.cachedCars$!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,4 +18,16 @@ export class RoutingService {
|
|||||||
async navigateToCreateEntry(): Promise<void> {
|
async navigateToCreateEntry(): Promise<void> {
|
||||||
await this.router.navigate(['entries', 'create']);
|
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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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,5 @@
|
|||||||
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;
|
||||||
@@ -15,13 +16,16 @@ 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,
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var rows = await dbContext.Cars
|
Activity? activity = Activity.Current;
|
||||||
|
activity?.SetTag("id", id);
|
||||||
|
|
||||||
|
int rows = await dbContext.Cars
|
||||||
.Where(x => x.Id == new CarId(id))
|
.Where(x => x.Id == new CarId(id))
|
||||||
.ExecuteDeleteAsync(cancellationToken);
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -32,7 +36,7 @@ public static class DeleteCar
|
|||||||
|
|
||||||
if (rows > 1)
|
if (rows > 1)
|
||||||
{
|
{
|
||||||
var logger = loggerFactory.CreateLogger(nameof(DeleteCar));
|
ILogger logger = loggerFactory.CreateLogger(typeof(DeleteCar));
|
||||||
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{CarId}'", rows, id);
|
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{CarId}'", rows, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Diagnostics;
|
||||||
using Vegasco.Server.Api.Persistence;
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Consumptions;
|
namespace Vegasco.Server.Api.Consumptions;
|
||||||
@@ -21,7 +22,10 @@ public static class DeleteConsumption
|
|||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var rows = await dbContext.Consumptions
|
Activity? activity = Activity.Current;
|
||||||
|
activity?.SetTag("id", id);
|
||||||
|
|
||||||
|
int rows = await dbContext.Consumptions
|
||||||
.Where(x => x.Id == new ConsumptionId(id))
|
.Where(x => x.Id == new ConsumptionId(id))
|
||||||
.ExecuteDeleteAsync(cancellationToken);
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -32,7 +36,7 @@ public static class DeleteConsumption
|
|||||||
|
|
||||||
if (rows > 1)
|
if (rows > 1)
|
||||||
{
|
{
|
||||||
var logger = loggerFactory.CreateLogger(nameof(DeleteConsumption));
|
ILogger logger = loggerFactory.CreateLogger(typeof(DeleteConsumption));
|
||||||
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{ConsumptionId}'", rows, id);
|
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{ConsumptionId}'", rows, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,17 +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));
|
||||||
.OrderByDescending(x => x.DateTime)
|
|
||||||
.Select(x =>
|
logger.LogTrace("Received request to get consumptions with parameters: {@Request}", request);
|
||||||
new ResponseDto(x.Id.Value, x.DateTime, x.Distance, x.Amount, x.IgnoreInCalculation, x.CarId.Value))
|
Activity? activity = Activity.Current;
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
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);
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
<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="8.3.0" />
|
<PackageReference Include="FluentAssertions" Version="8.7.1" />
|
||||||
<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="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.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>
|
||||||
@@ -34,7 +34,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>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<File Path="version.json" />
|
<File Path="version.json" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/Vegasco.Server.Api/Vegasco.Server.Api.csproj" />
|
||||||
<Project Path="src/Vegasco.Server.AppHost.Shared/Vegasco.Server.AppHost.Shared.csproj" />
|
<Project Path="src/Vegasco.Server.AppHost.Shared/Vegasco.Server.AppHost.Shared.csproj" />
|
||||||
<Project Path="src/Vegasco.Server.AppHost/Vegasco.Server.AppHost.csproj" />
|
<Project Path="src/Vegasco.Server.AppHost/Vegasco.Server.AppHost.csproj" />
|
||||||
<Project Path="src/Vegasco.Server.ServiceDefaults/Vegasco.Server.ServiceDefaults.csproj" />
|
<Project Path="src/Vegasco.Server.ServiceDefaults/Vegasco.Server.ServiceDefaults.csproj" />
|
||||||
<Project Path="src/Vegasco.Server.Api/Vegasco.Server.Api.csproj" />
|
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/Vegasco.Server.Api.Tests.Integration/Vegasco.Server.Api.Tests.Integration.csproj" />
|
<Project Path="tests/Vegasco.Server.Api.Tests.Integration/Vegasco.Server.Api.Tests.Integration.csproj" />
|
||||||
|
|||||||
Reference in New Issue
Block a user