Compare commits
119 Commits
5727707cce
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| d4ae137115 | |||
| 9f51f508ce | |||
| 62824549fc | |||
| 0cb5e44f7a | |||
| 7d7f5750e3 | |||
| 789ba35c60 | |||
| 1226c42f19 | |||
| 5e083aeaf6 | |||
| 31efd6b4ad | |||
| 69bb19e4eb | |||
| db791a1183 | |||
| ad77c2fe2b | |||
| 87a0241f11 | |||
| f248be4e1f | |||
| 67d29333d9 | |||
| 5956f27646 | |||
| 69901a295c | |||
| 527759eb7b | |||
| d4fff6741c | |||
| a10070b9c7 | |||
| d10d1a6fdb | |||
| c57972d9a6 | |||
| ea019ebfa6 | |||
| 97a275478d | |||
| 731eab3898 | |||
| f018e62163 | |||
| 10e02b5e9b | |||
| c365af1d42 | |||
| 66c23ffb4f | |||
| 7ddc346e88 | |||
| 00e0869a13 | |||
| 925293d626 | |||
| 9b024967e6 | |||
| 288d470c1b | |||
| 84a72a8557 | |||
| d4223ed38f | |||
| 9f2c5db825 | |||
| 18cbc2225f | |||
| 267c4165dd | |||
| ef1c1d8ba1 | |||
| 8d4ae30224 | |||
| 02e7ed7030 | |||
| 9595bedd8e | |||
| af661632cc | |||
| 5062887010 | |||
| b41d5c5d33 | |||
| 4b377ce9f4 | |||
| 5e084ab0a8 | |||
| 559804765b | |||
| 5da1e2fd75 | |||
| ab32be98a6 | |||
| 8681247e76 | |||
| f6dbf489ad | |||
| eaa06029bb | |||
| 9e16d6004a | |||
| 0df7449a99 | |||
| 7f61e011ed | |||
| 9c372b31a6 | |||
| fd7a8024a9 | |||
| 4a8e3d02e0 | |||
| f7af144275 | |||
| cb3c8c0d18 | |||
| a997a3b825 | |||
| c58f6fe364 | |||
| 69bc76cab4 | |||
| 4b1f9e78df | |||
| 4c00f868c7 | |||
| 8b9ccdc694 | |||
| b8d1fddd91 | |||
| 9246729edf | |||
| e13b5f2cdc | |||
| 63c7624a00 | |||
| f58613d661 | |||
| d71e523074 | |||
| 1c8e02b3fa | |||
| feadab4dff | |||
| 41c342bb0f | |||
| 2e3000c3fc | |||
| 92e4da4b93 | |||
| 5978a96dd7 | |||
| b9375d66b6 | |||
| b07b0c1f0f | |||
| fd9b9c7c2e | |||
| b6f9b5fb26 | |||
| 87d81f98e9 | |||
| c5555b3003 | |||
| d8f82bb2d1 | |||
| 390241aa53 | |||
| b323f7a29f | |||
| 8ca16936a8 | |||
| f0998c818a | |||
| 0cf9f3cd0f | |||
| b382446828 | |||
| 16318c70f7 | |||
| f173d46c2e | |||
| 73fbe30b3d | |||
| 229bfe0b79 | |||
| 321ffc3b7c | |||
| 0fa5b080d8 | |||
| 85052df8a5 | |||
| bcbf76fda6 | |||
| b989c43ec3 | |||
| cba564a811 | |||
| 297af2b95d | |||
| 70acaf9738 | |||
| 354d28d167 | |||
| 07ab1efbe5 | |||
| 8a0776bc33 | |||
| 0baf50a2a2 | |||
| 766d060707 | |||
| df93f8299f | |||
| b28bc04a53 | |||
| 28148e4f69 | |||
| 9fb0f584a6 | |||
| edafe0e4ec | |||
| a1174e3b42 | |||
| 4bf07b0972 | |||
| 7d6f85db82 | |||
| f426368d15 |
34
.drone.yml
34
.drone.yml
@@ -10,16 +10,21 @@ trigger:
|
|||||||
- custom
|
- custom
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: compile
|
- name: compile (.NET)
|
||||||
image: mcr.microsoft.com/dotnet/sdk:9.0-alpine
|
image: mcr.microsoft.com/dotnet/sdk:9.0-alpine
|
||||||
environment:
|
environment:
|
||||||
CI_WORKSPACE: "/drone/src"
|
CI_WORKSPACE: "/drone/src"
|
||||||
commands:
|
commands:
|
||||||
- dotnet build
|
- dotnet build ./vegasco-server.slnx
|
||||||
volumes:
|
|
||||||
- name: dockersock
|
- name: compile (Angular)
|
||||||
path: /var/run
|
image: node:lts
|
||||||
|
commands:
|
||||||
|
- npm install -g pnpm
|
||||||
|
- cd src/Vegasco-Web
|
||||||
|
- pnpm install
|
||||||
|
- pnpm build
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
image: quay.io/testcontainers/dind-drone-plugin
|
image: quay.io/testcontainers/dind-drone-plugin
|
||||||
environment:
|
environment:
|
||||||
@@ -32,14 +37,16 @@ steps:
|
|||||||
- name: dockersock
|
- name: dockersock
|
||||||
path: /var/run
|
path: /var/run
|
||||||
depends_on:
|
depends_on:
|
||||||
- compile
|
- compile (.NET)
|
||||||
|
|
||||||
- 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
|
||||||
@@ -55,8 +62,12 @@ steps:
|
|||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- main
|
- main
|
||||||
|
- production
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
depends_on:
|
depends_on:
|
||||||
- compile
|
- compile (.NET)
|
||||||
- test
|
- test
|
||||||
|
|
||||||
- name: Telegram notification
|
- name: Telegram notification
|
||||||
@@ -70,7 +81,8 @@ steps:
|
|||||||
status:
|
status:
|
||||||
- failure
|
- failure
|
||||||
depends_on:
|
depends_on:
|
||||||
- compile
|
- compile (.NET)
|
||||||
|
- compile (Angular)
|
||||||
- test
|
- test
|
||||||
- docker build and push
|
- docker build and push
|
||||||
|
|
||||||
|
|||||||
271
.editorconfig
Normal file
271
.editorconfig
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# C# files
|
||||||
|
[*.cs]
|
||||||
|
|
||||||
|
#### Core EditorConfig Options ####
|
||||||
|
|
||||||
|
# Indentation and spacing
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 4
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
end_of_line = crlf
|
||||||
|
insert_final_newline = false
|
||||||
|
|
||||||
|
#### .NET Coding Conventions ####
|
||||||
|
|
||||||
|
# Organize usings
|
||||||
|
dotnet_separate_import_directive_groups = false
|
||||||
|
dotnet_sort_system_directives_first = false
|
||||||
|
file_header_template = unset
|
||||||
|
|
||||||
|
# this. and Me. preferences
|
||||||
|
dotnet_style_qualification_for_event = false
|
||||||
|
dotnet_style_qualification_for_field = false
|
||||||
|
dotnet_style_qualification_for_method = false
|
||||||
|
dotnet_style_qualification_for_property = false
|
||||||
|
|
||||||
|
# Language keywords vs BCL types preferences
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||||
|
dotnet_style_predefined_type_for_member_access = true
|
||||||
|
|
||||||
|
# Parentheses preferences
|
||||||
|
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
|
||||||
|
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
|
||||||
|
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
|
||||||
|
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
dotnet_style_coalesce_expression = true
|
||||||
|
dotnet_style_collection_initializer = true
|
||||||
|
dotnet_style_explicit_tuple_names = true
|
||||||
|
dotnet_style_namespace_match_folder = true
|
||||||
|
dotnet_style_null_propagation = true
|
||||||
|
dotnet_style_object_initializer = true
|
||||||
|
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||||
|
dotnet_style_prefer_auto_properties = true:suggestion
|
||||||
|
dotnet_style_prefer_collection_expression = when_types_loosely_match
|
||||||
|
dotnet_style_prefer_compound_assignment = true
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = true
|
||||||
|
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
|
||||||
|
dotnet_style_prefer_inferred_anonymous_type_member_names = true
|
||||||
|
dotnet_style_prefer_inferred_tuple_names = true
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
|
||||||
|
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||||
|
dotnet_style_prefer_simplified_interpolation = true
|
||||||
|
|
||||||
|
# Field preferences
|
||||||
|
dotnet_style_readonly_field = true
|
||||||
|
|
||||||
|
# Parameter preferences
|
||||||
|
dotnet_code_quality_unused_parameters = all
|
||||||
|
|
||||||
|
# Suppression preferences
|
||||||
|
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion
|
||||||
|
dotnet_style_allow_statement_immediately_after_block_experimental = false:suggestion
|
||||||
|
|
||||||
|
#### C# Coding Conventions ####
|
||||||
|
|
||||||
|
# var preferences
|
||||||
|
csharp_style_var_elsewhere = false
|
||||||
|
csharp_style_var_for_built_in_types = false
|
||||||
|
csharp_style_var_when_type_is_apparent = false
|
||||||
|
|
||||||
|
# Expression-bodied members
|
||||||
|
csharp_style_expression_bodied_accessors = true:silent
|
||||||
|
csharp_style_expression_bodied_constructors = false:silent
|
||||||
|
csharp_style_expression_bodied_indexers = true:silent
|
||||||
|
csharp_style_expression_bodied_lambdas = true:silent
|
||||||
|
csharp_style_expression_bodied_local_functions = true:silent
|
||||||
|
csharp_style_expression_bodied_methods = false:silent
|
||||||
|
csharp_style_expression_bodied_operators = false:silent
|
||||||
|
csharp_style_expression_bodied_properties = true:silent
|
||||||
|
|
||||||
|
# Pattern matching preferences
|
||||||
|
csharp_style_pattern_matching_over_as_with_null_check = true
|
||||||
|
csharp_style_pattern_matching_over_is_with_cast_check = true
|
||||||
|
csharp_style_prefer_extended_property_pattern = true
|
||||||
|
csharp_style_prefer_not_pattern = true
|
||||||
|
csharp_style_prefer_pattern_matching = true
|
||||||
|
csharp_style_prefer_switch_expression = true
|
||||||
|
|
||||||
|
# Null-checking preferences
|
||||||
|
csharp_style_conditional_delegate_call = true
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
csharp_prefer_static_local_function = true
|
||||||
|
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
|
||||||
|
csharp_style_prefer_readonly_struct = true
|
||||||
|
csharp_style_prefer_readonly_struct_member = true
|
||||||
|
|
||||||
|
# Code-block preferences
|
||||||
|
csharp_prefer_braces = true:silent
|
||||||
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
|
csharp_style_namespace_declarations = file_scoped:silent
|
||||||
|
csharp_style_prefer_method_group_conversion = true:silent
|
||||||
|
csharp_style_prefer_primary_constructors = true:suggestion
|
||||||
|
csharp_style_prefer_top_level_statements = true:silent
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
csharp_prefer_simple_default_expression = true
|
||||||
|
csharp_style_deconstructed_variable_declaration = true
|
||||||
|
csharp_style_implicit_object_creation_when_type_is_apparent = true
|
||||||
|
csharp_style_inlined_variable_declaration = true
|
||||||
|
csharp_style_prefer_index_operator = true
|
||||||
|
csharp_style_prefer_local_over_anonymous_function = true
|
||||||
|
csharp_style_prefer_null_check_over_type_check = true
|
||||||
|
csharp_style_prefer_range_operator = true
|
||||||
|
csharp_style_prefer_tuple_swap = true
|
||||||
|
csharp_style_prefer_utf8_string_literals = true
|
||||||
|
csharp_style_throw_expression = true
|
||||||
|
csharp_style_unused_value_assignment_preference = discard_variable
|
||||||
|
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||||
|
|
||||||
|
# 'using' directive preferences
|
||||||
|
csharp_using_directive_placement = outside_namespace:silent
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
|
||||||
|
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
|
||||||
|
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
|
||||||
|
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion
|
||||||
|
csharp_style_allow_embedded_statements_on_same_line_experimental = true
|
||||||
|
|
||||||
|
#### C# Formatting Rules ####
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
csharp_new_line_before_catch = true
|
||||||
|
csharp_new_line_before_else = true
|
||||||
|
csharp_new_line_before_finally = true
|
||||||
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
|
csharp_new_line_before_members_in_object_initializers = true
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
csharp_new_line_between_query_expression_clauses = true
|
||||||
|
|
||||||
|
# Indentation preferences
|
||||||
|
csharp_indent_block_contents = true
|
||||||
|
csharp_indent_braces = false
|
||||||
|
csharp_indent_case_contents = true
|
||||||
|
csharp_indent_case_contents_when_block = true
|
||||||
|
csharp_indent_labels = one_less_than_current
|
||||||
|
csharp_indent_switch_labels = true
|
||||||
|
|
||||||
|
# Space preferences
|
||||||
|
csharp_space_after_cast = false
|
||||||
|
csharp_space_after_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_after_comma = true
|
||||||
|
csharp_space_after_dot = false
|
||||||
|
csharp_space_after_keywords_in_control_flow_statements = true
|
||||||
|
csharp_space_after_semicolon_in_for_statement = true
|
||||||
|
csharp_space_around_binary_operators = before_and_after
|
||||||
|
csharp_space_around_declaration_statements = false
|
||||||
|
csharp_space_before_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_before_comma = false
|
||||||
|
csharp_space_before_dot = false
|
||||||
|
csharp_space_before_open_square_brackets = false
|
||||||
|
csharp_space_before_semicolon_in_for_statement = false
|
||||||
|
csharp_space_between_empty_square_brackets = false
|
||||||
|
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||||
|
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||||
|
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_parentheses = false
|
||||||
|
csharp_space_between_square_brackets = false
|
||||||
|
|
||||||
|
# Wrapping preferences
|
||||||
|
csharp_preserve_single_line_blocks = true
|
||||||
|
csharp_preserve_single_line_statements = true
|
||||||
|
|
||||||
|
#### Naming styles ####
|
||||||
|
|
||||||
|
# Naming rules
|
||||||
|
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||||
|
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.severity = warning
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_or_internal_field_should_be_underscore.severity = warning
|
||||||
|
dotnet_naming_rule.private_or_internal_field_should_be_underscore.symbols = private_or_internal_field
|
||||||
|
dotnet_naming_rule.private_or_internal_field_should_be_underscore.style = underscore
|
||||||
|
|
||||||
|
dotnet_naming_rule.private_or_internal_static_field_should_be_sunderscore.severity = warning
|
||||||
|
dotnet_naming_rule.private_or_internal_static_field_should_be_sunderscore.symbols = private_or_internal_static_field
|
||||||
|
dotnet_naming_rule.private_or_internal_static_field_should_be_sunderscore.style = sunderscore
|
||||||
|
|
||||||
|
# Symbol specifications
|
||||||
|
|
||||||
|
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||||
|
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.interface.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
|
||||||
|
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected
|
||||||
|
dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static
|
||||||
|
|
||||||
|
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||||
|
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.types.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||||
|
|
||||||
|
# Naming styles
|
||||||
|
|
||||||
|
dotnet_naming_style.pascal_case.required_prefix =
|
||||||
|
dotnet_naming_style.pascal_case.required_suffix =
|
||||||
|
dotnet_naming_style.pascal_case.word_separator =
|
||||||
|
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||||
|
dotnet_naming_style.begins_with_i.required_suffix =
|
||||||
|
dotnet_naming_style.begins_with_i.word_separator =
|
||||||
|
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.underscore.required_prefix = _
|
||||||
|
dotnet_naming_style.underscore.required_suffix =
|
||||||
|
dotnet_naming_style.underscore.word_separator =
|
||||||
|
dotnet_naming_style.underscore.capitalization = camel_case
|
||||||
|
|
||||||
|
dotnet_naming_style.sunderscore.required_prefix = s_
|
||||||
|
dotnet_naming_style.sunderscore.required_suffix =
|
||||||
|
dotnet_naming_style.sunderscore.word_separator =
|
||||||
|
dotnet_naming_style.sunderscore.capitalization = camel_case
|
||||||
|
|
||||||
|
[*.{cs,vb}]
|
||||||
|
dotnet_style_coalesce_expression = true:suggestion
|
||||||
|
dotnet_style_null_propagation = true:suggestion
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||||
|
dotnet_style_prefer_auto_properties = true:suggestion
|
||||||
|
dotnet_style_object_initializer = true:suggestion
|
||||||
|
dotnet_style_collection_initializer = true:suggestion
|
||||||
|
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||||
|
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||||
|
tab_width = 4
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = crlf
|
||||||
@@ -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 .
|
||||||
|
```
|
||||||
|
|||||||
7
nuget.config
Normal file
7
nuget.config
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
10
src/Vegasco-Web/.dockerignore
Normal file
10
src/Vegasco-Web/.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
5
src/Vegasco-Web/.postcssrc.json
Normal file
5
src/Vegasco-Web/.postcssrc.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"@tailwindcss/postcss": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Vegasco-Web/.vscode/launch.json
vendored
12
src/Vegasco-Web/.vscode/launch.json
vendored
@@ -3,18 +3,12 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "ng serve",
|
"name": "Launch Web (Chrome)",
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "npm: start",
|
"preLaunchTask": "npm: start",
|
||||||
"url": "http://localhost:4200/"
|
"postDebugTask": "Terminate All Tasks",
|
||||||
},
|
"url": "http://localhost:44200/",
|
||||||
{
|
|
||||||
"name": "ng test",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "npm: test",
|
|
||||||
"url": "http://localhost:9876/debug.html"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/Vegasco-Web/.vscode/tasks.json
vendored
21
src/Vegasco-Web/.vscode/tasks.json
vendored
@@ -2,9 +2,22 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Terminate All Tasks",
|
||||||
|
"command": "echo ${input:terminate}",
|
||||||
|
"type": "shell",
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "start",
|
"script": "start",
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"PORT": "44200",
|
||||||
|
"services__Api__https__0": "https://localhost:7098",
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
}
|
||||||
|
},
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"problemMatcher": {
|
"problemMatcher": {
|
||||||
"owner": "typescript",
|
"owner": "typescript",
|
||||||
@@ -38,5 +51,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "terminate",
|
||||||
|
"type": "command",
|
||||||
|
"command": "workbench.action.tasks.terminate",
|
||||||
|
"args": "terminateAll"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/Vegasco-Web/Dockerfile
Normal file
19
src/Vegasco-Web/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
FROM node:latest AS build
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
ARG CONFIGURATION=development
|
||||||
|
WORKDIR /usr/local/app
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm install
|
||||||
|
RUN pnpm "build:$CONFIGURATION"
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
RUN rm /etc/nginx/conf.d/*
|
||||||
|
RUN apk add --update dos2unix
|
||||||
|
ENV DOLLAR=$
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
COPY --from=build /usr/local/app/dist/Vegasco-Web/browser .
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
RUN dos2unix /etc/nginx/nginx.conf
|
||||||
|
COPY webserver.conf.template /etc/nginx/templates/webserver.conf.template
|
||||||
|
RUN dos2unix /etc/nginx/templates/webserver.conf.template
|
||||||
|
EXPOSE 80
|
||||||
@@ -12,6 +12,16 @@ ng serve
|
|||||||
|
|
||||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## API Proxy
|
||||||
|
|
||||||
|
Because the solution utilizes Aspire which injects endpoint references for the API as environment variables, this application uses a proxy to access the API. The proxy is configured in the `proxy.config.js` file which is used in the `serve` section of the `angular.json` file. This makes the dev server provide a proxy when serving the application.
|
||||||
|
|
||||||
|
The environment variables for the API endpoint are named `services__Api__https__0` and `services__Api__http__0` for the https and the http endpoints respectively. If the https endpoint is not configured, the http endpoint is used. At least one of them has to be configured.
|
||||||
|
|
||||||
|
To allow the dev proxy to accept otherwise untrusted server certificates, set `NODE_ENV` to `development`. Otherwise the dev proxy rejects untrusted certificates.
|
||||||
|
|
||||||
|
When deploying the application elsewhere, another proxy has to be configured to provide the same functionality to ensure the application works correctly.
|
||||||
|
|
||||||
## Code scaffolding
|
## Code scaffolding
|
||||||
|
|
||||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular/build:application",
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
"options": {
|
"options": {
|
||||||
|
"outputPath": "dist/Vegasco-Web",
|
||||||
|
"index": "src/index.html",
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js"
|
"zone.js"
|
||||||
@@ -31,7 +33,8 @@
|
|||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
]
|
],
|
||||||
|
"scripts": []
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -47,18 +50,30 @@
|
|||||||
"maximumError": "8kB"
|
"maximumError": "8kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all",
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.production.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.development.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"proxyConfig": "proxy.config.js"
|
"proxyConfig": "proxy.config.js"
|
||||||
},
|
},
|
||||||
@@ -73,10 +88,10 @@
|
|||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular/build:extract-i18n"
|
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular/build:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js",
|
"zone.js",
|
||||||
@@ -92,10 +107,14 @@
|
|||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
]
|
],
|
||||||
|
"scripts": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -3,27 +3,42 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "run-script-os",
|
||||||
"build": "ng build",
|
"start:win32": "ng serve --port %PORT% --configuration development",
|
||||||
|
"start:default": "ng serve --port $PORT --configuration development",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "^20.0.3",
|
"@angular/common": "^19.2.14",
|
||||||
"@angular/compiler": "^20.0.3",
|
"@angular/compiler": "^19.2.14",
|
||||||
"@angular/core": "^20.0.3",
|
"@angular/core": "^19.2.14",
|
||||||
"@angular/forms": "^20.0.3",
|
"@angular/forms": "^19.2.14",
|
||||||
"@angular/platform-browser": "^20.0.3",
|
"@angular/platform-browser": "^19.2.14",
|
||||||
"@angular/router": "^20.0.3",
|
"@angular/router": "^19.2.14",
|
||||||
|
"@ng-icons/core": "^31.4.0",
|
||||||
|
"@ng-icons/material-file-icons": "^31.4.0",
|
||||||
|
"@ng-icons/material-icons": "^31.4.0",
|
||||||
|
"@primeng/themes": "^19.1.3",
|
||||||
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"keycloak-angular": "^19.0.2",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"primeng": "^19.1.3",
|
||||||
"rxjs": "~7.8.2",
|
"rxjs": "~7.8.2",
|
||||||
|
"tailwindcss": "^4.1.10",
|
||||||
|
"tailwindcss-primeui": "^0.6.1",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"zone.js": "~0.15.1"
|
"zone.js": "~0.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^20.0.2",
|
"@angular-devkit/build-angular": "^19.2.15",
|
||||||
"@angular/cli": "^20.0.2",
|
"@angular/cli": "^19.2.15",
|
||||||
"@angular/compiler-cli": "^20.0.3",
|
"@angular/compiler-cli": "^19.2.14",
|
||||||
"@types/jasmine": "~5.1.8",
|
"@types/jasmine": "~5.1.8",
|
||||||
"jasmine-core": "~5.7.1",
|
"jasmine-core": "~5.7.1",
|
||||||
"karma": "~6.4.4",
|
"karma": "~6.4.4",
|
||||||
@@ -31,6 +46,7 @@
|
|||||||
"karma-coverage": "~2.2.1",
|
"karma-coverage": "~2.2.1",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"run-script-os": "^1.1.6",
|
||||||
"typescript": "~5.8.3"
|
"typescript": "~5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5289
src/Vegasco-Web/pnpm-lock.yaml
generated
5289
src/Vegasco-Web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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": "",
|
||||||
|
|||||||
6
src/Vegasco-Web/src/app/api/api-base-path.ts
Normal file
6
src/Vegasco-Web/src/app/api/api-base-path.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { InjectionToken } from "@angular/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base path for all API requests, e.g. when using a proxy on the origin's address.
|
||||||
|
*/
|
||||||
|
export const API_BASE_PATH = new InjectionToken<string>('API_BASE_PATH');
|
||||||
35
src/Vegasco-Web/src/app/api/cars/car-client.ts
Normal file
35
src/Vegasco-Web/src/app/api/cars/car-client.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { API_BASE_PATH } from "../api-base-path";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class CarClient {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly apiBasePath = inject(API_BASE_PATH, { optional: true });
|
||||||
|
|
||||||
|
getAll(): Observable<GetCarsResponse> {
|
||||||
|
return this.http.get<GetCarsResponse>(`${this.apiBasePath}/v1/cars`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSingle(id: string): Observable<Car> {
|
||||||
|
return this.http.get<Car>(`${this.apiBasePath}/v1/cars/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(request: CreateCarRequest): Observable<Car> {
|
||||||
|
return this.http.post<Car>(`${this.apiBasePath}/v1/cars`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, request: UpdateCarRequest): Observable<Car> {
|
||||||
|
return this.http.put<Car>(`${this.apiBasePath}/v1/cars/${id}`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: string): Observable<void> {
|
||||||
|
return this.http.delete(`${this.apiBasePath}/v1/cars/${id}`)
|
||||||
|
.pipe(
|
||||||
|
map(_ => undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/Vegasco-Web/src/app/api/cars/car.ts
Normal file
4
src/Vegasco-Web/src/app/api/cars/car.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
interface Car {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
3
src/Vegasco-Web/src/app/api/cars/create-car-request.ts
Normal file
3
src/Vegasco-Web/src/app/api/cars/create-car-request.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
interface CreateCarRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
3
src/Vegasco-Web/src/app/api/cars/get-cars-response.ts
Normal file
3
src/Vegasco-Web/src/app/api/cars/get-cars-response.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
interface GetCarsResponse {
|
||||||
|
cars: Car[];
|
||||||
|
}
|
||||||
3
src/Vegasco-Web/src/app/api/cars/update-car-request.ts
Normal file
3
src/Vegasco-Web/src/app/api/cars/update-car-request.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
interface UpdateCarRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { inject, Injectable } from '@angular/core';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { API_BASE_PATH } from '../api-base-path';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ConsumptionClient {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly apiBasePath = inject(API_BASE_PATH, { optional: true });
|
||||||
|
|
||||||
|
getAll(): Observable<GetConsumptionEntriesResponse> {
|
||||||
|
return this.http.get<GetConsumptionEntriesResponse>(`${this.apiBasePath}/v1/consumptions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSingle(id: string): Observable<ConsumptionEntry> {
|
||||||
|
return this.http.get<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(request: CreateConsumptionEntry): Observable<ConsumptionEntry> {
|
||||||
|
return this.http.post<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, request: UpdateConsumptionEntry): Observable<ConsumptionEntry> {
|
||||||
|
return this.http.put<ConsumptionEntry>(`${this.apiBasePath}/v1/consumptions/${id}`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: string): Observable<void> {
|
||||||
|
return this.http.delete(`${this.apiBasePath}/v1/consumptions/${id}`)
|
||||||
|
.pipe(
|
||||||
|
map(_ => undefined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
interface ConsumptionEntry {
|
||||||
|
id: string;
|
||||||
|
dateTime: string;
|
||||||
|
distance: number;
|
||||||
|
amount: number;
|
||||||
|
carId: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
interface CreateConsumptionEntry {
|
||||||
|
dateTime: string;
|
||||||
|
distance: number;
|
||||||
|
amount: number;
|
||||||
|
carId: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
interface GetConsumptionEntriesEntry {
|
||||||
|
id: string;
|
||||||
|
dateTime: string;
|
||||||
|
distance: number;
|
||||||
|
amount: number;
|
||||||
|
car: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
literPer100Km: number | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
interface GetConsumptionEntriesResponse {
|
||||||
|
consumptions: GetConsumptionEntriesEntry[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
interface UpdateConsumptionEntry {
|
||||||
|
dateTime: string;
|
||||||
|
distance: number;
|
||||||
|
amount: number;
|
||||||
|
carId: string;
|
||||||
|
}
|
||||||
@@ -1,14 +1,30 @@
|
|||||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { provideRouter } from '@angular/router';
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
|
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
||||||
|
import Lara from '@primeng/themes/lara';
|
||||||
|
import { includeBearerTokenInterceptor } from 'keycloak-angular';
|
||||||
|
import { providePrimeNG } from 'primeng/config';
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import {provideHttpClient} from '@angular/common/http';
|
import { provideKeycloakAngular } from './auth/auth.config';
|
||||||
|
import {API_BASE_PATH} from './api/api-base-path';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideKeycloakAngular(),
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes),
|
provideRouter(routes, withComponentInputBinding()),
|
||||||
provideHttpClient(),
|
provideHttpClient(withInterceptors([includeBearerTokenInterceptor])),
|
||||||
|
provideAnimationsAsync(),
|
||||||
|
providePrimeNG({
|
||||||
|
theme: {
|
||||||
|
preset: Lara
|
||||||
|
},
|
||||||
|
ripple: true
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
provide: API_BASE_PATH,
|
||||||
|
useValue: '/api'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,366 +1,21 @@
|
|||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * to get started with your project! * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
--bright-blue: oklch(51.01% 0.274 263.83);
|
|
||||||
--electric-violet: oklch(53.18% 0.28 296.97);
|
|
||||||
--french-violet: oklch(47.66% 0.246 305.88);
|
|
||||||
--vivid-pink: oklch(69.02% 0.277 332.77);
|
|
||||||
--hot-red: oklch(61.42% 0.238 15.34);
|
|
||||||
--orange-red: oklch(63.32% 0.24 31.68);
|
|
||||||
|
|
||||||
--gray-900: oklch(19.37% 0.006 300.98);
|
|
||||||
--gray-700: oklch(36.98% 0.014 302.71);
|
|
||||||
--gray-400: oklch(70.9% 0.015 304.04);
|
|
||||||
|
|
||||||
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
var(--orange-red) 0%,
|
|
||||||
var(--vivid-pink) 50%,
|
|
||||||
var(--electric-violet) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
var(--orange-red) 0%,
|
|
||||||
var(--vivid-pink) 50%,
|
|
||||||
var(--electric-violet) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
|
|
||||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
||||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol";
|
|
||||||
box-sizing: border-box;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.125rem;
|
|
||||||
color: var(--gray-900);
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 100%;
|
|
||||||
letter-spacing: -0.125rem;
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
||||||
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol";
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--gray-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
box-sizing: inherit;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.angular-logo {
|
|
||||||
max-width: 9.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 700px;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content h1 {
|
|
||||||
margin-top: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content p {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
width: 1px;
|
|
||||||
background: var(--red-to-pink-to-purple-vertical-gradient);
|
|
||||||
margin-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
|
|
||||||
color: var(--pill-accent);
|
|
||||||
padding-inline: 0.75rem;
|
|
||||||
padding-block: 0.375rem;
|
|
||||||
border-radius: 2.75rem;
|
|
||||||
border: 0;
|
|
||||||
transition: background 0.3s ease;
|
|
||||||
font-family: var(--inter-font);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.4rem;
|
|
||||||
letter-spacing: -0.00875rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill:hover {
|
|
||||||
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group .pill:nth-child(6n + 1) {
|
|
||||||
--pill-accent: var(--bright-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group .pill:nth-child(6n + 2) {
|
|
||||||
--pill-accent: var(--french-violet);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group .pill:nth-child(6n + 3),
|
|
||||||
.pill-group .pill:nth-child(6n + 4),
|
|
||||||
.pill-group .pill:nth-child(6n + 5) {
|
|
||||||
--pill-accent: var(--hot-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pill-group svg {
|
|
||||||
margin-inline-start: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.73rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links path {
|
|
||||||
transition: fill 0.3s ease;
|
|
||||||
fill: var(--gray-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
td, th {
|
|
||||||
padding: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links a:hover svg path {
|
|
||||||
fill: var(--gray-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 650px) {
|
|
||||||
.content {
|
|
||||||
flex-direction: column;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
|
||||||
margin-block: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="content">
|
<header class="h-12 bg-primary text-primary-contrast">
|
||||||
<div class="left-side">
|
<div class="header max-content-width mx-auto flex items-center justify-between">
|
||||||
<svg
|
<a routerLink="/" class="reset cursor-pointer">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
Vegasco
|
||||||
viewBox="0 0 982 239"
|
</a>
|
||||||
fill="none"
|
<span class="flex items-center gap-4">
|
||||||
class="angular-logo"
|
<a routerLink="/entries" class="reset cursor-pointer">
|
||||||
>
|
Einträge
|
||||||
<g clip-path="url(#a)">
|
</a>
|
||||||
<path
|
<a routerLink="/cars" class="reset cursor-pointer">
|
||||||
fill="url(#b)"
|
Autos
|
||||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
</a>
|
||||||
/>
|
</span>
|
||||||
<path
|
|
||||||
fill="url(#c)"
|
|
||||||
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<radialGradient
|
|
||||||
id="c"
|
|
||||||
cx="0"
|
|
||||||
cy="0"
|
|
||||||
r="1"
|
|
||||||
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stop-color="#FF41F8"/>
|
|
||||||
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5"/>
|
|
||||||
<stop offset="1" stop-color="#FF41F8" stop-opacity="0"/>
|
|
||||||
</radialGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="b"
|
|
||||||
x1="0"
|
|
||||||
x2="982"
|
|
||||||
y1="192"
|
|
||||||
y2="192"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stop-color="#F0060B"/>
|
|
||||||
<stop offset="0" stop-color="#F0070C"/>
|
|
||||||
<stop offset=".526" stop-color="#CC26D5"/>
|
|
||||||
<stop offset="1" stop-color="#7702FF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="a">
|
|
||||||
<path fill="#fff" d="M0 0h982v239H0z"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
<h1>Hello, {{ title }}</h1>
|
|
||||||
<p>Congratulations! Your app is running. 🎉</p>
|
|
||||||
<div>
|
|
||||||
@if (serverInfo$ | async; as serverInfo) {
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Version</th>
|
|
||||||
<th>Commit ID</th>
|
|
||||||
<th>Commit Date</th>
|
|
||||||
<th>Environment</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>{{ serverInfo.fullVersion }}</td>
|
|
||||||
<td>{{ serverInfo.commitId }}</td>
|
|
||||||
<td>{{ serverInfo.commitDate | date:"dd.MM.yyyy HH:mm:ss" }}</td>
|
|
||||||
<td>{{ serverInfo.environment }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="divider" role="separator" aria-label="Divider"></div>
|
|
||||||
<div class="right-side">
|
|
||||||
<div class="pill-group">
|
|
||||||
@for (item of [
|
|
||||||
{title: 'Explore the Docs', link: 'https://angular.dev'},
|
|
||||||
{title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials'},
|
|
||||||
{title: 'CLI Docs', link: 'https://angular.dev/tools/cli'},
|
|
||||||
{title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service'},
|
|
||||||
{title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools'},
|
|
||||||
]; track item.title) {
|
|
||||||
<a
|
|
||||||
class="pill"
|
|
||||||
[href]="item.link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<span>{{ item.title }}</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
width="14"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="social-links">
|
|
||||||
<a
|
|
||||||
href="https://github.com/angular/angular"
|
|
||||||
aria-label="Github"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="25"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 25 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Github"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://twitter.com/angular"
|
|
||||||
aria-label="Twitter"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Twitter"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
|
|
||||||
aria-label="Youtube"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="29"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 29 20"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
alt="Youtube"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="content max-content-width mx-auto">
|
||||||
|
<p-toast />
|
||||||
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
|
|
||||||
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
|
|
||||||
|
|
||||||
|
|
||||||
<router-outlet/>
|
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
export const routes: Routes = [];
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'entries',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'entries',
|
||||||
|
loadChildren: () => import('./modules/entries/entries.routes').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'cars',
|
||||||
|
loadChildren: () => import('./modules/cars/cars.routes').then(m => m.routes)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,33 +1,15 @@
|
|||||||
import {Component, inject} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {RouterOutlet} from '@angular/router';
|
import { RouterLink, RouterOutlet } from '@angular/router';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import { MessageService } from 'primeng/api';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import {AsyncPipe, DatePipe} from '@angular/common';
|
|
||||||
import {tap} from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [AsyncPipe, DatePipe, RouterOutlet],
|
imports: [RouterLink, RouterOutlet, ToastModule],
|
||||||
|
providers: [MessageService],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss'
|
styleUrl: './app.scss'
|
||||||
})
|
})
|
||||||
export class App {
|
export class App {
|
||||||
protected title = 'Vegasco-Web';
|
|
||||||
|
|
||||||
private readonly http = inject(HttpClient);
|
|
||||||
|
|
||||||
protected readonly serverInfo$;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.serverInfo$ = this.http.get<ServerInfo>('/api/v1/info/server')
|
|
||||||
.pipe(takeUntilDestroyed());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerInfo {
|
|
||||||
fullVersion: string;
|
|
||||||
commitId: string;
|
|
||||||
commitDate: string;
|
|
||||||
environment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
45
src/Vegasco-Web/src/app/auth/auth.config.ts
Normal file
45
src/Vegasco-Web/src/app/auth/auth.config.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { environment } from '@vegasco-web/environments/environment';
|
||||||
|
import {
|
||||||
|
AutoRefreshTokenService,
|
||||||
|
createInterceptorCondition,
|
||||||
|
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
||||||
|
IncludeBearerTokenCondition,
|
||||||
|
provideKeycloak,
|
||||||
|
UserActivityService,
|
||||||
|
withAutoRefreshToken
|
||||||
|
} from 'keycloak-angular';
|
||||||
|
|
||||||
|
const serverHostBearerInterceptorCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
|
||||||
|
// The API is consumed through a proxy running on the same origin as the application.
|
||||||
|
// This means that the interceptor should include the bearer token for requests to the same origin
|
||||||
|
// which includes requests starting to / which implicitly sends the request to the same origin.
|
||||||
|
urlPattern: new RegExp(`^(${window.origin}|/)`)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const provideKeycloakAngular = () =>
|
||||||
|
provideKeycloak({
|
||||||
|
config: {
|
||||||
|
url: environment.keycloak.host,
|
||||||
|
realm: environment.keycloak.realm,
|
||||||
|
clientId: environment.keycloak.clientId,
|
||||||
|
},
|
||||||
|
initOptions: {
|
||||||
|
onLoad: 'login-required',
|
||||||
|
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
|
||||||
|
redirectUri: window.location.origin + '/',
|
||||||
|
checkLoginIframe: false,
|
||||||
|
},
|
||||||
|
features: [
|
||||||
|
withAutoRefreshToken({
|
||||||
|
onInactivityTimeout: 'login',
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AutoRefreshTokenService,
|
||||||
|
UserActivityService,
|
||||||
|
{
|
||||||
|
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
||||||
|
useValue: [serverHostBearerInterceptorCondition]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
17
src/Vegasco-Web/src/app/modules/cars/cars.routes.ts
Normal file
17
src/Vegasco-Web/src/app/modules/cars/cars.routes.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Routes } from "@angular/router";
|
||||||
|
import { CarsComponent } from "./cars/cars.component";
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CarsComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
loadComponent: () => import('./edit-car/edit-car.component').then(m => m.EditCarComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
loadComponent: () => import('./edit-car/edit-car.component').then(m => m.EditCarComponent)
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<section>
|
||||||
|
<p-scrollTop />
|
||||||
|
<div class="mb-4 flex gap-2 md:justify-end">
|
||||||
|
<div>
|
||||||
|
<p-button label="Erstellen" routerLink="/cars/create">
|
||||||
|
<ng-icon name="matAddSharp"></ng-icon>
|
||||||
|
</p-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (nonDeletedCars$ | async; as cars) {
|
||||||
|
<p-dataView
|
||||||
|
[value]="cars"
|
||||||
|
[paginator]="true"
|
||||||
|
[rows]="25"
|
||||||
|
[rowsPerPageOptions]="[10, 25, 50, 100]"
|
||||||
|
[pageLinks]="0"
|
||||||
|
[showCurrentPageReport]="true"
|
||||||
|
currentPageReportTemplate="{currentPage} / {totalPages}"
|
||||||
|
layout="list">
|
||||||
|
<ng-template #list let-cars>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
@for (car of cars; track car.id) {
|
||||||
|
<app-car-card [car]="car"
|
||||||
|
(carDeleted)="onCarDeleted($event)" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</p-dataView>
|
||||||
|
} @else {
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
@for (_ of skeletonsIterationSource; track $index) {
|
||||||
|
<p-skeleton height="4rem" styleClass="mb-2" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
th, td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
127
src/Vegasco-Web/src/app/modules/cars/cars/cars.component.ts
Normal file
127
src/Vegasco-Web/src/app/modules/cars/cars/cars.component.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, DestroyRef, inject } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
matAddSharp,
|
||||||
|
} from '@ng-icons/material-icons/sharp';
|
||||||
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { DataViewModule } from 'primeng/dataview';
|
||||||
|
import { ScrollTopModule } from 'primeng/scrolltop';
|
||||||
|
import { SelectModule } from 'primeng/select';
|
||||||
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
catchError,
|
||||||
|
combineLatest,
|
||||||
|
EMPTY,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
throwError
|
||||||
|
} from 'rxjs';
|
||||||
|
import { CarCardComponent } from './components/car-card/car-card.component';
|
||||||
|
import { SelectedCarService } from '@vegasco-web/modules/entries/services/selected-car.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-entries',
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
ButtonModule,
|
||||||
|
CommonModule,
|
||||||
|
DataViewModule,
|
||||||
|
CarCardComponent,
|
||||||
|
NgIconComponent,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterLink,
|
||||||
|
ScrollTopModule,
|
||||||
|
SelectModule,
|
||||||
|
SkeletonModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
matAddSharp,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
templateUrl: './cars.component.html',
|
||||||
|
styleUrl: './cars.component.scss'
|
||||||
|
})
|
||||||
|
export class CarsComponent {
|
||||||
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly selectedCarService = inject(SelectedCarService);
|
||||||
|
|
||||||
|
protected readonly nonDeletedCars$: Observable<Car[]>;
|
||||||
|
|
||||||
|
protected readonly skeletonsIterationSource = Array(10).fill(0);
|
||||||
|
|
||||||
|
private readonly deletedCars$ = new BehaviorSubject(<string[]>[]);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const cars$ = this.carClient.getAll()
|
||||||
|
.pipe(
|
||||||
|
map(response => response.cars),
|
||||||
|
map((cars) => cars
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.nonDeletedCars$ = combineLatest([
|
||||||
|
cars$,
|
||||||
|
this.deletedCars$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
map(([cars, deletedCars]) => cars.filter(car => !deletedCars.includes(car.id))),
|
||||||
|
catchError((error) => this.handleGetCarsError(error)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCarDeleted(car: Car): void {
|
||||||
|
this.deletedCars$.next([...this.deletedCars$.value, car.id]);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Auto gelöscht',
|
||||||
|
detail: 'Das Auto wurde erfolgreich gelöscht.',
|
||||||
|
});
|
||||||
|
this.resetSelectedCarIfDeleted(car);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetSelectedCarIfDeleted(car: Car) {
|
||||||
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||||
|
if (selectedCarId === car.id) {
|
||||||
|
this.selectedCarService.setSelectedCarId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetCarsError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => new Error('An unexpected error occurred'));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<p-confirmDialog></p-confirmDialog>
|
||||||
|
|
||||||
|
<div class="flex rounded-border shadow">
|
||||||
|
<div class="grow p-4 pos-relative edit-button" (click)="navigateToEdit()" role="button"
|
||||||
|
aria-roledescription="Bearbeite diesen Eintrag">
|
||||||
|
<div class="grid grid-cols-4 gap-4">
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matDirectionsCarOutline" />
|
||||||
|
<div>{{ car().name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-red-500 text-white rounded-r text-center flex flex-col justify-center">
|
||||||
|
<button type="button" title="Löschen" class="reset cursor-pointer primary-color-text p-4 h-full rounded-r"
|
||||||
|
(click)="confirmDeleteCar()">
|
||||||
|
<ng-icon name="matDeleteSharp"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.edit-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, DestroyRef, inject, input, output } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
matDirectionsCarOutline,
|
||||||
|
} from '@ng-icons/material-icons/outline';
|
||||||
|
import {
|
||||||
|
matDeleteSharp
|
||||||
|
} from '@ng-icons/material-icons/sharp';
|
||||||
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { CardModule } from 'primeng/card';
|
||||||
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
|
import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-car-card',
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CardModule,
|
||||||
|
ConfirmDialogModule,
|
||||||
|
NgIconComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
matDeleteSharp,
|
||||||
|
matDirectionsCarOutline,
|
||||||
|
}),
|
||||||
|
ConfirmationService,
|
||||||
|
],
|
||||||
|
templateUrl: './car-card.component.html',
|
||||||
|
styleUrl: './car-card.component.scss'
|
||||||
|
})
|
||||||
|
export class CarCardComponent {
|
||||||
|
readonly car = input.required<Car>();
|
||||||
|
|
||||||
|
readonly carDeleted = output<Car>();
|
||||||
|
|
||||||
|
private readonly routingService = inject(RoutingService);
|
||||||
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly confirmationService = inject(ConfirmationService);
|
||||||
|
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
async navigateToEdit(): Promise<void> {
|
||||||
|
await this.routingService.navigateToEditCar(this.car().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteCar(): void {
|
||||||
|
this.confirmationService.confirm({
|
||||||
|
closeOnEscape: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
header: 'Bist du sicher?',
|
||||||
|
message: `Möchtest du das Auto "${this.car().name}" wirklich löschen?`,
|
||||||
|
acceptButtonProps: {
|
||||||
|
label: 'Löschen',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
rejectButtonProps: {
|
||||||
|
label: 'Abbrechen',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
accept: () => this.deleteCar(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCar(): void {
|
||||||
|
this.carClient.delete(this.car().id)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
tap(() => this.carDeleted.emit(this.car())),
|
||||||
|
catchError((error) => this.handleError(error)),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Löschen des Autos ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 400:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Clientfehler',
|
||||||
|
detail:
|
||||||
|
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Löschen des Autos hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<span class="required">*</span>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-required-marker',
|
||||||
|
imports: [
|
||||||
|
],
|
||||||
|
templateUrl: './required-marker.component.html',
|
||||||
|
styleUrl: './required-marker.component.scss'
|
||||||
|
})
|
||||||
|
export class RequiredMarkerComponent {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@if (!isCarDataLoaded()) {
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<form [formGroup]="formGroup" class="flex flex-col gap-4" (ngSubmit)="onSubmit()">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label [for]="formFieldNames.name">
|
||||||
|
Name
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
placeholder="Name eingeben"
|
||||||
|
type="text"
|
||||||
|
pInputText
|
||||||
|
[formControlName]="formFieldNames.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<p-button type="button" label="Abbrechen" (click)="navigateToOverviewPage()" severity="warn" />
|
||||||
|
<p-button type="submit" label="Abschicken" severity="success" [disabled]="formGroup.invalid" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, DestroyRef, inject, input, OnInit, signal } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { ChipModule } from 'primeng/chip';
|
||||||
|
import { DatePickerModule } from 'primeng/datepicker';
|
||||||
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
|
import { InputGroupModule } from 'primeng/inputgroup';
|
||||||
|
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
|
||||||
|
import { InputNumberModule } from 'primeng/inputnumber';
|
||||||
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
|
import { MultiSelectModule } from 'primeng/multiselect';
|
||||||
|
import { SelectModule } from 'primeng/select';
|
||||||
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
|
import { catchError, EMPTY, Observable, switchMap, tap, throwError } from 'rxjs';
|
||||||
|
import { RequiredMarkerComponent } from './components/required-marker.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-entry',
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
ChipModule,
|
||||||
|
DatePickerModule,
|
||||||
|
FloatLabelModule,
|
||||||
|
InputGroupAddonModule,
|
||||||
|
InputGroupModule,
|
||||||
|
InputNumberModule,
|
||||||
|
InputTextModule,
|
||||||
|
MultiSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RequiredMarkerComponent,
|
||||||
|
SelectModule,
|
||||||
|
SkeletonModule,
|
||||||
|
],
|
||||||
|
templateUrl: './edit-car.component.html',
|
||||||
|
styleUrl: './edit-car.component.scss'
|
||||||
|
})
|
||||||
|
export class EditCarComponent implements OnInit {
|
||||||
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly routingService = inject(RoutingService);
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
|
||||||
|
protected readonly id = input<string | undefined>(undefined);
|
||||||
|
|
||||||
|
protected readonly today = new Date();
|
||||||
|
|
||||||
|
protected readonly formFieldNames = {
|
||||||
|
name: 'name',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
protected readonly formGroup = new FormGroup({
|
||||||
|
[this.formFieldNames.name]: new FormControl<string | null>({ value: null, disabled: true }, [Validators.required]),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected readonly isCarDataLoaded = signal(false);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadEntryDetailsAndEnableControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadEntryDetailsAndEnableControls() {
|
||||||
|
const carId = this.id();
|
||||||
|
|
||||||
|
if (carId === undefined || carId === null) {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isCarDataLoaded.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.carClient
|
||||||
|
.getSingle(carId)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleGetError(error)),
|
||||||
|
tap((car) => {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
[this.formFieldNames.name]: car.name,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isCarDataLoaded.set(true);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private enableFormControls(): void {
|
||||||
|
for (const controlName of Object.values(this.formFieldNames)) {
|
||||||
|
const control = this.formGroup.get(controlName);
|
||||||
|
if (control) {
|
||||||
|
control.enable();
|
||||||
|
} else {
|
||||||
|
console.warn(`Form control '${controlName}' not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToOverviewPage(): Promise<void> {
|
||||||
|
await this.routingService.navigateToCars();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var carId = this.id();
|
||||||
|
if (carId === undefined || carId === null) {
|
||||||
|
this.createCar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCar(carId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormData() {
|
||||||
|
return {
|
||||||
|
name: this.formGroup.controls[this.formFieldNames.name].value!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createCar() {
|
||||||
|
var request: CreateCarRequest = this.getFormData();
|
||||||
|
this.carClient.create(request)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleCreateOrUpdateError(error, false)),
|
||||||
|
switchMap(() => this.routingService.navigateToCars())
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCar(id: string) {
|
||||||
|
var request: UpdateCarRequest = this.getFormData();
|
||||||
|
this.carClient.update(id, request)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleCreateOrUpdateError(error, true)),
|
||||||
|
switchMap(() => this.routingService.navigateToCars())
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen des Autos ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen des Autos hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreateOrUpdateError(error: unknown, isUpdate: boolean): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = isUpdate ? 'Aktualisieren' : 'Erstellen';
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
`Beim ${action} des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 400:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Clientfehler',
|
||||||
|
detail:
|
||||||
|
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 409:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Konflikt',
|
||||||
|
detail:
|
||||||
|
'Es existiert bereits ein Auto mit diesem Namen. Bitte wähle einen anderen Namen.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
`Beim ${action} des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<span class="required">*</span>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-required-marker',
|
||||||
|
imports: [
|
||||||
|
],
|
||||||
|
templateUrl: './required-marker.component.html',
|
||||||
|
styleUrl: './required-marker.component.scss'
|
||||||
|
})
|
||||||
|
export class RequiredMarkerComponent {
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
@if (isLoading()) {
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<form [formGroup]="formGroup" class="flex flex-col gap-4" (ngSubmit)="onSubmit()">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label [for]="formFieldNames.car">
|
||||||
|
Auto
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
|
@if (cars(); as cars) {
|
||||||
|
<p-select
|
||||||
|
[options]="cars"
|
||||||
|
placeholder="Auto auswählen"
|
||||||
|
[formControlName]="formFieldNames.car"
|
||||||
|
optionLabel="name"
|
||||||
|
[inputId]="formFieldNames.car"
|
||||||
|
styleClass="w-full" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<label [for]="formFieldNames.date">
|
||||||
|
Datum
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p-datepicker [iconDisplay]="'input'"
|
||||||
|
[firstDayOfWeek]="1"
|
||||||
|
placeholder="Datum auswählen"
|
||||||
|
[showIcon]="true"
|
||||||
|
[maxDate]="today"
|
||||||
|
[defaultDate]="today"
|
||||||
|
[inputId]="formFieldNames.date"
|
||||||
|
[formControlName]="formFieldNames.date"
|
||||||
|
styleClass="w-full"
|
||||||
|
dateFormat="dd.mm.yy" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label [for]="formFieldNames.mileage">
|
||||||
|
Kilometerstand
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
|
<p-inputGroup>
|
||||||
|
<input
|
||||||
|
id="mileage"
|
||||||
|
placeholder="Kilometerstand eingeben"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
pInputText
|
||||||
|
[formControlName]="formFieldNames.mileage" />
|
||||||
|
<p-inputGroupAddon>km</p-inputGroupAddon>
|
||||||
|
</p-inputGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label [for]="formFieldNames.amount">
|
||||||
|
Menge
|
||||||
|
<app-required-marker />
|
||||||
|
</label>
|
||||||
|
<p-inputGroup>
|
||||||
|
<input
|
||||||
|
id="amount"
|
||||||
|
placeholder="Menge eingeben"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
pInputText
|
||||||
|
[formControlName]="formFieldNames.amount" />
|
||||||
|
<p-inputGroupAddon>ℓ</p-inputGroupAddon>
|
||||||
|
</p-inputGroup>
|
||||||
|
</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,293 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, computed, DestroyRef, inject, input, OnInit, signal, Signal } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||||
|
import { FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
|
||||||
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
|
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
||||||
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { 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, combineLatest, EMPTY, filter, map, Observable, switchMap, tap, throwError } from 'rxjs';
|
||||||
|
import { RequiredMarkerComponent } from './components/required-marker.component';
|
||||||
|
import { SelectedCarService } from '../services/selected-car.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-entry',
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
ChipModule,
|
||||||
|
DatePickerModule,
|
||||||
|
FloatLabelModule,
|
||||||
|
InputGroupAddonModule,
|
||||||
|
InputGroupModule,
|
||||||
|
InputNumberModule,
|
||||||
|
InputTextModule,
|
||||||
|
MultiSelectModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RequiredMarkerComponent,
|
||||||
|
SelectModule,
|
||||||
|
SkeletonModule,
|
||||||
|
],
|
||||||
|
templateUrl: './edit-entry.component.html',
|
||||||
|
styleUrl: './edit-entry.component.scss'
|
||||||
|
})
|
||||||
|
export class EditEntryComponent implements OnInit {
|
||||||
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
|
private readonly routingService = inject(RoutingService);
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly selectedCarService = inject(SelectedCarService);
|
||||||
|
|
||||||
|
protected readonly id = input<string | undefined>(undefined);
|
||||||
|
|
||||||
|
protected readonly today = new Date();
|
||||||
|
|
||||||
|
protected readonly formFieldNames = {
|
||||||
|
car: 'car',
|
||||||
|
date: 'date',
|
||||||
|
mileage: 'mileage',
|
||||||
|
amount: 'amount',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
protected readonly formGroup = new FormGroup({
|
||||||
|
[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.dateTimeGreaterThanOrEqualToTodayValidator]),
|
||||||
|
[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)]),
|
||||||
|
});
|
||||||
|
|
||||||
|
private readonly cars$: Observable<Car[]>;
|
||||||
|
protected readonly cars: Signal<Car[] | undefined>;
|
||||||
|
|
||||||
|
private readonly isEntryDataLoaded = signal(false);
|
||||||
|
|
||||||
|
protected readonly isLoading = computed(() => {
|
||||||
|
var cars = this.cars();
|
||||||
|
var isEntryDataLoaded = this.isEntryDataLoaded();
|
||||||
|
return cars === undefined || !isEntryDataLoaded;
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cars$ = this.carClient
|
||||||
|
.getAll()
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
map(response => response.cars
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
|
tap(cars => {
|
||||||
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||||
|
|
||||||
|
if (selectedCarId === null) {
|
||||||
|
const firstCar = cars[0];
|
||||||
|
this.formGroup.controls[this.formFieldNames.car].setValue(firstCar);
|
||||||
|
this.selectedCarService.setSelectedCarId(firstCar?.id ?? null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCar = cars.find(car => car.id === selectedCarId);
|
||||||
|
this.formGroup.controls[this.formFieldNames.car].setValue(selectedCar ?? null);
|
||||||
|
this.selectedCarService.setSelectedCarId(selectedCar?.id ?? null);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.cars = toSignal(this.cars$);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadEntryDetailsAndEnableControls();
|
||||||
|
|
||||||
|
this.formGroup.controls[this.formFieldNames.car]
|
||||||
|
.valueChanges
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
tap((car) => {
|
||||||
|
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadEntryDetailsAndEnableControls() {
|
||||||
|
const entryId = this.id();
|
||||||
|
|
||||||
|
if (entryId === undefined || entryId === null) {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isEntryDataLoaded.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumption$ = this.consumptionClient
|
||||||
|
.getSingle(entryId);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
consumption$, this.cars$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
filter(([_, cars]) => cars !== undefined),
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleGetError(error)),
|
||||||
|
tap(([consumption, cars]) => {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
[this.formFieldNames.car]: cars!.find(c => c.id === consumption.carId) ?? null,
|
||||||
|
[this.formFieldNames.date]: new Date(consumption.dateTime),
|
||||||
|
[this.formFieldNames.mileage]: consumption.distance,
|
||||||
|
[this.formFieldNames.amount]: consumption.amount,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isEntryDataLoaded.set(true);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enableFormControls(): void {
|
||||||
|
for (const controlName of Object.values(this.formFieldNames)) {
|
||||||
|
const control = this.formGroup.get(controlName);
|
||||||
|
if (control) {
|
||||||
|
control.enable();
|
||||||
|
} else {
|
||||||
|
console.warn(`Form control '${controlName}' not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToOverviewPage(): Promise<void> {
|
||||||
|
await this.routingService.navigateToEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryId = this.id();
|
||||||
|
if (entryId === undefined || entryId === null) {
|
||||||
|
this.createEntry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateEntry(entryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormData() {
|
||||||
|
var dateTime = new Date((this.formGroup.controls[this.formFieldNames.date].value ?? new Date).setHours(0, 0, 0, 0));
|
||||||
|
|
||||||
|
return {
|
||||||
|
carId: this.formGroup.controls[this.formFieldNames.car].value!.id,
|
||||||
|
dateTime: dateTime.toISOString(),
|
||||||
|
distance: this.formGroup.controls[this.formFieldNames.mileage].value!,
|
||||||
|
amount: this.formGroup.controls[this.formFieldNames.amount].value!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createEntry() {
|
||||||
|
var request: CreateConsumptionEntry = this.getFormData();
|
||||||
|
this.consumptionClient.create(request)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleCreateOrUpdateError(error)),
|
||||||
|
switchMap(() => this.routingService.navigateToEntries())
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEntry(id: string) {
|
||||||
|
var request: UpdateConsumptionEntry = this.getFormData();
|
||||||
|
this.consumptionClient.update(id, request)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleCreateOrUpdateError(error)),
|
||||||
|
switchMap(() => this.routingService.navigateToEntries())
|
||||||
|
)
|
||||||
|
.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 Erstellen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Erstellen des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreateOrUpdateError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Erstellen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 400:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Clientfehler',
|
||||||
|
detail:
|
||||||
|
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Erstellen des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private dateTimeGreaterThanOrEqualToTodayValidator(control: FormControl<Date>): ValidationErrors | null {
|
||||||
|
const tomorrowStartOfDay = dayjs().add(1, 'day').startOf('day');
|
||||||
|
const controlDate = dayjs(control.value);
|
||||||
|
|
||||||
|
if (controlDate.isBefore(tomorrowStartOfDay)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dateTimeGreaterThanOrEqualToToday: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Vegasco-Web/src/app/modules/entries/entries.routes.ts
Normal file
17
src/Vegasco-Web/src/app/modules/entries/entries.routes.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Routes } from "@angular/router";
|
||||||
|
import { EntriesComponent } from "./entries/entries.component";
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: EntriesComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
loadComponent: () => import('./edit-entry/edit-entry.component').then(m => m.EditEntryComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
loadComponent: () => import('./edit-entry/edit-entry.component').then(m => m.EditEntryComponent)
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<p-confirmDialog></p-confirmDialog>
|
||||||
|
|
||||||
|
<div class="flex rounded-border shadow">
|
||||||
|
<div class="grow p-4 pos-relative edit-button" (click)="navigateToEdit()" role="button"
|
||||||
|
aria-roledescription="Bearbeite diesen Eintrag">
|
||||||
|
<div class="grid grid-cols-4 gap-4">
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matCalendarMonthSharp" />
|
||||||
|
<div>{{ entry().dateTime | date:"dd.MM.yyyy" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matStraightenSharp" />
|
||||||
|
<div>{{entry().distance }} km</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matLocalGasStationSharp" />
|
||||||
|
<div>{{entry().amount }} ℓ</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||||
|
@if (formattedLiterPer100Km(); as formattedLiterPer100Km) {
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<ng-icon name="matSpeedSharp" />
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{{ formattedLiterPer100Km }}
|
||||||
|
<app-fraction numerator="ℓ" [denominator]="'100km'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-red-500 text-white rounded-r text-center flex flex-col justify-center">
|
||||||
|
|
||||||
|
<button type="button" title="Löschen" class="reset cursor-pointer primary-color-text p-4 h-full rounded-r"
|
||||||
|
(click)="confirmDeleteEntry()">
|
||||||
|
<ng-icon name="matDeleteSharp"></ng-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.edit-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { DatePipe } from '@angular/common';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, computed, DestroyRef, inject, input, output } from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
matCalendarMonthSharp,
|
||||||
|
matDeleteSharp,
|
||||||
|
matLocalGasStationSharp,
|
||||||
|
matSpeedSharp,
|
||||||
|
matStraightenSharp,
|
||||||
|
} from '@ng-icons/material-icons/sharp';
|
||||||
|
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
||||||
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ButtonModule } from 'primeng/button';
|
||||||
|
import { CardModule } from 'primeng/card';
|
||||||
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
|
import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
|
||||||
|
import { FractionComponent } from "../fraction/fraction.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-entry-card',
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CardModule,
|
||||||
|
ConfirmDialogModule,
|
||||||
|
DatePipe,
|
||||||
|
NgIconComponent,
|
||||||
|
FractionComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
matDeleteSharp,
|
||||||
|
matCalendarMonthSharp,
|
||||||
|
matSpeedSharp,
|
||||||
|
matStraightenSharp,
|
||||||
|
matLocalGasStationSharp,
|
||||||
|
}),
|
||||||
|
ConfirmationService,
|
||||||
|
],
|
||||||
|
templateUrl: './entry-card.component.html',
|
||||||
|
styleUrl: './entry-card.component.scss'
|
||||||
|
})
|
||||||
|
export class EntryCardComponent {
|
||||||
|
readonly entry = input.required<GetConsumptionEntriesEntry>();
|
||||||
|
|
||||||
|
protected readonly formattedLiterPer100Km = computed(() => {
|
||||||
|
const entry = this.entry();
|
||||||
|
|
||||||
|
const formatted = entry.literPer100Km
|
||||||
|
?.toFixed(2)
|
||||||
|
.replace('.', ',');
|
||||||
|
return formatted;
|
||||||
|
})
|
||||||
|
|
||||||
|
readonly entryDeleted = output<GetConsumptionEntriesEntry>();
|
||||||
|
|
||||||
|
private readonly routingService = inject(RoutingService);
|
||||||
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly confirmationService = inject(ConfirmationService);
|
||||||
|
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
async navigateToEdit(): Promise<void> {
|
||||||
|
await this.routingService.navigateToEditEntry(this.entry().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDeleteEntry(): void {
|
||||||
|
const weighedAt = new Date(
|
||||||
|
Date.parse(this.entry().dateTime),
|
||||||
|
).toLocaleString('de-DE', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.confirmationService.confirm({
|
||||||
|
closeOnEscape: true,
|
||||||
|
dismissableMask: true,
|
||||||
|
header: 'Bist du sicher?',
|
||||||
|
message: `Möchtest du diesen Eintrag (${weighedAt} für ${this.entry().car.name}) wirklich löschen?`,
|
||||||
|
acceptButtonProps: {
|
||||||
|
label: 'Löschen',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
rejectButtonProps: {
|
||||||
|
label: 'Abbrechen',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
accept: () => this.deleteEntry(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEntry(): void {
|
||||||
|
this.consumptionClient.delete(this.entry().id)
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
tap(() => this.entryDeleted.emit(this.entry())),
|
||||||
|
catchError((error) => this.handleError(error)),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Löschen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case error.status === 400:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Clientfehler',
|
||||||
|
detail:
|
||||||
|
'Die Anwendung scheint falsche Daten an den Server zu senden.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Löschen des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="flex flex-col items-center text-half-size">
|
||||||
|
<span>
|
||||||
|
{{ numerator() }}
|
||||||
|
</span>
|
||||||
|
<span class="separator"></span>
|
||||||
|
<span>
|
||||||
|
{{ denominator() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.separator {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-half-size {
|
||||||
|
// Specifically use em here to allow the parent to control the font size
|
||||||
|
// The font size here should be smaller because it is a fraction and would
|
||||||
|
// otherwise look too large
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-fraction',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './fraction.component.html',
|
||||||
|
styleUrl: './fraction.component.scss'
|
||||||
|
})
|
||||||
|
export class FractionComponent {
|
||||||
|
readonly numerator = input.required<number | string>();
|
||||||
|
readonly denominator = input.required<number | string>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<section>
|
||||||
|
<p-scrollTop />
|
||||||
|
<div class="mb-4 flex gap-2 md:justify-between">
|
||||||
|
<div class="basis-full lg:basis-1/4 md:basis-1/2 p-0">
|
||||||
|
<p-select styleClass="w-full" [formControl]="selectedCar" placeholder="Auto" [showClear]="false"
|
||||||
|
[options]="(cars$ | async)!" optionLabel="name" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p-button label="Erstellen" routerLink="/entries/create">
|
||||||
|
<ng-icon name="matAddSharp"></ng-icon>
|
||||||
|
</p-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (consumptionEntries$ | async; as entries) {
|
||||||
|
<p-dataView
|
||||||
|
[value]="entries"
|
||||||
|
[paginator]="true"
|
||||||
|
[rows]="25"
|
||||||
|
[rowsPerPageOptions]="[10, 25, 50, 100]"
|
||||||
|
[pageLinks]="0"
|
||||||
|
[showCurrentPageReport]="true"
|
||||||
|
currentPageReportTemplate="{currentPage} / {totalPages}"
|
||||||
|
layout="list"
|
||||||
|
>
|
||||||
|
<ng-template #list let-entries>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
@for (entry of entries; track entry.id) {
|
||||||
|
<app-entry-card [entry]="entry"
|
||||||
|
(entryDeleted)="onEntryDeleted($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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, DestroyRef, inject, OnInit } 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 { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-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,
|
||||||
|
startWith,
|
||||||
|
tap,
|
||||||
|
throwError
|
||||||
|
} from 'rxjs';
|
||||||
|
import { SelectedCarService } from '../services/selected-car.service';
|
||||||
|
import { EntryCardComponent } from './components/entry-card/entry-card.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-entries',
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
ButtonModule,
|
||||||
|
CommonModule,
|
||||||
|
DataViewModule,
|
||||||
|
EntryCardComponent,
|
||||||
|
NgIconComponent,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterLink,
|
||||||
|
ScrollTopModule,
|
||||||
|
SelectModule,
|
||||||
|
SkeletonModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
matAddSharp,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
templateUrl: './entries.component.html',
|
||||||
|
styleUrl: './entries.component.scss'
|
||||||
|
})
|
||||||
|
export class EntriesComponent implements OnInit {
|
||||||
|
private readonly carClient = inject(CarClient);
|
||||||
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
|
private readonly messageService = inject(MessageService);
|
||||||
|
private readonly selectedCarService = inject(SelectedCarService);
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
protected readonly consumptionEntries$: Observable<GetConsumptionEntriesEntry[]>;
|
||||||
|
protected readonly cars$: Observable<Car[]>;
|
||||||
|
|
||||||
|
protected readonly skeletonsIterationSource = Array(10).fill(0);
|
||||||
|
|
||||||
|
protected readonly selectedCar = new FormControl<Car | null | undefined>(null);
|
||||||
|
|
||||||
|
private readonly deletedEntries$ = new BehaviorSubject(<string[]>[]);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const entries = this.consumptionClient.getAll()
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
map(response => response.consumptions.sort((a, b) => b.dateTime.localeCompare(a.dateTime))),
|
||||||
|
catchError((error) => this.handleGetEntriesError(error))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.consumptionEntries$ = combineLatest([
|
||||||
|
entries,
|
||||||
|
this.selectedCar.valueChanges.pipe(startWith(null)),
|
||||||
|
this.deletedEntries$,
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
map(([entries, selectedCar, deletedEntries]) => {
|
||||||
|
const nonDeletedEntries =
|
||||||
|
deletedEntries.length === 0
|
||||||
|
? entries
|
||||||
|
: entries.filter(entry => !deletedEntries.includes(entry.id));
|
||||||
|
|
||||||
|
if (!selectedCar) {
|
||||||
|
return nonDeletedEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonDeletedEntries.filter(entry => entry.car.id === selectedCar.id);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cars$ = this.carClient.getAll()
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
map(response => response.cars),
|
||||||
|
map((cars) => cars
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
||||||
|
tap((cars) => {
|
||||||
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
||||||
|
|
||||||
|
if (selectedCarId === null) {
|
||||||
|
const firstCar = cars[0];
|
||||||
|
this.selectedCar.setValue(firstCar);
|
||||||
|
this.selectedCarService.setSelectedCarId(firstCar?.id ?? null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCar = cars.find(car => car.id === selectedCarId);
|
||||||
|
this.selectedCar.setValue(selectedCar ?? null);
|
||||||
|
this.selectedCarService.setSelectedCarId(selectedCar?.id ?? null);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.selectedCar.valueChanges
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
tap((car) => {
|
||||||
|
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntryDeleted(entry: GetConsumptionEntriesEntry): void {
|
||||||
|
this.deletedEntries$.next([...this.deletedEntries$.value, entry.id]);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Eintrag gelöscht',
|
||||||
|
detail: 'Der Eintrag wurde erfolgreich gelöscht.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGetEntriesError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => new Error('An unexpected error occurred'));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { BehaviorSubject, tap } from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class SelectedCarService {
|
||||||
|
static readonly SELECTED_CAR_ID_KEY = "SELECTED_CAR_ID";
|
||||||
|
|
||||||
|
private selectedCarId: string | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadStoredCarId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadStoredCarId(): void {
|
||||||
|
this.selectedCarId = localStorage.getItem(SelectedCarService.SELECTED_CAR_ID_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedCarId() {
|
||||||
|
return this.selectedCarId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedCarId(carId: string | null): void {
|
||||||
|
this.selectedCarId = carId;
|
||||||
|
if (carId === null) {
|
||||||
|
localStorage.removeItem(SelectedCarService.SELECTED_CAR_ID_KEY);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(SelectedCarService.SELECTED_CAR_ID_KEY, carId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/Vegasco-Web/src/app/services/routing.service.ts
Normal file
33
src/Vegasco-Web/src/app/services/routing.service.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RoutingService {
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
async navigateToEntries(): Promise<void> {
|
||||||
|
await this.router.navigateByUrl('/entries');
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToEditEntry(entryId: string): Promise<void> {
|
||||||
|
await this.router.navigate(['entries', 'edit', entryId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToCreateEntry(): Promise<void> {
|
||||||
|
await this.router.navigate(['entries', 'create']);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToCars(): Promise<void> {
|
||||||
|
await this.router.navigateByUrl('/cars');
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToEditCar(entryId: string): Promise<void> {
|
||||||
|
await this.router.navigate(['cars', 'edit', entryId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateToCreateCar(): Promise<void> {
|
||||||
|
await this.router.navigate(['cars', 'create']);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Vegasco-Web/src/environments/environment.development.ts
Normal file
11
src/Vegasco-Web/src/environments/environment.development.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Environment } from "./environment.interface";
|
||||||
|
|
||||||
|
export const environment: Environment = {
|
||||||
|
name: "Dev",
|
||||||
|
isProduction: false,
|
||||||
|
keycloak: {
|
||||||
|
host: "https://login.nuyken.dev",
|
||||||
|
realm: "development",
|
||||||
|
clientId: "vegasco"
|
||||||
|
}
|
||||||
|
};
|
||||||
17
src/Vegasco-Web/src/environments/environment.interface.ts
Normal file
17
src/Vegasco-Web/src/environments/environment.interface.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
/** The app's configuration based on the target environment */
|
||||||
|
export interface Environment {
|
||||||
|
/** A name for this configuration, e.g. 'Prod' */
|
||||||
|
name: string;
|
||||||
|
/** Whether this configuration is for production or not */
|
||||||
|
isProduction: boolean;
|
||||||
|
/** Keycloak login configuration */
|
||||||
|
keycloak: {
|
||||||
|
/** The host under which the keycloak is reachable */
|
||||||
|
host: string;
|
||||||
|
/** The keycloak realm in which the client lives */
|
||||||
|
realm: string;
|
||||||
|
/** The app's client id */
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Vegasco-Web/src/environments/environment.production.ts
Normal file
11
src/Vegasco-Web/src/environments/environment.production.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Environment } from "./environment.interface";
|
||||||
|
|
||||||
|
export const environment: Environment = {
|
||||||
|
name: "Prod",
|
||||||
|
isProduction: true,
|
||||||
|
keycloak: {
|
||||||
|
host: "https://login.nuyken.dev",
|
||||||
|
realm: "apps",
|
||||||
|
clientId: "vegasco"
|
||||||
|
}
|
||||||
|
};
|
||||||
11
src/Vegasco-Web/src/environments/environment.ts
Normal file
11
src/Vegasco-Web/src/environments/environment.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Environment } from "./environment.interface";
|
||||||
|
|
||||||
|
export const environment: Environment = {
|
||||||
|
name: "",
|
||||||
|
isProduction: false,
|
||||||
|
keycloak: {
|
||||||
|
host: "",
|
||||||
|
realm: "",
|
||||||
|
clientId: ""
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1 +1,39 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
@use "tailwindcss";
|
||||||
|
@plugin "tailwindcss-primeui";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-content-width {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trbl-0 {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-color-text {
|
||||||
|
color: var(--primary-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visually-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@vegasco-web/*": ["src/app/*"],
|
||||||
|
"@vegasco-web/assets/*": ["assets/*"],
|
||||||
|
"@vegasco-web/environments/*": ["src/environments/*"]
|
||||||
|
},
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
|||||||
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,13 +11,18 @@ 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)
|
||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapPost("cars", Endpoint)
|
.MapPost("cars", Endpoint)
|
||||||
.WithTags("Cars");
|
.WithTags("Cars")
|
||||||
|
.WithDescription("Creates a new car")
|
||||||
|
.Produces<Response>(201)
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(409);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Validator : AbstractValidator<Request>
|
public class Validator : AbstractValidator<Request>
|
||||||
@@ -29,41 +35,62 @@ public static class CreateCar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
Request request,
|
Request request,
|
||||||
IEnumerable<IValidator<Request>> validators,
|
IEnumerable<IValidator<Request>> validators,
|
||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
UserAccessor userAccessor,
|
UserAccessor userAccessor,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
|
ILogger logger = loggerFactory.CreateLogger(typeof(CreateCar));
|
||||||
|
|
||||||
|
List<ValidationResult> failedValidations =
|
||||||
|
await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
|
||||||
if (failedValidations.Count > 0)
|
if (failedValidations.Count > 0)
|
||||||
{
|
{
|
||||||
|
string[] errors = failedValidations
|
||||||
|
.Where(x => !x.IsValid)
|
||||||
|
.SelectMany(x => x.Errors)
|
||||||
|
.Select(x => x.ErrorMessage)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
logger.LogDebug(
|
||||||
|
"Validation failed for request {@Request} with errors {@Errors}",
|
||||||
|
request,
|
||||||
|
errors);
|
||||||
|
|
||||||
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isDuplicate = await dbContext.Cars
|
||||||
|
.AnyAsync(x => x.Name.ToUpper() == request.Name.ToUpper(), cancellationToken);
|
||||||
|
|
||||||
|
if (isDuplicate)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Car with name '{CarName}' (case insensitive) already exists", request.Name);
|
||||||
|
return TypedResults.Conflict();
|
||||||
|
}
|
||||||
|
|
||||||
string userId = userAccessor.GetUserId();
|
string userId = userAccessor.GetUserId();
|
||||||
|
|
||||||
User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken);
|
User? user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken);
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
user = new User
|
logger.LogDebug("User with ID '{UserId}' not found, creating new user", userId);
|
||||||
{
|
|
||||||
Id = userId
|
user = new User { Id = userId };
|
||||||
};
|
|
||||||
await dbContext.Users.AddAsync(user, cancellationToken);
|
await dbContext.Users.AddAsync(user, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Car car = new()
|
Car car = new() { Name = request.Name.Trim(), UserId = userId };
|
||||||
{
|
|
||||||
Name = request.Name,
|
|
||||||
UserId = userId
|
|
||||||
};
|
|
||||||
|
|
||||||
await dbContext.Cars.AddAsync(car, cancellationToken);
|
await dbContext.Cars.AddAsync(car, cancellationToken);
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogTrace("Created new car: {@Car}", car);
|
||||||
|
|
||||||
Response response = new(car.Id.Value, car.Name);
|
Response response = new(car.Id.Value, car.Name);
|
||||||
return TypedResults.Created($"/v1/cars/{car.Id}", response);
|
return TypedResults.Created($"/v1/cars/{car.Id}", response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using Vegasco.Server.Api.Persistence;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Cars;
|
namespace Vegasco.Server.Api.Cars;
|
||||||
|
|
||||||
@@ -8,23 +10,35 @@ public static class DeleteCar
|
|||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapDelete("cars/{id:guid}", Endpoint)
|
.MapDelete("cars/{id:guid}", Endpoint)
|
||||||
.WithTags("Cars");
|
.WithTags("Cars")
|
||||||
|
.WithDescription("Deletes a car by ID")
|
||||||
|
.Produces(204)
|
||||||
|
.Produces(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
Guid id,
|
Guid id,
|
||||||
ApplicationDbContext dbContext,
|
ApplicationDbContext dbContext,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Car? car = await dbContext.Cars.FindAsync([new CarId(id)], cancellationToken: cancellationToken);
|
Activity? activity = Activity.Current;
|
||||||
|
activity?.SetTag("id", id);
|
||||||
|
|
||||||
if (car is null)
|
int rows = await dbContext.Cars
|
||||||
|
.Where(x => x.Id == new CarId(id))
|
||||||
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (rows == 0)
|
||||||
{
|
{
|
||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.Cars.Remove(car);
|
if (rows > 1)
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
{
|
||||||
|
ILogger logger = loggerFactory.CreateLogger(typeof(DeleteCar));
|
||||||
|
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{CarId}'", rows, id);
|
||||||
|
}
|
||||||
|
|
||||||
return TypedResults.NoContent();
|
return TypedResults.NoContent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Vegasco.Server.Api.Persistence;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Cars;
|
namespace Vegasco.Server.Api.Cars;
|
||||||
|
|
||||||
@@ -10,7 +11,10 @@ public static class GetCar
|
|||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapGet("cars/{id:guid}", Endpoint)
|
.MapGet("cars/{id:guid}", Endpoint)
|
||||||
.WithTags("Cars");
|
.WithDescription("Returns a single car by ID")
|
||||||
|
.WithTags("Cars")
|
||||||
|
.Produces<Response>()
|
||||||
|
.Produces(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
@@ -25,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;
|
||||||
@@ -11,7 +12,7 @@ public static class GetCars
|
|||||||
{
|
{
|
||||||
public IEnumerable<ResponseDto> Cars { get; set; } = [];
|
public IEnumerable<ResponseDto> Cars { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ResponseDto(Guid Id, string Name);
|
public record ResponseDto(Guid Id, string Name);
|
||||||
|
|
||||||
public class Request
|
public class Request
|
||||||
@@ -25,19 +26,24 @@ public static class GetCars
|
|||||||
return builder
|
return builder
|
||||||
.MapGet("cars", Endpoint)
|
.MapGet("cars", Endpoint)
|
||||||
.WithDescription("Returns all cars")
|
.WithDescription("Returns all cars")
|
||||||
.WithTags("Cars");
|
.WithTags("Cars")
|
||||||
|
.Produces<ApiResponse>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<Ok<ApiResponse>> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
[AsParameters] Request request,
|
[AsParameters] Request request,
|
||||||
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,13 +10,19 @@ 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)
|
||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapPut("cars/{id:guid}", Endpoint)
|
.MapPut("cars/{id:guid}", Endpoint)
|
||||||
.WithTags("Cars");
|
.WithTags("Cars")
|
||||||
|
.WithDescription("Updates a car by ID")
|
||||||
|
.Produces<Response>()
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(404)
|
||||||
|
.Produces(409);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Validator : AbstractValidator<Request>
|
public class Validator : AbstractValidator<Request>
|
||||||
@@ -28,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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +70,21 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,7 @@ using FluentValidation;
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.AspNetCore.Localization;
|
|
||||||
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;
|
||||||
@@ -20,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()
|
||||||
@@ -29,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(() =>
|
||||||
@@ -51,7 +69,7 @@ public static class DependencyInjectionExtensions
|
|||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
|
|
||||||
services.AddHostedService<ApplyMigrationsService>();
|
services.AddHostedService<ApplyMigrationsService>();
|
||||||
|
|
||||||
services.AddRequestLocalization(o =>
|
services.AddRequestLocalization(o =>
|
||||||
{
|
{
|
||||||
string[] cultures =
|
string[] cultures =
|
||||||
@@ -61,7 +79,7 @@ public static class DependencyInjectionExtensions
|
|||||||
"de-DE",
|
"de-DE",
|
||||||
"de"
|
"de"
|
||||||
];
|
];
|
||||||
|
|
||||||
o.SetDefaultCulture(cultures[0])
|
o.SetDefaultCulture(cultures[0])
|
||||||
.AddSupportedCultures(cultures)
|
.AddSupportedCultures(cultures)
|
||||||
.AddSupportedUICultures(cultures);
|
.AddSupportedUICultures(cultures);
|
||||||
@@ -123,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 =>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Localization;
|
using Asp.Versioning.ApiExplorer;
|
||||||
using System.Globalization;
|
using Scalar.AspNetCore;
|
||||||
using Vegasco.Server.Api.Endpoints;
|
using Vegasco.Server.Api.Endpoints;
|
||||||
using Vegasco.Server.ServiceDefaults;
|
using Vegasco.Server.ServiceDefaults;
|
||||||
|
|
||||||
@@ -37,7 +37,8 @@ internal static class StartupExtensions
|
|||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.MapOpenApi("/swagger/{documentName}/swagger.json");
|
app.MapOpenApi();
|
||||||
|
app.MapScalarApiReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -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,23 +9,30 @@ 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)
|
||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapPost("consumptions", Endpoint)
|
.MapPost("consumptions", Endpoint)
|
||||||
.WithTags("Consumptions");
|
.WithTags("Consumptions")
|
||||||
|
.WithDescription("Creates a new consumption entry")
|
||||||
|
.Produces<Response>(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Validator : AbstractValidator<Request>
|
public class Validator : AbstractValidator<Request>
|
||||||
{
|
{
|
||||||
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)
|
||||||
@@ -42,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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,19 +78,21 @@ public static class CreateConsumption
|
|||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var consumption = new Consumption
|
Consumption consumption = new()
|
||||||
{
|
{
|
||||||
DateTime = request.DateTime.ToUniversalTime(),
|
DateTime = request.DateTime.ToUniversalTime(),
|
||||||
Distance = request.Distance,
|
Distance = request.Distance,
|
||||||
Amount = request.Amount,
|
Amount = request.Amount,
|
||||||
IgnoreInCalculation = request.IgnoreInCalculation,
|
|
||||||
CarId = new CarId(request.CarId)
|
CarId = new CarId(request.CarId)
|
||||||
};
|
};
|
||||||
|
|
||||||
dbContext.Consumptions.Add(consumption);
|
dbContext.Consumptions.Add(consumption);
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
logger.LogTrace("Created new consumption: {@Consumption}", consumption);
|
||||||
|
|
||||||
return TypedResults.Created($"consumptions/{consumption.Id.Value}",
|
return TypedResults.Created($"consumptions/{consumption.Id.Value}",
|
||||||
new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount, consumption.IgnoreInCalculation, consumption.CarId.Value));
|
new Response(consumption.Id.Value, consumption.DateTime, consumption.Distance, consumption.Amount,
|
||||||
|
consumption.CarId.Value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using Vegasco.Server.Api.Persistence;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Vegasco.Server.Api.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.Server.Api.Consumptions;
|
namespace Vegasco.Server.Api.Consumptions;
|
||||||
|
|
||||||
@@ -8,22 +10,35 @@ public static class DeleteConsumption
|
|||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapDelete("consumptions/{id:guid}", Endpoint)
|
.MapDelete("consumptions/{id:guid}", Endpoint)
|
||||||
.WithTags("Consumptions");
|
.WithTags("Consumptions")
|
||||||
|
.WithDescription("Deletes a consumption entry by ID")
|
||||||
|
.Produces(204)
|
||||||
|
.Produces(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
ApplicationDbContext dbContext,
|
|
||||||
Guid id,
|
Guid id,
|
||||||
|
ApplicationDbContext dbContext,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Consumption? consumption = await dbContext.Consumptions.FindAsync([new ConsumptionId(id)], cancellationToken);
|
Activity? activity = Activity.Current;
|
||||||
if (consumption is null)
|
activity?.SetTag("id", id);
|
||||||
|
|
||||||
|
int rows = await dbContext.Consumptions
|
||||||
|
.Where(x => x.Id == new ConsumptionId(id))
|
||||||
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (rows == 0)
|
||||||
{
|
{
|
||||||
return TypedResults.NotFound();
|
return TypedResults.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.Consumptions.Remove(consumption);
|
if (rows > 1)
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
{
|
||||||
|
ILogger logger = loggerFactory.CreateLogger(typeof(DeleteConsumption));
|
||||||
|
logger.LogWarning("Deleted '{DeletedRowCount}' rows for id '{ConsumptionId}'", rows, id);
|
||||||
|
}
|
||||||
|
|
||||||
return TypedResults.NoContent();
|
return TypedResults.NoContent();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ 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)
|
||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapGet("consumptions/{id:guid}", Endpoint)
|
.MapGet("consumptions/{id:guid}", Endpoint)
|
||||||
.WithTags("Consumptions");
|
.WithTags("Consumptions")
|
||||||
|
.WithDescription("Returns a single consumption entry by ID")
|
||||||
|
.Produces<Response>()
|
||||||
|
.Produces(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> Endpoint(
|
private static async Task<IResult> Endpoint(
|
||||||
@@ -25,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,53 +1,102 @@
|
|||||||
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;
|
||||||
|
|
||||||
public static class GetConsumptions
|
public static class GetConsumptions
|
||||||
{
|
{
|
||||||
public class ApiResponse
|
public class ApiResponse
|
||||||
{
|
{
|
||||||
public IEnumerable<ResponseDto> Consumptions { get; set; } = [];
|
public IEnumerable<ResponseDto> Consumptions { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ResponseDto(
|
public record ResponseDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
DateTimeOffset DateTime,
|
DateTimeOffset DateTime,
|
||||||
double Distance,
|
double Distance,
|
||||||
double Amount,
|
double Amount,
|
||||||
bool IgnoreInCalculation,
|
CarDto Car,
|
||||||
Guid CarId);
|
double? LiterPer100Km);
|
||||||
|
|
||||||
public class Request
|
public record CarDto(
|
||||||
{
|
Guid Id,
|
||||||
[FromQuery(Name = "page")] public int? Page { get; set; }
|
string Name)
|
||||||
[FromQuery(Name = "pageSize")] public int? PageSize { get; set; }
|
{
|
||||||
}
|
public static CarDto FromCar(Car car)
|
||||||
|
{
|
||||||
|
return new CarDto(car.Id.Value, car.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
public class Request
|
||||||
{
|
{
|
||||||
return builder
|
[FromQuery(Name = "page")] public int? Page { get; set; }
|
||||||
.MapGet("consumptions", Endpoint)
|
[FromQuery(Name = "pageSize")] public int? PageSize { get; set; }
|
||||||
.WithDescription("Returns all consumption entries")
|
}
|
||||||
.WithTags("Consumptions");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<Ok<ApiResponse>> Endpoint(
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
[AsParameters] Request request,
|
{
|
||||||
ApplicationDbContext dbContext,
|
return builder
|
||||||
CancellationToken cancellationToken)
|
.MapGet("consumptions", Endpoint)
|
||||||
{
|
.WithDescription("Returns all consumption entries")
|
||||||
List<ResponseDto> consumptions = await dbContext.Consumptions
|
.WithTags("Consumptions")
|
||||||
.Select(x =>
|
.Produces<ApiResponse>();
|
||||||
new ResponseDto(x.Id.Value, x.DateTime, x.Distance, x.Amount, x.IgnoreInCalculation, x.CarId.Value))
|
}
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var apiResponse = new ApiResponse
|
private static async Task<Ok<ApiResponse>> Endpoint(
|
||||||
{
|
[AsParameters] Request request,
|
||||||
Consumptions = consumptions
|
ApplicationDbContext dbContext,
|
||||||
};
|
ILoggerFactory loggerFactory,
|
||||||
return TypedResults.Ok(apiResponse);
|
CancellationToken cancellationToken)
|
||||||
}
|
{
|
||||||
|
ILogger logger = loggerFactory.CreateLogger(typeof(GetConsumptions));
|
||||||
|
|
||||||
|
logger.LogTrace("Received request to get consumptions with parameters: {@Request}", request);
|
||||||
|
Activity? activity = Activity.Current;
|
||||||
|
|
||||||
|
Dictionary<CarId, List<Consumption>> consumptionsByCar = await dbContext.Consumptions
|
||||||
|
.Include(x => x.Car)
|
||||||
|
.GroupBy(x => x.CarId)
|
||||||
|
.ToDictionaryAsync(x => x.Key, x => x.OrderByDescending(x => x.DateTime).ToList(), cancellationToken);
|
||||||
|
|
||||||
|
List<ResponseDto> responses = [];
|
||||||
|
|
||||||
|
foreach (List<Consumption> consumptions in consumptionsByCar.Select(x => x.Value))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < consumptions.Count; i++)
|
||||||
|
{
|
||||||
|
Consumption consumption = consumptions[i];
|
||||||
|
|
||||||
|
double? literPer100Km = null;
|
||||||
|
|
||||||
|
bool isLast = i == consumptions.Count - 1;
|
||||||
|
if (!isLast)
|
||||||
|
{
|
||||||
|
Consumption previousConsumption = consumptions[i + 1];
|
||||||
|
double distanceDiff = consumption.Distance - previousConsumption.Distance;
|
||||||
|
literPer100Km = consumption.Amount / (distanceDiff / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.Add(new ResponseDto(
|
||||||
|
consumption.Id.Value,
|
||||||
|
consumption.DateTime,
|
||||||
|
consumption.Distance,
|
||||||
|
consumption.Amount,
|
||||||
|
CarDto.FromCar(consumption.Car),
|
||||||
|
literPer100Km));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activity?.SetTag("consumptionCount", responses.Count);
|
||||||
|
|
||||||
|
ApiResponse apiResponse = new()
|
||||||
|
{
|
||||||
|
Consumptions = responses
|
||||||
|
};
|
||||||
|
return TypedResults.Ok(apiResponse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,23 +7,32 @@ 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)
|
||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapPut("consumptions/{id:guid}", Endpoint)
|
.MapPut("consumptions/{id:guid}", Endpoint)
|
||||||
.WithTags("Consumptions");
|
.WithTags("Consumptions")
|
||||||
|
.WithDescription("Updates a consumption entry by ID")
|
||||||
|
.Produces<Response>()
|
||||||
|
.ProducesValidationProblem()
|
||||||
|
.Produces(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Validator : AbstractValidator<Request>
|
public class Validator : AbstractValidator<Request>
|
||||||
{
|
{
|
||||||
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)
|
||||||
@@ -39,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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ public static class EndpointExtensions
|
|||||||
|
|
||||||
RouteGroupBuilder versionedApis = builder.MapGroup("/v{apiVersion:apiVersion}")
|
RouteGroupBuilder versionedApis = builder.MapGroup("/v{apiVersion:apiVersion}")
|
||||||
.WithApiVersionSet(apiVersionSet);
|
.WithApiVersionSet(apiVersionSet);
|
||||||
|
|
||||||
GetCar.MapEndpoint(versionedApis)
|
GetCar.MapEndpoint(versionedApis)
|
||||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||||
GetCars.MapEndpoint(versionedApis)
|
GetCars.MapEndpoint(versionedApis)
|
||||||
@@ -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,28 +2,28 @@
|
|||||||
|
|
||||||
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,
|
||||||
string CommitId,
|
string CommitId,
|
||||||
DateTimeOffset CommitDate,
|
DateTimeOffset CommitDate,
|
||||||
string Environment);
|
string Environment);
|
||||||
|
|
||||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapGet("info/server", Endpoint)
|
.MapGet("info/server", Endpoint)
|
||||||
.WithTags("Info");
|
.WithTags("Info");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Ok<Response> Endpoint(
|
private static Ok<Response> Endpoint(
|
||||||
IHostEnvironment environment)
|
IHostEnvironment environment)
|
||||||
{
|
{
|
||||||
return TypedResults.Ok(new Response(
|
return TypedResults.Ok(new Response(
|
||||||
ThisAssembly.AssemblyInformationalVersion,
|
ThisAssembly.AssemblyInformationalVersion,
|
||||||
ThisAssembly.GitCommitId,
|
ThisAssembly.GitCommitId,
|
||||||
new DateTimeOffset(ThisAssembly.GitCommitDate, TimeSpan.Zero),
|
new DateTimeOffset(ThisAssembly.GitCommitDate, TimeSpan.Zero),
|
||||||
environment.EnvironmentName));
|
environment.EnvironmentName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,86 +4,86 @@
|
|||||||
|
|
||||||
namespace Vegasco.Server.Api.Persistence.Migrations
|
namespace Vegasco.Server.Api.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class Initial : Migration
|
public partial class Initial : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "Users",
|
name: "Users",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "text", nullable: false)
|
Id = table.Column<string>(type: "text", nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_Users", x => x.Id);
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "Cars",
|
name: "Cars",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
UserId = table.Column<string>(type: "text", nullable: false)
|
UserId = table.Column<string>(type: "text", nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_Cars", x => x.Id);
|
table.PrimaryKey("PK_Cars", x => x.Id);
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_Cars_Users_UserId",
|
name: "FK_Cars_Users_UserId",
|
||||||
column: x => x.UserId,
|
column: x => x.UserId,
|
||||||
principalTable: "Users",
|
principalTable: "Users",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "Consumptions",
|
name: "Consumptions",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
DateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
DateTime = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
Distance = table.Column<double>(type: "double precision", nullable: false),
|
Distance = table.Column<double>(type: "double precision", nullable: false),
|
||||||
Amount = table.Column<double>(type: "double precision", nullable: false),
|
Amount = table.Column<double>(type: "double precision", nullable: false),
|
||||||
IgnoreInCalculation = table.Column<bool>(type: "boolean", nullable: false),
|
IgnoreInCalculation = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
CarId = table.Column<Guid>(type: "uuid", nullable: false)
|
CarId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_Consumptions", x => x.Id);
|
table.PrimaryKey("PK_Consumptions", x => x.Id);
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_Consumptions_Cars_CarId",
|
name: "FK_Consumptions_Cars_CarId",
|
||||||
column: x => x.CarId,
|
column: x => x.CarId,
|
||||||
principalTable: "Cars",
|
principalTable: "Cars",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Cars_UserId",
|
name: "IX_Cars_UserId",
|
||||||
table: "Cars",
|
table: "Cars",
|
||||||
column: "UserId");
|
column: "UserId");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Consumptions_CarId",
|
name: "IX_Consumptions_CarId",
|
||||||
table: "Consumptions",
|
table: "Consumptions",
|
||||||
column: "CarId");
|
column: "CarId");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Consumptions");
|
name: "Consumptions");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Cars");
|
name: "Cars");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Users");
|
name: "Users");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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");
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "scalar/v1",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,22 +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.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>
|
||||||
@@ -39,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,21 +1,41 @@
|
|||||||
|
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(targetPort: 4200);
|
.WithHttpEndpoint(port: 44200, env: "PORT")
|
||||||
|
.WithExternalHttpEndpoints()
|
||||||
|
.WithHttpHealthCheck("/", 200);
|
||||||
|
|
||||||
builder.Build().Run();
|
builder.Build().Run();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user