Compare commits
12 Commits
master
...
practical5
| Author | SHA256 | Date | |
|---|---|---|---|
|
ce452f8278
|
|||
|
b5cd1ada7c
|
|||
|
8ad2bc0d2d
|
|||
|
9fca6d81a2
|
|||
|
4f1fa86833
|
|||
|
d203544a6f
|
|||
|
1c162907fd
|
|||
|
cfc6f170ad
|
|||
|
d81b40ee8e
|
|||
|
ca028f9262
|
|||
|
a3665918f0
|
|||
|
ce9abe9940
|
@@ -1,18 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Build/Test Commands
|
|
||||||
- Build: `./gradlew build`
|
|
||||||
- Test all: `./gradlew test`
|
|
||||||
- Run single test: `./gradlew test --tests "FreightIntegrationTest.getByIdReturnsEntity"`
|
|
||||||
- Run app: `./gradlew bootRun`
|
|
||||||
|
|
||||||
## Code Style Guidelines
|
|
||||||
- Package structure: `ua.com.dxrkness.{controller,service,repository,model}`
|
|
||||||
- Use Spring Boot annotations (@RestController, @Service, etc.)
|
|
||||||
- Records for models with Jackson @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
- Constructor injection for dependencies
|
|
||||||
- JSpecify annotations (@NullMarked, @Nullable) for null safety
|
|
||||||
- Integration tests with @SpringBootTest and parameterized tests
|
|
||||||
- REST controllers support both JSON and XML media types
|
|
||||||
- Use ResponseEntity for HTTP responses with proper status codes
|
|
||||||
- OpenAPI documentation with Swagger annotations on controllers
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
plugins {
|
||||||
|
id("java")
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "ua.com.dxrkness"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// spring
|
||||||
|
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||||
|
implementation(platform(libs.spring.cloud.dependencies))
|
||||||
|
implementation("org.springframework.cloud:spring-cloud-starter-gateway-server-webflux")
|
||||||
|
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
|
||||||
|
implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ua.com.dxrkness.gateway;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class ApiGatewayApplication {
|
||||||
|
static void main(String[] args) {
|
||||||
|
SpringApplication.run(ApiGatewayApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
server:
|
||||||
|
port: 8079
|
||||||
|
spring:
|
||||||
|
cloud:
|
||||||
|
gateway:
|
||||||
|
server:
|
||||||
|
webflux:
|
||||||
|
discovery:
|
||||||
|
locator:
|
||||||
|
enabled: true
|
||||||
|
lower-case-service-id: true
|
||||||
|
globalcors:
|
||||||
|
cors-configurations:
|
||||||
|
'[/**]':
|
||||||
|
allowedOrigins: "*"
|
||||||
|
allowedMethods:
|
||||||
|
- '*'
|
||||||
|
allowedHeaders: "*"
|
||||||
|
application:
|
||||||
|
name: api-gateway
|
||||||
|
eureka:
|
||||||
|
client:
|
||||||
|
service-url:
|
||||||
|
defaultZone: http://localhost:8070/eureka
|
||||||
|
register-with-eureka: false
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"pluginSearchDirs": ["."], // should be removed in v3
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
|
||||||
|
"printWidth": 120,
|
||||||
|
"singleAttributePerLine": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Svelte + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Svelte in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||||
|
|
||||||
|
## Need an official Svelte framework?
|
||||||
|
|
||||||
|
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
||||||
|
|
||||||
|
## Technical considerations
|
||||||
|
|
||||||
|
**Why use this over SvelteKit?**
|
||||||
|
|
||||||
|
- It brings its own routing solution which might not be preferable for some users.
|
||||||
|
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
||||||
|
|
||||||
|
This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
||||||
|
|
||||||
|
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
|
||||||
|
|
||||||
|
**Why include `.vscode/extensions.json`?**
|
||||||
|
|
||||||
|
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
|
||||||
|
|
||||||
|
**Why enable `checkJs` in the JS template?**
|
||||||
|
|
||||||
|
It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration.
|
||||||
|
|
||||||
|
**Why is HMR not preserving my local component state?**
|
||||||
|
|
||||||
|
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state).
|
||||||
|
|
||||||
|
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// store.js
|
||||||
|
// An extremely simple external store
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
export default writable(0)
|
||||||
|
```
|
||||||
+293
@@ -0,0 +1,293 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "client",
|
||||||
|
"dependencies": {
|
||||||
|
"flowbite-svelte": "^1.31.0",
|
||||||
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
|
"flowbite": "^4.0.1",
|
||||||
|
"svelte": "^5.43.8",
|
||||||
|
"svelte5-router": "^3.0.2",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"vite": "npm:rolldown-vite@7.2.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"vite": "npm:rolldown-vite@7.2.5",
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
|
"@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||||
|
|
||||||
|
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
||||||
|
|
||||||
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
|
||||||
|
|
||||||
|
"@oxc-project/runtime": ["@oxc-project/runtime@0.97.0", "", {}, "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w=="],
|
||||||
|
|
||||||
|
"@oxc-project/types": ["@oxc-project/types@0.97.0", "", {}, "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ=="],
|
||||||
|
|
||||||
|
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "x64" }, "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.50", "", { "os": "freebsd", "cpu": "x64" }, "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm" }, "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.50", "", { "os": "none", "cpu": "arm64" }, "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.50", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "arm64" }, "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "ia32" }, "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "x64" }, "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="],
|
||||||
|
|
||||||
|
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="],
|
||||||
|
|
||||||
|
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
||||||
|
|
||||||
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
|
||||||
|
|
||||||
|
"@svgdotjs/svg.draggable.js": ["@svgdotjs/svg.draggable.js@3.0.6", "", { "peerDependencies": { "@svgdotjs/svg.js": "^3.2.4" } }, "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA=="],
|
||||||
|
|
||||||
|
"@svgdotjs/svg.filter.js": ["@svgdotjs/svg.filter.js@3.0.9", "", { "dependencies": { "@svgdotjs/svg.js": "^3.2.4" } }, "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw=="],
|
||||||
|
|
||||||
|
"@svgdotjs/svg.js": ["@svgdotjs/svg.js@3.2.5", "", {}, "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ=="],
|
||||||
|
|
||||||
|
"@svgdotjs/svg.resize.js": ["@svgdotjs/svg.resize.js@2.0.5", "", { "peerDependencies": { "@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.select.js": "^4.0.1" } }, "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA=="],
|
||||||
|
|
||||||
|
"@svgdotjs/svg.select.js": ["@svgdotjs/svg.select.js@4.0.3", "", { "peerDependencies": { "@svgdotjs/svg.js": "^3.2.4" } }, "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||||
|
|
||||||
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||||
|
|
||||||
|
"@yr/monotone-cubic-spline": ["@yr/monotone-cubic-spline@1.0.3", "", {}, "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
|
"apexcharts": ["apexcharts@5.3.6", "", { "dependencies": { "@svgdotjs/svg.draggable.js": "^3.0.4", "@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.resize.js": "^2.0.2", "@svgdotjs/svg.select.js": "^4.0.1", "@yr/monotone-cubic-spline": "^1.0.3" } }, "sha512-sVEPw+J0Gp0IHQabKu8cfdsxlfME0e36Wid7RIaPclGM2OUt+O7O4+6mfAmTUYhy5bDk8cNHzEhPfVtLCIXEJA=="],
|
||||||
|
|
||||||
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
|
|
||||||
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||||
|
|
||||||
|
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||||
|
|
||||||
|
"esrap": ["esrap@2.2.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"flowbite": ["flowbite@4.0.1", "", { "dependencies": { "@popperjs/core": "^2.9.3", "flowbite-datepicker": "^2.0.0", "mini-svg-data-uri": "^1.4.3", "postcss": "^8.5.1", "tailwindcss": "^4.1.12" } }, "sha512-UwUjvnqrQTiFm3uMJ0WWnzKXKoDyNyfyEzoNnxmZo6KyDzCedjqZw1UW0Oqdn+E0iYVdPu0fizydJN6e4pP9Rw=="],
|
||||||
|
|
||||||
|
"flowbite-datepicker": ["flowbite-datepicker@2.0.0", "", { "dependencies": { "@rollup/plugin-node-resolve": "^15.2.3", "@tailwindcss/postcss": "^4.1.17" } }, "sha512-m81hl0Bimq45MUg4maJLOnXrX+C9lZ0AkjMb9uotuVUSr729k/YiymWDfVAm63AYDH7g7y3rI3ke3XaBzWWqLw=="],
|
||||||
|
|
||||||
|
"flowbite-svelte": ["flowbite-svelte@1.31.0", "", { "dependencies": { "@floating-ui/dom": "^1.7.4", "@floating-ui/utils": "^0.2.10", "apexcharts": "^5.3.6", "clsx": "^2.1.1", "date-fns": "^4.1.0", "esm-env": "^1.2.2", "flowbite": "^3.1.2", "tailwind-merge": "^3.4.0", "tailwind-variants": "^3.2.2" }, "peerDependencies": { "svelte": "^5.40.0", "tailwindcss": "^4.1.4" } }, "sha512-A7Ts/R5GsL8DbgRf+8+1wdrIOOK0nq4ggEkv4RuY0oGuzH1PLBAH+bvC1L8AgQ5li9mj3o8eE9tHW7Md8yjPsw=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
|
|
||||||
|
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||||
|
|
||||||
|
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
||||||
|
|
||||||
|
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||||
|
|
||||||
|
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="],
|
||||||
|
|
||||||
|
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||||
|
|
||||||
|
"rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"svelte": ["svelte@5.46.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw=="],
|
||||||
|
|
||||||
|
"svelte5-router": ["svelte5-router@3.0.2", "", { "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-3XE97eEVrV2OY/7iuhrBIvD48KWmD9M8Cf2mZqjoxnI9bR5ZiwtAg9ObGP3kP7Z/kyVrn0n8riNBJdHeddctlQ=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||||
|
|
||||||
|
"tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"vite": ["rolldown-vite@7.2.5", "", { "dependencies": { "@oxc-project/runtime": "0.97.0", "fdir": "^6.5.0", "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.50", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA=="],
|
||||||
|
|
||||||
|
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||||
|
|
||||||
|
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"flowbite-svelte/flowbite": ["flowbite@3.1.2", "", { "dependencies": { "@popperjs/core": "^2.9.3", "flowbite-datepicker": "^1.3.1", "mini-svg-data-uri": "^1.4.3", "postcss": "^8.5.1" } }, "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q=="],
|
||||||
|
|
||||||
|
"flowbite-svelte/flowbite/flowbite-datepicker": ["flowbite-datepicker@1.3.2", "", { "dependencies": { "@rollup/plugin-node-resolve": "^15.2.3", "flowbite": "^2.0.0" } }, "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g=="],
|
||||||
|
|
||||||
|
"flowbite-svelte/flowbite/flowbite-datepicker/flowbite": ["flowbite@2.5.2", "", { "dependencies": { "@popperjs/core": "^2.9.3", "flowbite-datepicker": "^1.3.0", "mini-svg-data-uri": "^1.4.3" } }, "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ITROI client application</title>
|
||||||
|
<style>
|
||||||
|
html.dark {
|
||||||
|
background-color: #111827;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
/**
|
||||||
|
* svelte-preprocess cannot figure out whether you have
|
||||||
|
* a value or a type, so tell TypeScript to enforce using
|
||||||
|
* `import type` instead of `import` for Types.
|
||||||
|
*/
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
/**
|
||||||
|
* To have warnings / errors of the Svelte compiler at the
|
||||||
|
* correct position, enable source maps by default.
|
||||||
|
*/
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
/**
|
||||||
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
|
* Disable this if you'd like to use dynamic types.
|
||||||
|
*/
|
||||||
|
"checkJs": true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Use global.d.ts instead of compilerOptions.types
|
||||||
|
* to avoid limiting type declarations.
|
||||||
|
*/
|
||||||
|
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
|
"flowbite": "^4.0.1",
|
||||||
|
"svelte": "^5.43.8",
|
||||||
|
"svelte5-router": "^3.0.2",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"vite": "npm:rolldown-vite@7.2.5"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"vite": "npm:rolldown-vite@7.2.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"flowbite-svelte": "^1.31.0",
|
||||||
|
"prettier-plugin-svelte": "^3.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
<script>
|
||||||
|
import "./app.css";
|
||||||
|
import Header from "./Header.svelte";
|
||||||
|
import Vehicles from "./Vehicles.svelte";
|
||||||
|
import Freights from "./Freights.svelte";
|
||||||
|
import Routes from "./Routes.svelte";
|
||||||
|
import { Route, Router } from "svelte5-router";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import Toaster from "./Toaster.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
|
||||||
|
<Router>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<Router
|
||||||
|
viewtransition={() => {
|
||||||
|
return {
|
||||||
|
fn: fly,
|
||||||
|
duration: 1000,
|
||||||
|
y: 200,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<main class="pt-20">
|
||||||
|
<Route path="/vehicles"><Vehicles /></Route>
|
||||||
|
<Route path="/freights"><Freights /></Route>
|
||||||
|
<Route path="/routes"><Routes /></Route>
|
||||||
|
</main>
|
||||||
|
</Router>
|
||||||
|
</Router>
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
<script>
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableBodyCell,
|
||||||
|
TableBodyRow,
|
||||||
|
TableHead,
|
||||||
|
TableHeadCell,
|
||||||
|
Modal,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Badge,
|
||||||
|
Heading,
|
||||||
|
Helper,
|
||||||
|
} from "flowbite-svelte";
|
||||||
|
import toastManager from "./toastManager.svelte";
|
||||||
|
|
||||||
|
let freights = $state([]);
|
||||||
|
let isModalOpen = $state(false);
|
||||||
|
let editingId = $state(null);
|
||||||
|
|
||||||
|
let formData = $state({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
weight_kg: 0,
|
||||||
|
dimensions: { length_cm: 0, width_cm: 0, height_cm: 0 },
|
||||||
|
status: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
let errors = $derived.by(() => {
|
||||||
|
const e = {};
|
||||||
|
if (!formData.name.trim()) e.name = "Name is required";
|
||||||
|
if (formData.weight_kg <= 0) e.weight_kg = "Weight must be positive";
|
||||||
|
if (formData.dimensions.length_cm <= 0) e.length_cm = "Length must be positive";
|
||||||
|
if (formData.dimensions.width_cm <= 0) e.width_cm = "Width must be positive";
|
||||||
|
if (formData.dimensions.height_cm <= 0) e.height_cm = "Height must be positive";
|
||||||
|
if (!formData.status.trim()) e.status = "Status is required";
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const BASE_URL = "http://localhost:8079/freight-service/freights";
|
||||||
|
|
||||||
|
async function fetchFreights() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(BASE_URL);
|
||||||
|
freights = await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFreight() {
|
||||||
|
const method = editingId ? "PUT" : "POST";
|
||||||
|
const url = editingId ? `${BASE_URL}/${editingId}` : BASE_URL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
await fetchFreights();
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFreight(id) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/${id}`, { method: "DELETE" });
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.detail ?? res.statusText);
|
||||||
|
}
|
||||||
|
fetchFreights();
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(freight = null) {
|
||||||
|
if (freight) {
|
||||||
|
editingId = freight.id;
|
||||||
|
formData = JSON.parse(JSON.stringify(freight)); // deep clone
|
||||||
|
} else {
|
||||||
|
editingId = null;
|
||||||
|
formData = {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
weight_kg: 0,
|
||||||
|
dimensions: { length_cm: 0, width_cm: 0, height_cm: 0 },
|
||||||
|
status: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
isModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => (isModalOpen = false);
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
PENDING: { color: "yellow", label: "Pending" },
|
||||||
|
IN_TRANSIT: { color: "indigo", label: "In Transit" },
|
||||||
|
DELIVERED: { color: "green", label: "Delivered" },
|
||||||
|
};
|
||||||
|
|
||||||
|
onDestroy(() => toastManager.onDestroy());
|
||||||
|
|
||||||
|
onMount(fetchFreights);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-8 space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<Heading
|
||||||
|
tag="h2"
|
||||||
|
class="text-2xl font-bold"
|
||||||
|
>
|
||||||
|
Freights
|
||||||
|
</Heading>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
onclick={() => openModal()}
|
||||||
|
>
|
||||||
|
+ Add Freight
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
shadow
|
||||||
|
hoverable={true}
|
||||||
|
>
|
||||||
|
<TableHead>
|
||||||
|
<TableHeadCell>ID</TableHeadCell>
|
||||||
|
<TableHeadCell>Cargo Name</TableHeadCell>
|
||||||
|
<TableHeadCell>Weight (kg)</TableHeadCell>
|
||||||
|
<TableHeadCell>Dimensions (L/W/H)</TableHeadCell>
|
||||||
|
<TableHeadCell>Status</TableHeadCell>
|
||||||
|
<TableHeadCell>Actions</TableHeadCell>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{#each freights as item (item.id)}
|
||||||
|
<TableBodyRow>
|
||||||
|
<TableBodyCell>{item.id}</TableBodyCell>
|
||||||
|
<TableBodyCell>
|
||||||
|
<div class="font-medium">{item.name}</div>
|
||||||
|
<div class="text-xs text-gray-500">{item.description}</div>
|
||||||
|
</TableBodyCell>
|
||||||
|
<TableBodyCell>{item.weight_kg} kg</TableBodyCell>
|
||||||
|
<TableBodyCell>
|
||||||
|
{item.dimensions.length_cm}×{item.dimensions.width_cm}×{item.dimensions.height_cm} cm
|
||||||
|
</TableBodyCell>
|
||||||
|
<TableBodyCell>
|
||||||
|
<Badge color={statusMap[item.status].color}>{statusMap[item.status].label}</Badge>
|
||||||
|
</TableBodyCell>
|
||||||
|
<TableBodyCell class="space-x-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="alternative"
|
||||||
|
onclick={() => openModal(item)}>Edit</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="red"
|
||||||
|
onclick={() => deleteFreight(item.id)}>Delete</Button
|
||||||
|
>
|
||||||
|
</TableBodyCell>
|
||||||
|
</TableBodyRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editingId ? "Update Freight" : "Add Freight"}
|
||||||
|
bind:open={isModalOpen}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.name ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Item Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
bind:value={formData.name}
|
||||||
|
placeholder="e.g. Electronics"
|
||||||
|
color={errors.name ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.name}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.name}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<Label class="mb-2">Description</Label>
|
||||||
|
<Input
|
||||||
|
bind:value={formData.description}
|
||||||
|
placeholder="Cargo details (optional)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.weight_kg ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Weight (kg)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
bind:value={formData.weight_kg}
|
||||||
|
color={errors.weight_kg ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.weight_kg}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.weight_kg}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.status ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
items={Object.keys(statusMap).map((k) => ({
|
||||||
|
value: k,
|
||||||
|
name: statusMap[k].label,
|
||||||
|
}))}
|
||||||
|
bind:value={formData.status}
|
||||||
|
color={errors.status ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.status}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.status}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2 border-t pt-4 mt-2">
|
||||||
|
<Label class="mb-2 font-bold">Dimensions (cm)</Label>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<div>
|
||||||
|
<Helper
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.length_cm ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Length (cm.)
|
||||||
|
</Helper>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
bind:value={formData.dimensions.length_cm}
|
||||||
|
color={errors.width_cm ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.length_cm}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.length_cm}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Helper
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.width_cm ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Width (cm.)
|
||||||
|
</Helper>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
bind:value={formData.dimensions.width_cm}
|
||||||
|
color={errors.width_cm ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.width_cm}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.width_cm}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Helper
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.height_cm ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Height (cm.)
|
||||||
|
</Helper>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
bind:value={formData.dimensions.height_cm}
|
||||||
|
color={errors.height_cm ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.height_cm}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.height_cm}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-3 mt-6">
|
||||||
|
<Button
|
||||||
|
color="alternative"
|
||||||
|
onclick={closeModal}>Cancel</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
disabled={!(Object.keys(errors).length === 0)}
|
||||||
|
onclick={saveFreight}>Confirm</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script>
|
||||||
|
import { Navbar, NavBrand, NavUl, NavLi } from "flowbite-svelte";
|
||||||
|
import { Link } from "svelte5-router";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="fixed top-0 left-0 right-0 z-50">
|
||||||
|
<Navbar>
|
||||||
|
<NavBrand href="/">
|
||||||
|
<span
|
||||||
|
class="self-center text-xl font-semibold whitespace-nowrap dark:text-white"
|
||||||
|
>Logistics app</span
|
||||||
|
>
|
||||||
|
</NavBrand>
|
||||||
|
<NavUl>
|
||||||
|
<NavLi><Link to="/freights">Freights</Link></NavLi>
|
||||||
|
<NavLi><Link to="/vehicles">Vehicles</Link></NavLi>
|
||||||
|
<NavLi><Link to="/routes">Routes</Link></NavLi>
|
||||||
|
</NavUl>
|
||||||
|
</Navbar>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
<script>
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableBodyCell,
|
||||||
|
TableBodyRow,
|
||||||
|
TableHead,
|
||||||
|
TableHeadCell,
|
||||||
|
Modal,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Badge,
|
||||||
|
Heading,
|
||||||
|
MultiSelect,
|
||||||
|
Helper,
|
||||||
|
} from "flowbite-svelte";
|
||||||
|
import toastManager from "./toastManager.svelte";
|
||||||
|
|
||||||
|
let routes = $state([]);
|
||||||
|
let vehicles = $state([]); // For dropdown
|
||||||
|
let freights = $state([]); // For dropdown
|
||||||
|
let isModalOpen = $state(false);
|
||||||
|
let editingId = $state(null);
|
||||||
|
|
||||||
|
onDestroy(() => toastManager.onDestroy());
|
||||||
|
|
||||||
|
let formData = $state({
|
||||||
|
vehicle_id: "",
|
||||||
|
freight_id: [],
|
||||||
|
start_location: "",
|
||||||
|
end_location: "",
|
||||||
|
distance_km: 0,
|
||||||
|
estimated_duration_hours: 0,
|
||||||
|
status: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const BASE_URL = "http://localhost:8079/route-service/routes";
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const [routeRes, vehRes, frRes] = await Promise.all([
|
||||||
|
fetch(BASE_URL),
|
||||||
|
fetch("http://localhost:8079/vehicle-service/vehicles"),
|
||||||
|
fetch("http://localhost:8079/freight-service/freights"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
routes = await routeRes.json();
|
||||||
|
|
||||||
|
vehicles = (await vehRes.json()).map((v) => ({
|
||||||
|
value: v.id,
|
||||||
|
name: `${v.brand} ${v.model} (${v.license_plate})`,
|
||||||
|
}));
|
||||||
|
freights = (await frRes.json()).map((f) => ({
|
||||||
|
value: f.id,
|
||||||
|
name: f.name,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let errors = $derived.by(() => {
|
||||||
|
const e = {};
|
||||||
|
if (!formData.vehicle_id) e.vehicle_id = "Vehicle is required";
|
||||||
|
if (!formData.status.trim()) e.status = "Status is required";
|
||||||
|
if (formData.freight_id.length === 0) e.freight_id = "Select at least one freight";
|
||||||
|
if (!formData.start_location.trim()) e.start_location = "Origin required";
|
||||||
|
if (!formData.end_location.trim()) e.end_location = "Destination required";
|
||||||
|
if (formData.distance_km <= 0) e.distance_km = "Distance must be > 0";
|
||||||
|
if (formData.estimated_duration_hours <= 0) e.duration = "Duration must be > 0";
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function saveRoute() {
|
||||||
|
const method = editingId ? "PUT" : "POST";
|
||||||
|
const url = editingId ? `${BASE_URL}/${editingId}` : BASE_URL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
await fetchData();
|
||||||
|
closeModal();
|
||||||
|
} else {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.detail ?? res.statusText);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRoute(id) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/${id}`, { method: "DELETE" });
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.detail ?? res.statusText);
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(route = null) {
|
||||||
|
if (route) {
|
||||||
|
editingId = route.id;
|
||||||
|
formData = { ...route };
|
||||||
|
} else {
|
||||||
|
editingId = null;
|
||||||
|
formData = {
|
||||||
|
vehicle_id: "",
|
||||||
|
freight_id: [],
|
||||||
|
start_location: "",
|
||||||
|
end_location: "",
|
||||||
|
distance_km: 0,
|
||||||
|
estimated_duration_hours: 0,
|
||||||
|
status: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
isModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => (isModalOpen = false);
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
PLANNED: { color: "blue", label: "Planned" },
|
||||||
|
IN_PROGRESS: { color: "yellow", label: "In Progress" },
|
||||||
|
COMPLETED: { color: "green", label: "Completed" },
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(fetchData);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-8 space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<Heading
|
||||||
|
tag="h2"
|
||||||
|
class="text-2xl font-bold text-gray-800">Routes</Heading
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
onclick={() => openModal()}>+ Add Route</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
shadow
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<TableHead>
|
||||||
|
<TableHeadCell>ID</TableHeadCell>
|
||||||
|
<TableHeadCell>Path</TableHeadCell>
|
||||||
|
<TableHeadCell>Vehicle ID</TableHeadCell>
|
||||||
|
<TableHeadCell>Metrics</TableHeadCell>
|
||||||
|
<TableHeadCell>Status</TableHeadCell>
|
||||||
|
<TableHeadCell>Actions</TableHeadCell>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{#each routes as route (route.id)}
|
||||||
|
<TableBodyRow>
|
||||||
|
<TableBodyCell>{route.id}</TableBodyCell>
|
||||||
|
<TableBodyCell>
|
||||||
|
<span class="font-bold text-blue-600">{route.start_location}</span>
|
||||||
|
→
|
||||||
|
<span class="font-bold text-red-600">{route.end_location}</span>
|
||||||
|
</TableBodyCell>
|
||||||
|
<TableBodyCell>Vehicle #{route.vehicle_id}</TableBodyCell>
|
||||||
|
<TableBodyCell>
|
||||||
|
<div class="text-xs">{route.distance_km} km</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{route.estimated_duration_hours} hrs
|
||||||
|
</div>
|
||||||
|
</TableBodyCell>
|
||||||
|
<TableBodyCell>
|
||||||
|
<Badge color={statusMap[route.status].color}>{statusMap[route.status].label}</Badge>
|
||||||
|
</TableBodyCell>
|
||||||
|
<TableBodyCell class="space-x-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="alternative"
|
||||||
|
onclick={() => openModal(route)}>Edit</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="red"
|
||||||
|
onclick={() => deleteRoute(route.id)}>Delete</Button
|
||||||
|
>
|
||||||
|
</TableBodyCell>
|
||||||
|
</TableBodyRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editingId ? "Update Route" : "Add Route"}
|
||||||
|
bind:open={isModalOpen}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
color={errors.vehicle_id ? "red" : "default"}
|
||||||
|
class="mb-2">Assign Vehicle</Label
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
items={vehicles}
|
||||||
|
bind:value={formData.vehicle_id}
|
||||||
|
color={errors.vehicle_id ? "red" : "default"}
|
||||||
|
placeholder="Select vehicle..."
|
||||||
|
/>
|
||||||
|
{#if errors.vehicle_id}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.vehicle_id}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.status ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
items={Object.keys(statusMap).map((k) => ({
|
||||||
|
value: k,
|
||||||
|
name: statusMap[k].label,
|
||||||
|
}))}
|
||||||
|
bind:value={formData.status}
|
||||||
|
color={errors.status ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.status}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.status}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.freight_id ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Freights
|
||||||
|
</Label>
|
||||||
|
<MultiSelect
|
||||||
|
items={freights}
|
||||||
|
bind:value={formData.freight_id}
|
||||||
|
color={errors.freight_id ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.freight_id}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.freight_id}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.start_location ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Start Location
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
bind:value={formData.start_location}
|
||||||
|
color={errors.start_location ? "red" : "default"}
|
||||||
|
placeholder="City A"
|
||||||
|
/>
|
||||||
|
{#if errors.start_location}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.start_location}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.end_location ? "red" : "default"}
|
||||||
|
>
|
||||||
|
End Location
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
bind:value={formData.end_location}
|
||||||
|
placeholder="City B"
|
||||||
|
color={errors.end_location ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.end_location}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.end_location}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.distance_km ? "red" : "default"}
|
||||||
|
>
|
||||||
|
Distance (km)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
bind:value={formData.distance_km}
|
||||||
|
color={errors.distance_km ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.distance_km}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.distance_km}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
class="mb-2"
|
||||||
|
color={errors.estimated_duration_hours ? "red" : "default"}>Est. Time (hours)</Label
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.5"
|
||||||
|
bind:value={formData.estimated_duration_hours}
|
||||||
|
color={errors.estimated_duration_hours ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{#if errors.estimated_duration_hours}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errors.estimated_duration_hours}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-3 mt-6">
|
||||||
|
<Button
|
||||||
|
color="alternative"
|
||||||
|
onclick={closeModal}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
disabled={!(Object.keys(errors).length === 0)}
|
||||||
|
onclick={saveRoute}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import { Toast, ToastContainer } from "flowbite-svelte";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import toastManager from "./toastManager.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ToastContainer position="top-right">
|
||||||
|
{#each toastManager.toasts as toast (toast.id)}
|
||||||
|
<Toast
|
||||||
|
color={toast.color}
|
||||||
|
dismissable={true}
|
||||||
|
transition={fly}
|
||||||
|
params={{ x: 200, duration: 800 }}
|
||||||
|
class="w-64"
|
||||||
|
onclose={toastManager.handleClose(toast.id)}
|
||||||
|
bind:toastStatus={toast.visible}
|
||||||
|
>
|
||||||
|
{toast.message}
|
||||||
|
</Toast>
|
||||||
|
{/each}
|
||||||
|
</ToastContainer>
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
<script>
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableBodyCell,
|
||||||
|
TableBodyRow,
|
||||||
|
TableHead,
|
||||||
|
TableHeadCell,
|
||||||
|
Modal,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Badge,
|
||||||
|
Heading,
|
||||||
|
Helper,
|
||||||
|
} from "flowbite-svelte";
|
||||||
|
import toastManager from "./toastManager.svelte";
|
||||||
|
|
||||||
|
let vehicles = $state([]);
|
||||||
|
let isModalOpen = $state(false);
|
||||||
|
let editingId = $state(null);
|
||||||
|
|
||||||
|
let formData = $state({
|
||||||
|
brand: "",
|
||||||
|
model: "",
|
||||||
|
license_plate: "",
|
||||||
|
year: 0,
|
||||||
|
capacity_kg: 0,
|
||||||
|
status: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const BASE_URL = "http://localhost:8079/vehicle-service/vehicles";
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: "AVAILABLE", name: "Available" },
|
||||||
|
{ value: "MAINTENANCE", name: "Maintenance" },
|
||||||
|
{ value: "IN_TRANSIT", name: "In Transit" },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function fetchVehicles() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(BASE_URL);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(res.statusText);
|
||||||
|
}
|
||||||
|
vehicles = await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let errors = $derived.by(() => {
|
||||||
|
const e = {};
|
||||||
|
if (!formData.brand.trim()) e.brand = "Manufacturer is required";
|
||||||
|
if (!formData.model.trim()) e.model = "Model is required";
|
||||||
|
if (!formData.status.trim()) e.status = "Status is required";
|
||||||
|
if (!formData.license_plate.trim()) e.license_plate = "License plate required";
|
||||||
|
if (formData.capacity_kg <= 0) e.capacity_kg = "Capacity must be > 0";
|
||||||
|
if (formData.year <= 1980) e.year = "Year must be > 1980";
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function saveVehicle() {
|
||||||
|
const method = editingId ? "PUT" : "POST";
|
||||||
|
const url = editingId ? `${BASE_URL}/${editingId}` : BASE_URL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
await fetchVehicles();
|
||||||
|
closeModal();
|
||||||
|
} else {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.detail ?? res.statusText);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteVehicle(id) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/${id}`, { method: "DELETE" });
|
||||||
|
if (res.ok) {
|
||||||
|
fetchVehicles();
|
||||||
|
} else {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.detail ?? res.statusText);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toastManager.addToast(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(vehicle = null) {
|
||||||
|
if (vehicle) {
|
||||||
|
editingId = vehicle.id;
|
||||||
|
formData = { ...vehicle };
|
||||||
|
} else {
|
||||||
|
editingId = null;
|
||||||
|
formData = {
|
||||||
|
brand: "",
|
||||||
|
model: "",
|
||||||
|
license_plate: "",
|
||||||
|
year: 0,
|
||||||
|
capacity_kg: 0,
|
||||||
|
status: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
isModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
isModalOpen = false;
|
||||||
|
editingId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status) {
|
||||||
|
switch (status) {
|
||||||
|
case "AVAILABLE":
|
||||||
|
return "green";
|
||||||
|
case "MAINTENANCE":
|
||||||
|
return "yellow";
|
||||||
|
case "IN_TRANSIT":
|
||||||
|
return "indigo";
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => toastManager.onDestroy());
|
||||||
|
|
||||||
|
onMount(fetchVehicles);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-8 space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<Heading
|
||||||
|
tag="h2"
|
||||||
|
class="text-2xl font-bold">Vehicles</Heading
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
onclick={() => openModal()}>+ Add Vehicle</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
shadow
|
||||||
|
hoverable={true}
|
||||||
|
>
|
||||||
|
<TableHead>
|
||||||
|
<TableHeadCell>ID</TableHeadCell>
|
||||||
|
<TableHeadCell>Manufacturer/Model</TableHeadCell>
|
||||||
|
<TableHeadCell>License Plate</TableHeadCell>
|
||||||
|
<TableHeadCell>Year</TableHeadCell>
|
||||||
|
<TableHeadCell>Capacity (kg.)</TableHeadCell>
|
||||||
|
<TableHeadCell>Status</TableHeadCell>
|
||||||
|
<TableHeadCell>Actions</TableHeadCell>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{#each vehicles as vehicle (vehicle.id)}
|
||||||
|
<TableBodyRow>
|
||||||
|
<TableBodyCell>{vehicle.id}</TableBodyCell>
|
||||||
|
<TableBodyCell class="font-medium">{vehicle.brand} {vehicle.model}</TableBodyCell>
|
||||||
|
<TableBodyCell>{vehicle.license_plate}</TableBodyCell>
|
||||||
|
<TableBodyCell>{vehicle.year}</TableBodyCell>
|
||||||
|
<TableBodyCell>{vehicle.capacity_kg}</TableBodyCell>
|
||||||
|
<TableBodyCell>
|
||||||
|
<Badge color={getStatusColor(vehicle.status)}
|
||||||
|
>{statusOptions.filter((s) => s.value === vehicle.status).at(0).name}</Badge
|
||||||
|
>
|
||||||
|
</TableBodyCell>
|
||||||
|
<TableBodyCell class="space-x-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="alternative"
|
||||||
|
onclick={() => openModal(vehicle)}>Edit</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="red"
|
||||||
|
onclick={() => deleteVehicle(vehicle.id)}>Delete</Button
|
||||||
|
>
|
||||||
|
</TableBodyCell>
|
||||||
|
</TableBodyRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#snippet err(errs)}
|
||||||
|
{#if errs}
|
||||||
|
<Helper
|
||||||
|
class="mt-2"
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{errs}
|
||||||
|
</Helper>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editingId ? "Update Vehicle" : "Add Vehicle"}
|
||||||
|
bind:open={isModalOpen}
|
||||||
|
size="sm"
|
||||||
|
autoclose={false}
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="brand"
|
||||||
|
color={errors.brand ? "red" : "default"}
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
Manufacturer
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="brand"
|
||||||
|
color={errors.brand ? "red" : "default"}
|
||||||
|
bind:value={formData.brand}
|
||||||
|
placeholder="e.g. Mercedes"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{@render err(errors.brand)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="model"
|
||||||
|
color={errors.model ? "red" : "default"}
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
Model
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="model"
|
||||||
|
bind:value={formData.model}
|
||||||
|
color={errors.brand ? "red" : "default"}
|
||||||
|
placeholder="e.g. Actros"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{@render err(errors.model)}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="plate"
|
||||||
|
color={errors.license_plate ? "red" : "default"}
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
License Plate
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="plate"
|
||||||
|
color={errors.license_plate ? "red" : "default"}
|
||||||
|
bind:value={formData.license_plate}
|
||||||
|
placeholder="AA1234BB"
|
||||||
|
/>
|
||||||
|
{@render err(errors.license_plate)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="year"
|
||||||
|
color={errors.year ? "red" : "default"}
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
Year
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id="year"
|
||||||
|
color={errors.year ? "red" : "default"}
|
||||||
|
bind:value={formData.year}
|
||||||
|
/>
|
||||||
|
{@render err(errors.year)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="capacity"
|
||||||
|
color={errors.capacity_kg ? "red" : "default"}
|
||||||
|
class="mb-2">Capacity (kg.)</Label
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
color={errors.capacity_kg ? "red" : "default"}
|
||||||
|
id="capacity"
|
||||||
|
bind:value={formData.capacity_kg}
|
||||||
|
/>
|
||||||
|
{@render err(errors.capacity_kg)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
for="status"
|
||||||
|
color={errors.status ? "red" : "default"}
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
items={statusOptions}
|
||||||
|
bind:value={formData.status}
|
||||||
|
color={errors.status ? "red" : "default"}
|
||||||
|
/>
|
||||||
|
{@render err(errors.status)}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-3 pt-4">
|
||||||
|
<Button
|
||||||
|
color="alternative"
|
||||||
|
onclick={closeModal}>Cancel</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
disabled={!(Object.keys(errors).length === 0)}
|
||||||
|
onclick={saveVehicle}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
@plugin 'flowbite/plugin';
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary-50: #fff5f2;
|
||||||
|
--color-primary-100: #fff1ee;
|
||||||
|
--color-primary-200: #ffe4de;
|
||||||
|
--color-primary-300: #ffd5cc;
|
||||||
|
--color-primary-400: #ffbcad;
|
||||||
|
--color-primary-500: #fe795d;
|
||||||
|
--color-primary-600: #ef562f;
|
||||||
|
--color-primary-700: #eb4f27;
|
||||||
|
--color-primary-800: #cc4522;
|
||||||
|
--color-primary-900: #a5371b;
|
||||||
|
|
||||||
|
--color-secondary-50: #f0f9ff;
|
||||||
|
--color-secondary-100: #e0f2fe;
|
||||||
|
--color-secondary-200: #bae6fd;
|
||||||
|
--color-secondary-300: #7dd3fc;
|
||||||
|
--color-secondary-400: #38bdf8;
|
||||||
|
--color-secondary-500: #0ea5e9;
|
||||||
|
--color-secondary-600: #0284c7;
|
||||||
|
--color-secondary-700: #0369a1;
|
||||||
|
--color-secondary-800: #075985;
|
||||||
|
--color-secondary-900: #0c4a6e;
|
||||||
|
}
|
||||||
|
|
||||||
|
@source "../node_modules/flowbite-svelte/dist";
|
||||||
|
@source "../node_modules/flowbite-svelte-icons/dist";
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
/* disable chrome cancel button */
|
||||||
|
input[type="search"]::-webkit-search-cancel-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-gray-900 text-gray-100;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
import { mount } from "svelte";
|
||||||
|
import App from "./App.svelte";
|
||||||
|
|
||||||
|
const app = mount(App, {
|
||||||
|
target: document.getElementById("app"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
class ToastManager {
|
||||||
|
toasts = $state([]);
|
||||||
|
#nextId = $state(1);
|
||||||
|
|
||||||
|
addToast(msg) {
|
||||||
|
const selectedColor = "red";
|
||||||
|
const newToast = {
|
||||||
|
id: this.#nextId,
|
||||||
|
message: msg,
|
||||||
|
color: selectedColor,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-dismiss after 5 seconds
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
this.dismissToast(newToast.id);
|
||||||
|
}, 5000);
|
||||||
|
newToast.timeoutId = timeoutId;
|
||||||
|
|
||||||
|
this.toasts = [...this.toasts, newToast];
|
||||||
|
this.#nextId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissToast(id) {
|
||||||
|
const toast = this.toasts.find((t) => t.id === id);
|
||||||
|
if (toast?.timeoutId) {
|
||||||
|
clearTimeout(toast.timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toasts = this.toasts.map((t) => (t.id === id ? { ...t, visible: false } : t));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.toasts = this.toasts.filter((t) => t.id !== id);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose(id) {
|
||||||
|
return () => {
|
||||||
|
this.dismissToast(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
this.toasts.forEach((toast) => {
|
||||||
|
if (toast.timeoutId) {
|
||||||
|
clearTimeout(toast.timeoutId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ToastManager();
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
|
||||||
|
export default {
|
||||||
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
compilerOptions: {
|
||||||
|
runes: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./src/**/*.{html,js,svelte,ts}",
|
||||||
|
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
|
||||||
|
],
|
||||||
|
darkMode: "class", // This is the crucial line
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({ plugins: [tailwindcss(), svelte()] });
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
plugins {
|
||||||
|
id("java")
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "ua.com.dxrkness"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// spring
|
||||||
|
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||||
|
implementation(platform(libs.spring.cloud.dependencies))
|
||||||
|
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ua.com.dxrkness;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableEurekaServer
|
||||||
|
public class EurekaServerApplication {
|
||||||
|
static void main(String[] args) {
|
||||||
|
SpringApplication.run(EurekaServerApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
server:
|
||||||
|
port: 8070
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: eureka-server
|
||||||
|
eureka:
|
||||||
|
instance:
|
||||||
|
hostname: localhost
|
||||||
|
client:
|
||||||
|
register-with-eureka: false
|
||||||
|
fetch-registry: false
|
||||||
@@ -13,10 +13,10 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// spring
|
// spring
|
||||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||||
|
implementation(platform(libs.spring.cloud.dependencies))
|
||||||
|
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
|
||||||
implementation(libs.spring.boot.starter.web)
|
implementation(libs.spring.boot.starter.web)
|
||||||
implementation(libs.spring.boot.starter.web.test)
|
implementation(libs.spring.boot.starter.web.test)
|
||||||
implementation(libs.spring.boot.starter.hateoas)
|
|
||||||
developmentOnly(libs.spring.boot.devtools)
|
|
||||||
|
|
||||||
// http client
|
// http client
|
||||||
implementation(libs.apache.http.client)
|
implementation(libs.apache.http.client)
|
||||||
@@ -24,15 +24,15 @@ dependencies {
|
|||||||
// openapi docs
|
// openapi docs
|
||||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||||
|
|
||||||
// xml
|
|
||||||
implementation(libs.jackson.dataformat.xml)
|
|
||||||
|
|
||||||
// testing
|
// testing
|
||||||
testImplementation(platform(libs.junit.bom))
|
testImplementation(platform(libs.junit.bom))
|
||||||
testImplementation(libs.junit.jupiter)
|
testImplementation(libs.junit.jupiter)
|
||||||
testRuntimeOnly(libs.junit.platform.launcher)
|
testRuntimeOnly(libs.junit.platform.launcher)
|
||||||
|
|
||||||
implementation(libs.jspecify)
|
implementation(libs.jspecify)
|
||||||
|
|
||||||
|
implementation(project(":models"))
|
||||||
|
implementation(project(":shared"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
+2
-2
@@ -4,8 +4,8 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class LogisticsApplication {
|
public class FreightServiceApp {
|
||||||
static void main(String[] args) {
|
static void main(String[] args) {
|
||||||
SpringApplication.run(LogisticsApplication.class, args);
|
SpringApplication.run(FreightServiceApp.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-48
@@ -1,14 +1,13 @@
|
|||||||
package ua.com.dxrkness.controller;
|
package ua.com.dxrkness.controller;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.jspecify.annotations.NullMarked;
|
import org.jspecify.annotations.Nullable;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import ua.com.dxrkness.dto.FreightRequest;
|
||||||
import ua.com.dxrkness.model.Freight;
|
import ua.com.dxrkness.model.Freight;
|
||||||
import ua.com.dxrkness.service.FreightService;
|
import ua.com.dxrkness.service.FreightService;
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ import java.util.List;
|
|||||||
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
|
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
|
||||||
)
|
)
|
||||||
@Tag(name = "Freight", description = "Freights management")
|
@Tag(name = "Freight", description = "Freights management")
|
||||||
@NullMarked
|
|
||||||
public class FreightController {
|
public class FreightController {
|
||||||
private final FreightService service;
|
private final FreightService service;
|
||||||
|
|
||||||
@@ -31,18 +29,19 @@ public class FreightController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Retrieve all freights",
|
summary = "Retrieve all freights",
|
||||||
description = "Gets a list of all freight records in the system"
|
description = "Gets a list of all freight records in the system or filter by status"
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Successfully retrieved list of freights (may be empty!)",
|
description = "Successfully retrieved list of freights (may be empty!)"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||||
public List<Freight> getAll() {
|
public List<Freight> getAll(
|
||||||
|
@Parameter(description = "Filter freights by status (PENDING, IN_TRANSIT, DELIVERED)")
|
||||||
|
@RequestParam(name = "status", required = false) Freight.@Nullable Status status) {
|
||||||
|
if (status != null) {
|
||||||
|
return service.getByStatus(status);
|
||||||
|
}
|
||||||
return service.getAll();
|
return service.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,19 +51,15 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Successfully retrieved freight",
|
description = "Successfully retrieved freight"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "Freight not found"
|
description = "Freight not found"
|
||||||
)
|
)
|
||||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Freight> getById(@PathVariable("id") long id) {
|
public List<Freight> getById(@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.getById(id));
|
return List.of(service.getById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -73,19 +68,15 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Freight successfully created",
|
description = "Freight successfully created"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "400",
|
responseCode = "400",
|
||||||
description = "Invalid input"
|
description = "Invalid freight data (e.g., weight exceeds vehicle capacity)"
|
||||||
)
|
)
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<Freight> add(@RequestBody Freight newFreight) {
|
public Freight add(@RequestBody FreightRequest newFreight) {
|
||||||
return ResponseEntity.ok(service.add(newFreight));
|
return service.add(newFreight.toEntity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -94,19 +85,15 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Freight successfully updated",
|
description = "Freight successfully updated"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "400",
|
responseCode = "400",
|
||||||
description = "Invalid input"
|
description = "Invalid input"
|
||||||
)
|
)
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<Freight> update(@PathVariable("id") long id, @RequestBody Freight newFreight) {
|
public Freight update(@PathVariable("id") long id, @RequestBody FreightRequest newFreight) {
|
||||||
return ResponseEntity.ok(service.update(id, newFreight));
|
return service.update(id, newFreight.toEntity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -115,19 +102,15 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Freight successfully updated",
|
description = "Freight successfully updated"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "400",
|
responseCode = "400",
|
||||||
description = "Invalid input"
|
description = "Invalid input"
|
||||||
)
|
)
|
||||||
@PatchMapping("/{id}")
|
@PatchMapping("/{id}")
|
||||||
public ResponseEntity<Freight> updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) {
|
public Freight updatePatch(@PathVariable("id") long id, @RequestBody FreightRequest newFreight) {
|
||||||
return ResponseEntity.ok(service.update(id, newFreight));
|
return service.update(id, newFreight.toEntity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -136,18 +119,14 @@ public class FreightController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Freight successfully deleted",
|
description = "Freight successfully deleted"
|
||||||
content = @Content(
|
|
||||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
|
||||||
schema = @Schema(implementation = Freight.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "Freight not found"
|
description = "Freight not found"
|
||||||
)
|
)
|
||||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Freight> delete(@PathVariable("id") long id) {
|
public Freight delete(@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.delete(id));
|
return service.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package ua.com.dxrkness.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import ua.com.dxrkness.exception.FreightNotFoundException;
|
||||||
|
import ua.com.dxrkness.model.Freight;
|
||||||
|
import ua.com.dxrkness.repository.FreightRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public final class FreightService {
|
||||||
|
private final FreightRepository repo;
|
||||||
|
|
||||||
|
public FreightService(FreightRepository repo) {
|
||||||
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Freight> getAll() {
|
||||||
|
return repo.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Freight add(Freight freight) {
|
||||||
|
var newFreight = new Freight(repo.lastId(), freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status());
|
||||||
|
repo.add(newFreight);
|
||||||
|
return newFreight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Freight getById(long id) {
|
||||||
|
Freight freight = repo.getById(id);
|
||||||
|
if (freight == null) {
|
||||||
|
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return freight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Freight> getByStatus(Freight.Status status) {
|
||||||
|
return repo.getAll().stream()
|
||||||
|
.filter(f -> f.status() == status)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Freight update(long id, Freight newFreight) {
|
||||||
|
newFreight = new Freight(id, newFreight.name(), newFreight.description(), newFreight.weightKg(), newFreight.dimensions(), newFreight.status());
|
||||||
|
return repo.update(id, newFreight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Freight delete(long id) {
|
||||||
|
var deleted = repo.delete(id);
|
||||||
|
if (deleted == null) {
|
||||||
|
throw new FreightNotFoundException("Freight with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: freight-service
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
eureka:
|
||||||
|
client:
|
||||||
|
service-url:
|
||||||
|
defaultZone: http://localhost:8070/eureka
|
||||||
@@ -4,6 +4,8 @@ spring-boot-plugin = "4.0.0"
|
|||||||
jspecify = "1.0.0"
|
jspecify = "1.0.0"
|
||||||
springdoc = "3.0.0"
|
springdoc = "3.0.0"
|
||||||
apache-http-client = "4.5.14"
|
apache-http-client = "4.5.14"
|
||||||
|
json-schema-validator = "3.0.0"
|
||||||
|
spring-cloud = "2025.1.0"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-plugin" }
|
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-plugin" }
|
||||||
@@ -14,19 +16,24 @@ spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-b
|
|||||||
spring-boot-starter-web-test = { group = "org.springframework.boot", name = "spring-boot-starter-webmvc-test" }
|
spring-boot-starter-web-test = { group = "org.springframework.boot", name = "spring-boot-starter-webmvc-test" }
|
||||||
spring-boot-starter-hateoas = { group = "org.springframework.boot", name = "spring-boot-starter-hateoas" }
|
spring-boot-starter-hateoas = { group = "org.springframework.boot", name = "spring-boot-starter-hateoas" }
|
||||||
spring-boot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" }
|
spring-boot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" }
|
||||||
|
spring-cloud-dependencies = { group = "org.springframework.cloud", name = "spring-cloud-dependencies", version.ref = "spring-cloud" }
|
||||||
|
|
||||||
# http client
|
# http client
|
||||||
apache-http-client = { group = "org.apache.httpcomponents", name = "httpclient", version.ref ="apache-http-client" }
|
apache-http-client = { group = "org.apache.httpcomponents", name = "httpclient", version.ref = "apache-http-client" }
|
||||||
|
|
||||||
# openapi
|
# openapi
|
||||||
springdoc-openapi-starter-webmvc-ui = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" }
|
springdoc-openapi-starter-webmvc-ui = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" }
|
||||||
|
|
||||||
# xml
|
# jackson
|
||||||
jackson-dataformat-xml = { group = "tools.jackson.dataformat", name = "jackson-dataformat-xml" }
|
jackson-dataformat-xml = { group = "tools.jackson.dataformat", name = "jackson-dataformat-xml" }
|
||||||
|
oldjackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind" }
|
||||||
|
oldjackson-dataformat-xml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-xml" }
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit" }
|
junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit" }
|
||||||
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter" }
|
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter" }
|
||||||
junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
|
junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
|
||||||
|
|
||||||
jspecify = { group = "org.jspecify", name = "jspecify", version.ref = "jspecify" }
|
jspecify = { group = "org.jspecify", name = "jspecify", version.ref = "jspecify" }
|
||||||
|
|
||||||
|
json-schema-validator = { group = 'com.networknt', name = 'json-schema-validator', version.ref = 'json-schema-validator' }
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
plugins {
|
||||||
|
`java-library`
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "ua.com.dxrkness"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// spring
|
||||||
|
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||||
|
|
||||||
|
// jackson
|
||||||
|
api(libs.jackson.dataformat.xml)
|
||||||
|
api(libs.oldjackson.dataformat.xml)
|
||||||
|
implementation(libs.oldjackson.databind)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ua.com.dxrkness.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import ua.com.dxrkness.model.Freight;
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
public record FreightDto(
|
||||||
|
long id,
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
int weightKg,
|
||||||
|
Freight.Dimensions dimensions,
|
||||||
|
Freight.Status status
|
||||||
|
) {
|
||||||
|
public static FreightDto fromFreight(Freight freight) {
|
||||||
|
return new FreightDto(
|
||||||
|
freight.id(),
|
||||||
|
freight.name(),
|
||||||
|
freight.description(),
|
||||||
|
freight.weightKg(),
|
||||||
|
freight.dimensions(),
|
||||||
|
freight.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ua.com.dxrkness.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import ua.com.dxrkness.model.Freight;
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
public record FreightRequest(
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
int weightKg,
|
||||||
|
Freight.Dimensions dimensions,
|
||||||
|
Freight.Status status
|
||||||
|
) {
|
||||||
|
public Freight toEntity() {
|
||||||
|
return new Freight(0, name, description, weightKg, dimensions, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package ua.com.dxrkness.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import ua.com.dxrkness.model.Route;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
public record RouteDto(
|
||||||
|
long id,
|
||||||
|
long vehicleId,
|
||||||
|
List<Long> freightId,
|
||||||
|
String startLocation,
|
||||||
|
String endLocation,
|
||||||
|
Double distanceKm,
|
||||||
|
Double estimatedDurationHours,
|
||||||
|
Route.Status status
|
||||||
|
) {
|
||||||
|
public static RouteDto fromRoute(Route route) {
|
||||||
|
return new RouteDto(
|
||||||
|
route.id(),
|
||||||
|
route.vehicleId(),
|
||||||
|
route.freightId(),
|
||||||
|
route.startLocation(),
|
||||||
|
route.endLocation(),
|
||||||
|
route.distanceKm(),
|
||||||
|
route.estimatedDurationHours(),
|
||||||
|
route.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ua.com.dxrkness.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import ua.com.dxrkness.model.Route;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
public record RouteRequest(long vehicleId,
|
||||||
|
List<Long> freightId,
|
||||||
|
String startLocation,
|
||||||
|
String endLocation,
|
||||||
|
Double distanceKm,
|
||||||
|
Double estimatedDurationHours,
|
||||||
|
Route.Status status) {
|
||||||
|
public Route toEntity() {
|
||||||
|
return new Route(0, vehicleId, freightId, startLocation, endLocation, distanceKm, estimatedDurationHours, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ua.com.dxrkness.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
public record VehicleDto(
|
||||||
|
long id,
|
||||||
|
String brand,
|
||||||
|
String model,
|
||||||
|
String licensePlate,
|
||||||
|
int year,
|
||||||
|
int capacityKg,
|
||||||
|
Vehicle.Status status
|
||||||
|
) {
|
||||||
|
public static VehicleDto fromVehicle(Vehicle vehicle) {
|
||||||
|
return new VehicleDto(
|
||||||
|
vehicle.id(),
|
||||||
|
vehicle.brand(),
|
||||||
|
vehicle.model(),
|
||||||
|
vehicle.licensePlate(),
|
||||||
|
vehicle.year(),
|
||||||
|
vehicle.capacityKg(),
|
||||||
|
vehicle.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package ua.com.dxrkness.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
public record VehicleRequest(
|
||||||
|
String brand,
|
||||||
|
String model,
|
||||||
|
String licensePlate,
|
||||||
|
int year,
|
||||||
|
int capacityKg,
|
||||||
|
Vehicle.Status status
|
||||||
|
) {
|
||||||
|
public Vehicle toEntity() {
|
||||||
|
return new Vehicle(0, brand, model, licensePlate, year, capacityKg, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -1,15 +1,19 @@
|
|||||||
package ua.com.dxrkness.model;
|
package ua.com.dxrkness.model;
|
||||||
|
|
||||||
import tools.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import tools.jackson.databind.annotation.JsonNaming;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
public record Freight(long id,
|
public record Freight(long id,
|
||||||
String name,
|
String name,
|
||||||
String description,
|
String description,
|
||||||
int weightKg,
|
int weightKg,
|
||||||
Dimensions dimensions,
|
Dimensions dimensions,
|
||||||
Status status) implements Identifiable {
|
Status status) implements Identifiable {
|
||||||
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
public record Dimensions(int widthCm,
|
public record Dimensions(int widthCm,
|
||||||
int heightCm,
|
int heightCm,
|
||||||
int lengthCm) {
|
int lengthCm) {
|
||||||
+4
-2
@@ -1,11 +1,13 @@
|
|||||||
package ua.com.dxrkness.model;
|
package ua.com.dxrkness.model;
|
||||||
|
|
||||||
import tools.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import tools.jackson.databind.annotation.JsonNaming;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
public record Route(long id,
|
public record Route(long id,
|
||||||
long vehicleId,
|
long vehicleId,
|
||||||
List<Long> freightId,
|
List<Long> freightId,
|
||||||
+3
-2
@@ -1,9 +1,10 @@
|
|||||||
package ua.com.dxrkness.model;
|
package ua.com.dxrkness.model;
|
||||||
|
|
||||||
import tools.jackson.databind.PropertyNamingStrategies;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
import tools.jackson.databind.annotation.JsonNaming;
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
|
@tools.jackson.databind.annotation.JsonNaming(tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||||
public record Vehicle(long id,
|
public record Vehicle(long id,
|
||||||
String brand,
|
String brand,
|
||||||
String model,
|
String model,
|
||||||
Executable
+93
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env fish
|
||||||
|
|
||||||
|
function publish_correct
|
||||||
|
set vehicle '{
|
||||||
|
"id": 444,
|
||||||
|
"brand": "test vehicle",
|
||||||
|
"model": "test vehicle",
|
||||||
|
"license_plate": "AB4444AB",
|
||||||
|
"year": 1980,
|
||||||
|
"capacity_kg": 3321,
|
||||||
|
"status": "AVAILABLE"
|
||||||
|
}'
|
||||||
|
|
||||||
|
set freight1 '{
|
||||||
|
"id": 444,
|
||||||
|
"name": "test freight",
|
||||||
|
"description": "test freight",
|
||||||
|
"weight_kg": 444,
|
||||||
|
"dimensions": {
|
||||||
|
"width_cm": 4,
|
||||||
|
"height_cm": 4,
|
||||||
|
"length_cm": 4
|
||||||
|
},
|
||||||
|
"status": "PENDING"
|
||||||
|
}'
|
||||||
|
|
||||||
|
set freight2 '{
|
||||||
|
"id": 555,
|
||||||
|
"name": "test freight 2",
|
||||||
|
"description": "test freight 2",
|
||||||
|
"weight_kg": 555,
|
||||||
|
"dimensions": {
|
||||||
|
"width_cm": 8,
|
||||||
|
"height_cm": 8,
|
||||||
|
"length_cm": 8
|
||||||
|
},
|
||||||
|
"status": "IN_TRANSIT"
|
||||||
|
}'
|
||||||
|
|
||||||
|
set route '{
|
||||||
|
"id": 444,
|
||||||
|
"vehicle_id": 1,
|
||||||
|
"freight_id": [
|
||||||
|
1, 2
|
||||||
|
],
|
||||||
|
"start_location": "string",
|
||||||
|
"end_location": "string",
|
||||||
|
"distance_km": 0.1,
|
||||||
|
"estimated_duration_hours": 0.1,
|
||||||
|
"status": "PLANNED"
|
||||||
|
}'
|
||||||
|
|
||||||
|
printf '\n\n'
|
||||||
|
curl -X POST http://localhost:8079/vehicle-service/vehicles \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d $vehicle
|
||||||
|
|
||||||
|
printf '\n\n'
|
||||||
|
curl -X POST http://localhost:8079/freight-service/freights \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d $freight1
|
||||||
|
|
||||||
|
printf '\n\n'
|
||||||
|
curl -X POST http://localhost:8079/freight-service/freights \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d $freight2
|
||||||
|
|
||||||
|
printf '\n\n'
|
||||||
|
curl -X POST http://localhost:8079/route-service/routes \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d $route
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function add_invalid_route
|
||||||
|
set route '{
|
||||||
|
"id": 444,
|
||||||
|
"vehicle_id": 444,
|
||||||
|
"freight_id": [
|
||||||
|
1, 2
|
||||||
|
],
|
||||||
|
"start_location": "string",
|
||||||
|
"end_location": "string",
|
||||||
|
"distance_km": 0.1,
|
||||||
|
"estimated_duration_hours": 0.1,
|
||||||
|
"status": "PLANNED"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 404 status is expected, since vehicle svc propagates it's error code to route svc
|
||||||
|
curl -X POST http://localhost:8082/routes \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d $route
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
plugins {
|
||||||
|
id("java")
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "ua.com.dxrkness"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// spring
|
||||||
|
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||||
|
implementation(platform(libs.spring.cloud.dependencies))
|
||||||
|
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
|
||||||
|
implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")
|
||||||
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
implementation(libs.spring.boot.starter.web.test)
|
||||||
|
|
||||||
|
// http client
|
||||||
|
implementation(libs.apache.http.client)
|
||||||
|
|
||||||
|
// openapi docs
|
||||||
|
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||||
|
|
||||||
|
// testing
|
||||||
|
testImplementation(platform(libs.junit.bom))
|
||||||
|
testImplementation(libs.junit.jupiter)
|
||||||
|
testRuntimeOnly(libs.junit.platform.launcher)
|
||||||
|
|
||||||
|
implementation(libs.jspecify)
|
||||||
|
|
||||||
|
implementation(project(":models"))
|
||||||
|
implementation(project(":shared"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ua.com.dxrkness;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class RouteServiceApp {
|
||||||
|
static void main(String[] args) {
|
||||||
|
SpringApplication.run(RouteServiceApp.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package ua.com.dxrkness.client;
|
||||||
|
|
||||||
|
import com.networknt.schema.InputFormat;
|
||||||
|
import com.networknt.schema.Schema;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
import ua.com.dxrkness.model.Freight;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class FreightClient {
|
||||||
|
private final RestClient freightRestClient;
|
||||||
|
private final Schema freightSchema;
|
||||||
|
private final JsonMapper mapper = JsonMapper.shared();
|
||||||
|
|
||||||
|
public FreightClient(@Qualifier("freightRestClient") RestClient.Builder freightRestClient, Schema freightSchema) {
|
||||||
|
this.freightRestClient = freightRestClient.build();
|
||||||
|
this.freightSchema = freightSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Freight getById(long id) {
|
||||||
|
var resp = freightRestClient.get().uri("/{id}", id)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
|
||||||
|
throw new ResponseStatusException(res.getStatusCode(), res.getStatusText());
|
||||||
|
})
|
||||||
|
.body(String.class);
|
||||||
|
System.out.println(resp);
|
||||||
|
List<com.networknt.schema.Error> errs = freightSchema.validate(resp, InputFormat.JSON);
|
||||||
|
if (!errs.isEmpty()) {
|
||||||
|
System.out.println("Some errors have occured!");
|
||||||
|
errs.forEach(err -> {
|
||||||
|
System.out.print(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return mapper.readValue(resp, new TypeReference<List<Freight>>() {
|
||||||
|
}).getFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package ua.com.dxrkness.client;
|
||||||
|
|
||||||
|
import com.networknt.schema.Error;
|
||||||
|
import com.networknt.schema.InputFormat;
|
||||||
|
import com.networknt.schema.Schema;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class VehicleClient {
|
||||||
|
private final RestClient vehicleRestClient;
|
||||||
|
private final Schema vehicleSchema;
|
||||||
|
private final JsonMapper mapper = JsonMapper.shared();
|
||||||
|
|
||||||
|
public VehicleClient(@Qualifier("vehicleRestClient") RestClient.Builder vehicleRestClient, Schema vehicleSchema) {
|
||||||
|
this.vehicleRestClient = vehicleRestClient.build();
|
||||||
|
this.vehicleSchema = vehicleSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vehicle getById(long id) {
|
||||||
|
var resp = vehicleRestClient.get().uri("/{id}", id)
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
|
||||||
|
throw new ResponseStatusException(res.getStatusCode(), res.getStatusText());
|
||||||
|
})
|
||||||
|
.body(String.class);
|
||||||
|
System.out.println(resp);
|
||||||
|
List<Error> errs = vehicleSchema.validate(resp, InputFormat.JSON);
|
||||||
|
if (!errs.isEmpty()) {
|
||||||
|
System.out.println("Some errors have occured!");
|
||||||
|
errs.forEach(err -> {
|
||||||
|
System.out.print(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return mapper.readValue(resp, new TypeReference<List<Vehicle>>() {
|
||||||
|
}).getFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ua.com.dxrkness.config;
|
||||||
|
|
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class ClientConfiguration {
|
||||||
|
@Bean
|
||||||
|
@LoadBalanced
|
||||||
|
public RestClient.Builder vehicleRestClient() {
|
||||||
|
return RestClient.builder().baseUrl("http://vehicle-service/vehicles");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@LoadBalanced
|
||||||
|
public RestClient.Builder freightRestClient() {
|
||||||
|
return RestClient.builder().baseUrl("http://freight-service/freights");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public RestClient.Builder defaultRestClient() {
|
||||||
|
return RestClient.builder();
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
-114
@@ -2,18 +2,21 @@ package ua.com.dxrkness.controller;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.jspecify.annotations.NullMarked;
|
import org.jspecify.annotations.NullMarked;
|
||||||
import org.jspecify.annotations.Nullable;
|
import org.jspecify.annotations.Nullable;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import ua.com.dxrkness.client.FreightClient;
|
||||||
|
import ua.com.dxrkness.client.VehicleClient;
|
||||||
|
import ua.com.dxrkness.dto.RouteRequest;
|
||||||
|
import ua.com.dxrkness.model.Freight;
|
||||||
import ua.com.dxrkness.model.Route;
|
import ua.com.dxrkness.model.Route;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
import ua.com.dxrkness.service.RouteService;
|
import ua.com.dxrkness.service.RouteService;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -25,128 +28,48 @@ import java.util.List;
|
|||||||
@Tag(name = "Route", description = "Routes management")
|
@Tag(name = "Route", description = "Routes management")
|
||||||
@NullMarked
|
@NullMarked
|
||||||
public class RouteController {
|
public class RouteController {
|
||||||
private final RouteService service;
|
private final RouteService routeService;
|
||||||
|
private final VehicleClient vehicleClient;
|
||||||
|
private final FreightClient freightClient;
|
||||||
|
|
||||||
public RouteController(RouteService service) {
|
public RouteController(RouteService routeService,
|
||||||
this.service = service;
|
VehicleClient vehicleClient,
|
||||||
|
FreightClient freightClient) {
|
||||||
|
this.routeService = routeService;
|
||||||
|
this.vehicleClient = vehicleClient;
|
||||||
|
this.freightClient = freightClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get routes",
|
summary = "Get routes",
|
||||||
description = "Get all routes or filter by freight ID/vehicle ID. May result in empty list!"
|
description = "Get all routes or filter by freight ID/vehicle ID/status. May result in empty list!"
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Successfully retrieved routes",
|
description = "Successfully retrieved routes"
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||||
public List<Route> getAll(
|
public List<Route> getAll(
|
||||||
@Parameter(description = "Filter routes by freight ID")
|
@Parameter(description = "Filter routes by freight ID")
|
||||||
@RequestParam(name = "freightId", required = false) @Nullable Long freightId,
|
@RequestParam(name = "freightId", required = false) @Nullable Long freightId,
|
||||||
@Parameter(description = "Filter routes by vehicle ID")
|
@Parameter(description = "Filter routes by vehicle ID")
|
||||||
@RequestParam(name = "vehicleId", required = false) @Nullable Long vehicleId) {
|
@RequestParam(name = "vehicleId", required = false) @Nullable Long vehicleId,
|
||||||
|
@Parameter(description = "Filter routes by status (PLANNED, IN_PROGRESS, COMPLETED, CANCELLED)")
|
||||||
|
@RequestParam(name = "status", required = false) Route.@Nullable Status status) {
|
||||||
|
if (status != null) {
|
||||||
|
return routeService.getByStatus(status);
|
||||||
|
}
|
||||||
if (vehicleId != null) {
|
if (vehicleId != null) {
|
||||||
return service.getByVehicleId(vehicleId);
|
return routeService.getByVehicleId(vehicleId);
|
||||||
}
|
}
|
||||||
if (freightId != null) {
|
if (freightId != null) {
|
||||||
var route = service.getByFreightId(freightId);
|
var route = routeService.getByFreightId(freightId);
|
||||||
if (route == null) {
|
if (route == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return List.of(route);
|
return List.of(route);
|
||||||
}
|
}
|
||||||
return service.getAll();
|
return routeService.getAll();
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Create new route",
|
|
||||||
description = "Add new route"
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "Route successfully created",
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "Invalid route data provided"
|
|
||||||
)
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity<Route> add(
|
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
|
||||||
description = "Route object to be created",
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@RequestBody Route newRoute) {
|
|
||||||
return ResponseEntity.ok(service.add(newRoute));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Update a route (full)",
|
|
||||||
description = "Update all fields of an existing route by ID"
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "Route successfully updated",
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "Invalid route data provided"
|
|
||||||
)
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
public ResponseEntity<Route> update(
|
|
||||||
@Parameter(description = "ID of the route to update", required = true)
|
|
||||||
@PathVariable("id") long id,
|
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
|
||||||
description = "Updated route object",
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@RequestBody Route newRoute) {
|
|
||||||
return ResponseEntity.ok(service.update(id, newRoute));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Update a route (partial)",
|
|
||||||
description = "Update specific fields of an existing route by ID"
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "Route successfully updated",
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "400",
|
|
||||||
description = "Invalid route data provided"
|
|
||||||
)
|
|
||||||
@PatchMapping("/{id}")
|
|
||||||
public ResponseEntity<Route> updatePatch(
|
|
||||||
@Parameter(description = "ID of the route to update", required = true)
|
|
||||||
@PathVariable("id") long id,
|
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
|
||||||
description = "Route object with fields to update",
|
|
||||||
required = true,
|
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@RequestBody Route newRoute) {
|
|
||||||
return ResponseEntity.ok(service.update(id, newRoute));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -155,40 +78,151 @@ public class RouteController {
|
|||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Route found",
|
description = "Route found"
|
||||||
content = @Content(
|
|
||||||
schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "Route not found"
|
description = "Route not found"
|
||||||
)
|
)
|
||||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Route> getById(
|
public Route getById(
|
||||||
@Parameter(description = "ID of the route to retrieve", required = true)
|
@Parameter(description = "ID of the route to retrieve", required = true)
|
||||||
@PathVariable("id") long id) {
|
@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.getById(id));
|
return routeService.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Retrieve a vehicle by route ID"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route and vehicle found"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "Route or vehicle not found"
|
||||||
|
)
|
||||||
|
@GetMapping(value = "/{id}/vehicle", consumes = MediaType.ALL_VALUE)
|
||||||
|
public Vehicle getVehicleById(
|
||||||
|
@Parameter(description = "ID of the route to retrieve", required = true)
|
||||||
|
@PathVariable("id") long id) {
|
||||||
|
return vehicleClient.getById(routeService.getById(id).vehicleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Retrieve freights by route ID"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route and freights found"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "Route or freight not found"
|
||||||
|
)
|
||||||
|
@GetMapping(value = "/{id}/freights", consumes = MediaType.ALL_VALUE)
|
||||||
|
public List<Freight> getFreightsById(
|
||||||
|
@Parameter(description = "ID of the route to retrieve", required = true)
|
||||||
|
@PathVariable("id") long id) {
|
||||||
|
var route = routeService.getById(id);
|
||||||
|
var freights = new ArrayList<Freight>();
|
||||||
|
for (var freightId : route.freightId()) {
|
||||||
|
freights.add(freightClient.getById(freightId));
|
||||||
|
}
|
||||||
|
return freights;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Create new route",
|
||||||
|
description = "Add new route"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route successfully created"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "400",
|
||||||
|
description = "Invalid route data: vehicle or freight not found"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "Referenced vehicle or freight not found"
|
||||||
|
)
|
||||||
|
@PostMapping
|
||||||
|
public Route add(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "Route object to be created. Must include valid vehicleId and freightId list.",
|
||||||
|
required = true
|
||||||
|
)
|
||||||
|
@RequestBody RouteRequest newRoute) {
|
||||||
|
return routeService.add(newRoute.toEntity());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Update a route (full)",
|
||||||
|
description = "Update all fields of an existing route by ID"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route successfully updated"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "400",
|
||||||
|
description = "Invalid route data provided"
|
||||||
|
)
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Route update(
|
||||||
|
@Parameter(description = "ID of the route to update", required = true)
|
||||||
|
@PathVariable("id") long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "Updated route object",
|
||||||
|
required = true
|
||||||
|
)
|
||||||
|
@RequestBody RouteRequest newRoute) {
|
||||||
|
return routeService.update(id, newRoute.toEntity());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Update a route (partial)",
|
||||||
|
description = "Update specific fields of an existing route by ID"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Route successfully updated"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "400",
|
||||||
|
description = "Invalid route data provided"
|
||||||
|
)
|
||||||
|
@PatchMapping("/{id}")
|
||||||
|
public Route updatePatch(
|
||||||
|
@Parameter(description = "ID of the route to update", required = true)
|
||||||
|
@PathVariable("id") long id,
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "Route object with fields to update",
|
||||||
|
required = true
|
||||||
|
)
|
||||||
|
@RequestBody RouteRequest newRoute) {
|
||||||
|
return routeService.update(id, newRoute.toEntity());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Delete a route",
|
summary = "Delete a route",
|
||||||
description = "Delete a route by ID"
|
description = "Delete a route by ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = "200",
|
||||||
description = "Route successfully deleted",
|
description = "Route successfully deleted"
|
||||||
content = @Content(schema = @Schema(implementation = Route.class)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
responseCode = "404",
|
responseCode = "404",
|
||||||
description = "Route not found"
|
description = "Route not found"
|
||||||
)
|
)
|
||||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Route> delete(
|
public Route delete(
|
||||||
@Parameter(description = "ID of the route to delete", required = true)
|
@Parameter(description = "ID of the route to delete", required = true)
|
||||||
@PathVariable("id") long id) {
|
@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.delete(id));
|
return routeService.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package ua.com.dxrkness.service;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import ua.com.dxrkness.client.FreightClient;
|
||||||
|
import ua.com.dxrkness.client.VehicleClient;
|
||||||
|
import ua.com.dxrkness.dto.FreightDto;
|
||||||
|
import ua.com.dxrkness.dto.VehicleDto;
|
||||||
|
import ua.com.dxrkness.exception.FreightNotFoundException;
|
||||||
|
import ua.com.dxrkness.exception.RouteNotFoundException;
|
||||||
|
import ua.com.dxrkness.exception.VehicleNotFoundException;
|
||||||
|
import ua.com.dxrkness.model.Route;
|
||||||
|
import ua.com.dxrkness.repository.RouteRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public final class RouteService {
|
||||||
|
private final RouteRepository repo;
|
||||||
|
private final VehicleClient vehicleClient;
|
||||||
|
private final FreightClient freightClient;
|
||||||
|
|
||||||
|
public RouteService(RouteRepository repo,
|
||||||
|
VehicleClient vehicleClient,
|
||||||
|
FreightClient freightClient) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.vehicleClient = vehicleClient;
|
||||||
|
this.freightClient = freightClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Route> getAll() {
|
||||||
|
return repo.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Route getById(long id) {
|
||||||
|
Route route = repo.getById(id);
|
||||||
|
if (route == null) {
|
||||||
|
throw new RouteNotFoundException("Route with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Route add(Route route) {
|
||||||
|
VehicleDto vehicleDto = validateAndGetVehicle(route.vehicleId());
|
||||||
|
List<FreightDto> freightDtos = validateAndGetFreights(route.freightId());
|
||||||
|
|
||||||
|
var newRoute = new Route(repo.lastId(),
|
||||||
|
route.vehicleId(),
|
||||||
|
route.freightId(),
|
||||||
|
route.startLocation(),
|
||||||
|
route.endLocation(),
|
||||||
|
route.distanceKm(),
|
||||||
|
route.estimatedDurationHours(),
|
||||||
|
route.status());
|
||||||
|
repo.add(newRoute);
|
||||||
|
return newRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VehicleDto validateAndGetVehicle(long vehicleId) {
|
||||||
|
try {
|
||||||
|
var vehicle = vehicleClient.getById(vehicleId);
|
||||||
|
return VehicleDto.fromVehicle(vehicle);
|
||||||
|
} catch (VehicleNotFoundException e) {
|
||||||
|
throw new VehicleNotFoundException(
|
||||||
|
"Cannot create or update route: Referenced vehicle with ID " + vehicleId + " not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FreightDto> validateAndGetFreights(List<Long> freightIds) {
|
||||||
|
List<FreightDto> freightDtos = new java.util.ArrayList<>();
|
||||||
|
for (long freightId : freightIds) {
|
||||||
|
try {
|
||||||
|
var freight = freightClient.getById(freightId);
|
||||||
|
freightDtos.add(FreightDto.fromFreight(freight));
|
||||||
|
} catch (FreightNotFoundException e) {
|
||||||
|
throw new FreightNotFoundException(
|
||||||
|
"Cannot create or update route: Referenced freight with ID " + freightId + " not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return freightDtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable Route getByFreightId(long freightId) {
|
||||||
|
return repo.getByFreightId(freightId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Route> getByVehicleId(long vehicleId) {
|
||||||
|
return repo.getByVehicleId(vehicleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Route> getByStatus(Route.Status status) {
|
||||||
|
return repo.getAll().stream()
|
||||||
|
.filter(r -> r.status() == status)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Route update(long id, Route newRoute) {
|
||||||
|
VehicleDto vehicleDto = validateAndGetVehicle(newRoute.vehicleId());
|
||||||
|
List<FreightDto> freightDtos = validateAndGetFreights(newRoute.freightId());
|
||||||
|
|
||||||
|
newRoute = new Route(id,
|
||||||
|
newRoute.vehicleId(),
|
||||||
|
newRoute.freightId(),
|
||||||
|
newRoute.startLocation(),
|
||||||
|
newRoute.endLocation(),
|
||||||
|
newRoute.distanceKm(),
|
||||||
|
newRoute.estimatedDurationHours(),
|
||||||
|
newRoute.status());
|
||||||
|
return repo.update(id, newRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Route delete(long id) {
|
||||||
|
var deleted = repo.delete(id);
|
||||||
|
if (deleted == null) {
|
||||||
|
throw new RouteNotFoundException("Route with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: route-service
|
||||||
|
cloud:
|
||||||
|
loadbalancer:
|
||||||
|
cache:
|
||||||
|
enabled: false
|
||||||
|
server:
|
||||||
|
port: 8082
|
||||||
|
eureka:
|
||||||
|
client:
|
||||||
|
service-url:
|
||||||
|
defaultZone: http://localhost:8070/eureka
|
||||||
+3
-1
@@ -1,2 +1,4 @@
|
|||||||
rootProject.name = "itroi"
|
rootProject.name = "itroi"
|
||||||
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
|
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
|
||||||
|
|
||||||
|
include("models", "vehicle-service", "route-service", "freight-service", "shared", "api-gateway", "eureka-server")
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
plugins {
|
||||||
|
`java-library`
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "ua.com.dxrkness"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// spring
|
||||||
|
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||||
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
|
||||||
|
// http client
|
||||||
|
implementation(libs.apache.http.client)
|
||||||
|
|
||||||
|
// xml
|
||||||
|
implementation(libs.jackson.dataformat.xml)
|
||||||
|
|
||||||
|
implementation(libs.jspecify)
|
||||||
|
|
||||||
|
implementation(project(":models"))
|
||||||
|
|
||||||
|
api(libs.json.schema.validator);
|
||||||
|
|
||||||
|
implementation("org.jruby.joni:joni:2.2.6")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
package ua.com.dxrkness.client;
|
||||||
|
|
||||||
|
import org.apache.http.client.methods.*;
|
||||||
|
import org.apache.http.entity.StringEntity;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
import tools.jackson.dataformat.xml.XmlMapper;
|
||||||
|
import ua.com.dxrkness.model.Freight;
|
||||||
|
import ua.com.dxrkness.model.Route;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ApplicationClient {
|
||||||
|
private static final String BASE_URL = "http://localhost:8079";
|
||||||
|
private static final JsonMapper jsonMapper = JsonMapper.builder().build();
|
||||||
|
private static final XmlMapper xmlMapper = XmlMapper.builder().build();
|
||||||
|
|
||||||
|
static void main() {
|
||||||
|
try (CloseableHttpClient client = HttpClients.createDefault()) {
|
||||||
|
testVehicles(client);
|
||||||
|
testFreights(client);
|
||||||
|
testRoutes(client);
|
||||||
|
testSubResources(client);
|
||||||
|
testStatusFiltering(client);
|
||||||
|
testErrorCases(client);
|
||||||
|
testInterServiceValidation(client);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
IO.println("error: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testVehicles(CloseableHttpClient client) {
|
||||||
|
IO.println("--- VEHICLES ---");
|
||||||
|
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles"), "GET all vehicles");
|
||||||
|
|
||||||
|
Vehicle newVehicle = new Vehicle(0, "Mercedes", "Actros", "AA1234BB", 2023, 18000, Vehicle.Status.AVAILABLE);
|
||||||
|
String vehicleXml = xmlMapper.writeValueAsString(newVehicle);
|
||||||
|
HttpPost post = new HttpPost(BASE_URL + "/vehicle-service/vehicles");
|
||||||
|
post.setEntity(new StringEntity(vehicleXml, "UTF-8"));
|
||||||
|
post.setHeader("Content-Type", "application/xml");
|
||||||
|
post.setHeader("Accept", "application/xml");
|
||||||
|
String createdVehicle = execute(client, post, "POST new vehicle");
|
||||||
|
|
||||||
|
var get = new HttpGet(BASE_URL + "/vehicle-service/vehicles/1");
|
||||||
|
get.setHeader("Content-Type", "application/xml");
|
||||||
|
execute(client, get, "GET vehicle by ID=1");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles/999"), "GET vehicle by ID=999 (not found)");
|
||||||
|
|
||||||
|
Vehicle updateVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.MAINTENANCE);
|
||||||
|
var vehiclePut = createHttpRequest(updateVehicle, "/vehicle-service/vehicles/1", HttpMethod.PUT);
|
||||||
|
execute(client, vehiclePut, "PUT update vehicle ID=1");
|
||||||
|
|
||||||
|
Vehicle patchVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.IN_TRANSIT);
|
||||||
|
var vehiclePatch = createHttpRequest(patchVehicle, "/vehicle-service/vehicles/1", HttpMethod.PATCH);
|
||||||
|
execute(client, vehiclePatch, "PATCH update vehicle ID=1");
|
||||||
|
|
||||||
|
execute(client, new HttpDelete(BASE_URL + "/vehicle-service/vehicles/999"), "DELETE vehicle ID=999 (not found)");
|
||||||
|
|
||||||
|
IO.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testFreights(CloseableHttpClient client) {
|
||||||
|
IO.println("--- FREIGHTS ---");
|
||||||
|
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/freight-service" + "/freights"), "GET all freights");
|
||||||
|
|
||||||
|
Freight.Dimensions dims = new Freight.Dimensions(120, 100, 200);
|
||||||
|
Freight newFreight = new Freight(1, "Electronics", "Laptops and monitors", 500, dims, Freight.Status.PENDING);
|
||||||
|
var freightPost = createHttpRequest(newFreight, "/freight-service/freights", HttpMethod.POST);
|
||||||
|
execute(client, freightPost, "POST new freight");
|
||||||
|
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/freight-service/freights/1"), "GET freight by ID=1");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/freight-service/freights/999"), "GET freight by ID=999 (not found)");
|
||||||
|
|
||||||
|
Freight updateFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.IN_TRANSIT);
|
||||||
|
var freightPut = createHttpRequest(updateFreight, "/freight-service/freights/1", HttpMethod.PUT);
|
||||||
|
execute(client, freightPut, "PUT update freight ID=1");
|
||||||
|
|
||||||
|
Freight patchFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.DELIVERED);
|
||||||
|
var freightPatch = createHttpRequest(patchFreight, "/freight-service/freights/1", HttpMethod.PATCH);
|
||||||
|
execute(client, freightPatch, "PATCH update freight ID=1");
|
||||||
|
|
||||||
|
execute(client, new HttpDelete(BASE_URL + "/freight-service/freights/999"), "DELETE freight ID=999 (not found)");
|
||||||
|
|
||||||
|
IO.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testRoutes(CloseableHttpClient client) {
|
||||||
|
IO.println("--- ROUTES ---");
|
||||||
|
|
||||||
|
Route newRoute = new Route(1, 1, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
var routePost = createHttpRequest(newRoute, "/route-service/routes", HttpMethod.POST);
|
||||||
|
execute(client, routePost, "POST new route");
|
||||||
|
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/route-service" + "/routes"), "GET all routes");
|
||||||
|
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/route-service" + "/routes/1"), "GET route by ID=1");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/route-service" + "/routes/999"), "GET route by ID=999 (not found)");
|
||||||
|
|
||||||
|
Route updateRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.IN_PROGRESS);
|
||||||
|
var routePut = createHttpRequest(updateRoute, "/route-service/routes/1", HttpMethod.PUT);
|
||||||
|
execute(client, routePut, "PUT update route ID=1");
|
||||||
|
|
||||||
|
Route patchRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.COMPLETED);
|
||||||
|
var routePatch = createHttpRequest(patchRoute, "/route-service/routes/1", HttpMethod.PATCH);
|
||||||
|
execute(client, routePatch, "PATCH update route ID=1");
|
||||||
|
|
||||||
|
execute(client, new HttpDelete(BASE_URL + "/route-service" + "/routes/999"), "DELETE route ID=999 (not found)");
|
||||||
|
|
||||||
|
IO.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testSubResources(CloseableHttpClient client) {
|
||||||
|
IO.println("--- SUB-RESOURCES ---");
|
||||||
|
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/route-service/routes/1/freights"), "GET freights for route ID=1 (sub-resource)");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/route-service/routes/1/vehicle"), "GET vehicle for route ID=1 (sub-resource)");
|
||||||
|
|
||||||
|
IO.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testStatusFiltering(CloseableHttpClient client) {
|
||||||
|
IO.println("--- STATUS FILTERING ---");
|
||||||
|
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles?status=AVAILABLE"), "GET vehicles with status=AVAILABLE");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles?status=IN_TRANSIT"), "GET vehicles with status=IN_TRANSIT");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/freight-service/freights?status=PENDING"), "GET freights with status=PENDING");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/freight-service/freights?status=DELIVERED"), "GET freights with status=DELIVERED");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/route-service/routes?status=PLANNED"), "GET routes with status=PLANNED");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/route-service/routes?status=COMPLETED"), "GET routes with status=COMPLETED");
|
||||||
|
|
||||||
|
IO.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testErrorCases(CloseableHttpClient client) {
|
||||||
|
IO.println("--- ERROR CASES (TESTING EXCEPTION HANDLING) ---");
|
||||||
|
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/vehicle-service/vehicles/999"), "GET non-existent vehicle (404 Not Found)");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/freight-service/freights/-1"), "GET freight with invalid ID (404 Not Found)");
|
||||||
|
execute(client, new HttpGet(BASE_URL + "/route-service/routes/-1"), "GET route with invalid ID (404 Not Found)");
|
||||||
|
|
||||||
|
IO.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testInterServiceValidation(CloseableHttpClient client) {
|
||||||
|
IO.println("--- INTER-SERVICE COMMUNICATION & VALIDATION ---");
|
||||||
|
|
||||||
|
// Test 1: Create route with valid references (should succeed)
|
||||||
|
IO.println("[INTER-SERVICE TEST 1] Creating route with valid vehicle and freight references:");
|
||||||
|
Route validRoute = new Route(1, 1, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
var validRoutePost = createHttpRequest(validRoute, "/route-service/routes", HttpMethod.POST);
|
||||||
|
execute(client, validRoutePost, "POST route with valid vehicle and freights (should succeed)");
|
||||||
|
|
||||||
|
// Test 2: Create route with invalid vehicle (should fail with 404)
|
||||||
|
IO.println("[INTER-SERVICE TEST 2] Creating route with INVALID vehicle ID:");
|
||||||
|
Route invalidVehicleRoute = new Route(1, -999, List.of(1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
var invalidRoutePost = createHttpRequest(invalidVehicleRoute, "/route-service/routes", HttpMethod.POST);
|
||||||
|
execute(client, invalidRoutePost, "POST route with invalid vehicle ID, should result in 404");
|
||||||
|
|
||||||
|
// Test 3: Create route with invalid freight (should fail with 404)
|
||||||
|
IO.println("[INTER-SERVICE TEST 3] Creating route with INVALID freight ID:");
|
||||||
|
Route invalidFreightRoute = new Route(1, 1, List.of(-999L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
var invalidFreightPost = createHttpRequest(invalidFreightRoute, "/route-service/routes", HttpMethod.POST);
|
||||||
|
execute(client, invalidFreightPost, "POST route with invalid freight ID, should result in 404");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> HttpUriRequest createHttpRequest(T entity, String path, HttpMethod method) {
|
||||||
|
String entityJson = jsonMapper.writeValueAsString(entity);
|
||||||
|
HttpEntityEnclosingRequestBase req;
|
||||||
|
|
||||||
|
if (method == HttpMethod.GET) {
|
||||||
|
return new HttpGet(BASE_URL + path);
|
||||||
|
} else if (method == HttpMethod.POST) {
|
||||||
|
req = new HttpPost(BASE_URL + path);
|
||||||
|
req.setEntity(new StringEntity(entityJson, "UTF-8"));
|
||||||
|
req.setHeader("Content-Type", "application/json");
|
||||||
|
return req;
|
||||||
|
} else if (method == HttpMethod.PUT) {
|
||||||
|
req = new HttpPut(BASE_URL + path);
|
||||||
|
req.setEntity(new StringEntity(entityJson, "UTF-8"));
|
||||||
|
req.setHeader("Content-Type", "application/json");
|
||||||
|
return req;
|
||||||
|
} else if (method == HttpMethod.PATCH) {
|
||||||
|
req = new HttpPatch(BASE_URL + path);
|
||||||
|
req.setEntity(new StringEntity(entityJson, "UTF-8"));
|
||||||
|
req.setHeader("Content-Type", "application/json");
|
||||||
|
return req;
|
||||||
|
} else if (method == HttpMethod.DELETE) {
|
||||||
|
return new HttpDelete(BASE_URL + path);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("method is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String execute(CloseableHttpClient client, HttpUriRequest request, String description) {
|
||||||
|
try {
|
||||||
|
IO.println("[" + request.getMethod() + "] " + description);
|
||||||
|
CloseableHttpResponse response = client.execute(request);
|
||||||
|
int statusCode = response.getStatusLine().getStatusCode();
|
||||||
|
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : "";
|
||||||
|
|
||||||
|
IO.println("Status: " + statusCode);
|
||||||
|
if (!body.isEmpty()) {
|
||||||
|
IO.println("Response: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HttpStatus.valueOf(statusCode).isError()) {
|
||||||
|
IO.println("ERROR: Request failed with status " + statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
IO.println();
|
||||||
|
return body;
|
||||||
|
} catch (Exception e) {
|
||||||
|
IO.println("EXCEPTION: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
IO.println();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package ua.com.dxrkness.config;
|
||||||
|
|
||||||
|
import com.networknt.schema.*;
|
||||||
|
import com.networknt.schema.regex.JoniRegularExpressionFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JsonSchemaConfiguration {
|
||||||
|
@Bean
|
||||||
|
SchemaRegistry schemaRegistry() {
|
||||||
|
var schemaRegistryConfig = SchemaRegistryConfig.builder()
|
||||||
|
.regularExpressionFactory(JoniRegularExpressionFactory.getInstance())
|
||||||
|
.failFast(true).build();
|
||||||
|
|
||||||
|
return SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
|
||||||
|
builder -> builder.schemaRegistryConfig(schemaRegistryConfig)
|
||||||
|
/*
|
||||||
|
* This creates a mapping from $id which starts with
|
||||||
|
* https://www.example.org/schema to the retrieval IRI classpath:schema.
|
||||||
|
*/
|
||||||
|
.schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers
|
||||||
|
.mapPrefix("https://nure.ua/itroi", "classpath:json-schema")));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Schema vehicleSchema(SchemaRegistry schemaRegistry) {
|
||||||
|
return schemaRegistry.getSchema(SchemaLocation.of("https://nure.ua/itroi/vehicle-schema.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Schema routeSchema(SchemaRegistry schemaRegistry) {
|
||||||
|
return schemaRegistry.getSchema(SchemaLocation.of("https://nure.ua/itroi/route-schema.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Schema freightSchema(SchemaRegistry schemaRegistry) {
|
||||||
|
return schemaRegistry.getSchema(SchemaLocation.of("https://nure.ua/itroi/freight-schema.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package ua.com.dxrkness.controller;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.ErrorResponse;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import ua.com.dxrkness.exception.FreightNotFoundException;
|
||||||
|
import ua.com.dxrkness.exception.RouteNotFoundException;
|
||||||
|
import ua.com.dxrkness.exception.VehicleNotFoundException;
|
||||||
|
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
@NullMarked
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(exception = {
|
||||||
|
VehicleNotFoundException.class,
|
||||||
|
FreightNotFoundException.class,
|
||||||
|
RouteNotFoundException.class
|
||||||
|
})
|
||||||
|
public ErrorResponse handleVehicleNotFoundException(Exception ex) {
|
||||||
|
return constructExceptionBody(HttpStatus.NOT_FOUND, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException ex) {
|
||||||
|
return constructExceptionBody(HttpStatus.BAD_REQUEST, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(ResponseStatusException.class)
|
||||||
|
public ErrorResponse handleResponseStatusException(ResponseStatusException ex) {
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ErrorResponse handleGeneralException(Exception ex) {
|
||||||
|
return constructExceptionBody(HttpStatus.INTERNAL_SERVER_ERROR, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ErrorResponse constructExceptionBody(HttpStatus errorStatus, Exception ex) {
|
||||||
|
return ErrorResponse.builder(ex, errorStatus, ex.getMessage()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ua.com.dxrkness.exception;
|
||||||
|
|
||||||
|
public class FreightNotFoundException extends RuntimeException {
|
||||||
|
public FreightNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ua.com.dxrkness.exception;
|
||||||
|
|
||||||
|
public class RouteNotFoundException extends RuntimeException {
|
||||||
|
public RouteNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ua.com.dxrkness.exception;
|
||||||
|
|
||||||
|
public class VehicleNotFoundException extends RuntimeException {
|
||||||
|
public VehicleNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -5,9 +5,11 @@ import ua.com.dxrkness.model.Identifiable;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
abstract class CrudRepository<T extends Identifiable> {
|
abstract class CrudRepository<T extends Identifiable> {
|
||||||
protected final List<T> STORAGE = new ArrayList<>();
|
protected final List<T> STORAGE = new ArrayList<>();
|
||||||
|
private final AtomicLong id = new AtomicLong(0);
|
||||||
|
|
||||||
public List<T> getAll() {
|
public List<T> getAll() {
|
||||||
return new ArrayList<>(STORAGE); // defensive copy
|
return new ArrayList<>(STORAGE); // defensive copy
|
||||||
@@ -55,4 +57,8 @@ abstract class CrudRepository<T extends Identifiable> {
|
|||||||
public void deleteAll() {
|
public void deleteAll() {
|
||||||
STORAGE.clear();
|
STORAGE.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long lastId() {
|
||||||
|
return id.incrementAndGet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package ua.com.dxrkness.service;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import ua.com.dxrkness.model.Freight;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
|
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@NullMarked
|
||||||
|
public class PopulateService {
|
||||||
|
|
||||||
|
public PopulateService() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populate(int vals){
|
||||||
|
for (int i = 0; i < vals; i++) {
|
||||||
|
var f = new Freight(i, "", "", 0, null, null);
|
||||||
|
// freightRepository.add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < vals; i++) {
|
||||||
|
var v = new Vehicle(i, "", "", "", 0, 0, null);
|
||||||
|
// vehicleRepository.add(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < vals; i++) {}
|
||||||
|
// routeRepository.add(new Route(i, i, List.of((long)i), "", "", 0., 0., null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
// routeRepository.deleteAll();
|
||||||
|
// freightRepository.deleteAll();
|
||||||
|
// vehicleRepository.deleteAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nure.ua/itroi/freight-schema.json",
|
||||||
|
"title": "Freight management schema",
|
||||||
|
"description": "Describes domain objects of freights",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 500
|
||||||
|
},
|
||||||
|
"weight_kg": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"exclusiveMaximum": 4294967296
|
||||||
|
},
|
||||||
|
"dimensions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"width_cm": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 150000
|
||||||
|
},
|
||||||
|
"height_cm": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 150000
|
||||||
|
},
|
||||||
|
"length_cm": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 150000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"width_cm",
|
||||||
|
"height_cm",
|
||||||
|
"length_cm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"PENDING",
|
||||||
|
"IN_TRANSIT",
|
||||||
|
"DELIVERED"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"weight_kg",
|
||||||
|
"dimensions",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nure.ua/itroi/route-schema.json",
|
||||||
|
"title": "Routes management schema",
|
||||||
|
"description": "Describes domain objects of routes",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"vehicle_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"freight_id": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"start_location": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"end_location": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"distance_km": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"estimated_duration_hours": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"PLANNED",
|
||||||
|
"IN_PROGRESS",
|
||||||
|
"COMPLETED",
|
||||||
|
"CANCELLED"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"vehicle_id",
|
||||||
|
"freight_id",
|
||||||
|
"start_location",
|
||||||
|
"end_location",
|
||||||
|
"distance_km",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://nure.ua/itroi/vehicle-schema.json",
|
||||||
|
"title": "Vehicles management schema",
|
||||||
|
"description": "Describes domain objects of vehicles",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 4294967295
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 2,
|
||||||
|
"maxLength": 64
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"license_plate": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[A-Z]{2}[0-9]{4}[A-Z]{2}$",
|
||||||
|
"minLength": 8,
|
||||||
|
"maxLength": 8
|
||||||
|
},
|
||||||
|
"year": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1980
|
||||||
|
},
|
||||||
|
"capacity_kg": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 100,
|
||||||
|
"exclusiveMaximum": 4294967296
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"AVAILABLE",
|
||||||
|
"IN_TRANSIT",
|
||||||
|
"MAINTENANCE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"brand",
|
||||||
|
"model",
|
||||||
|
"license_plate",
|
||||||
|
"year",
|
||||||
|
"capacity_kg",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package ua.com.dxrkness.integration;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
import ua.com.dxrkness.model.Route;
|
||||||
|
import ua.com.dxrkness.service.PopulateService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.BDDAssertions.then;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@NullMarked
|
||||||
|
class InterServiceCommunicationTest {
|
||||||
|
private static final int AMOUNT = 100;
|
||||||
|
@Autowired
|
||||||
|
private PopulateService populateService;
|
||||||
|
|
||||||
|
private RestTestClient routesClient;
|
||||||
|
private RestTestClient vehiclesClient;
|
||||||
|
private RestTestClient freightsClient;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(WebApplicationContext context) {
|
||||||
|
populateService.populate(AMOUNT);
|
||||||
|
routesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/routes").build();
|
||||||
|
vehiclesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/vehicles").build();
|
||||||
|
freightsClient = RestTestClient.bindToApplicationContext(context).baseUrl("/freights").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void teardown() {
|
||||||
|
populateService.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<MediaType> mediaTypes() {
|
||||||
|
return Stream.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== INTER-SERVICE VALIDATION TESTS =====
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void createRoute_WithValidVehicleAndFreights_Succeeds(MediaType mediaType) {
|
||||||
|
// given - a route with valid vehicle and freight references
|
||||||
|
final var validVehicleId = 1;
|
||||||
|
final var validFreightIds = List.of(1L, 2L);
|
||||||
|
final var newRoute = new Route(0, validVehicleId, validFreightIds,
|
||||||
|
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
|
||||||
|
// when - creating the route
|
||||||
|
// then - should succeed because vehicle and freights exist (validated through inter-service communication)
|
||||||
|
routesClient.post()
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(newRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void createRoute_WithInvalidVehicleId_Returns404(MediaType mediaType) {
|
||||||
|
// given - a route with an invalid (non-existent) vehicle ID
|
||||||
|
final var invalidVehicleId = -1;
|
||||||
|
final var validFreightIds = List.of(1L);
|
||||||
|
final var newRoute = new Route(0, invalidVehicleId, validFreightIds,
|
||||||
|
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
|
||||||
|
// when - creating the route
|
||||||
|
// then - should fail with 404 because vehicle doesn't exist (inter-service validation)
|
||||||
|
routesClient.post()
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(newRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found").contains("vehicle");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void createRoute_WithInvalidFreightId_Returns404(MediaType mediaType) {
|
||||||
|
// given - a route with a valid vehicle but invalid freight ID
|
||||||
|
final var validVehicleId = 1;
|
||||||
|
final var invalidFreightIds = List.of(-1L); // Invalid freight ID
|
||||||
|
final var newRoute = new Route(0, validVehicleId, invalidFreightIds,
|
||||||
|
"Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
||||||
|
|
||||||
|
// when - creating the route
|
||||||
|
// then - should fail with 404 because freight doesn't exist (inter-service validation)
|
||||||
|
routesClient.post()
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(newRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found").contains("freight");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void updateRoute_ValidatesReferences_OnInterServiceCall(MediaType mediaType) {
|
||||||
|
// given - an existing route and new valid references
|
||||||
|
final var routeId = 1;
|
||||||
|
final var validVehicleId = 2;
|
||||||
|
final var validFreightIds = List.of(3L, 4L);
|
||||||
|
final var updatedRoute = new Route(routeId, validVehicleId, validFreightIds,
|
||||||
|
"Odesa", "Kharkiv", 700.0, 10.0, Route.Status.IN_PROGRESS);
|
||||||
|
|
||||||
|
// when - updating the route
|
||||||
|
// then - should succeed because vehicle and freights exist (inter-service validation)
|
||||||
|
routesClient.put().uri("/{id}", routeId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(updatedRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void updateRoute_WithInvalidVehicleOnUpdate_Returns404(MediaType mediaType) {
|
||||||
|
// given - an existing route and new references with invalid vehicle
|
||||||
|
final var routeId = 1;
|
||||||
|
final var invalidVehicleId = -999;
|
||||||
|
final var validFreightIds = List.of(3L);
|
||||||
|
final var updatedRoute = new Route(routeId, invalidVehicleId, validFreightIds,
|
||||||
|
"Odesa", "Kharkiv", 700.0, 10.0, Route.Status.IN_PROGRESS);
|
||||||
|
|
||||||
|
// when - updating the route
|
||||||
|
// then - should fail with 404 (inter-service validation on update)
|
||||||
|
routesClient.put().uri("/{id}", routeId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.contentType(mediaType)
|
||||||
|
.body(updatedRoute)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== VEHICLE INTER-SERVICE TESTS =====
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getVehicle_UsedInRoute_ReturnsVehicleData(MediaType mediaType) {
|
||||||
|
// given - a vehicle that is used in routes
|
||||||
|
final var vehicleId = 1;
|
||||||
|
|
||||||
|
// when - retrieving the vehicle
|
||||||
|
// then - should return vehicle data (VehicleService provides data to other services)
|
||||||
|
vehiclesClient.get().uri("/{id}", vehicleId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== FREIGHT INTER-SERVICE TESTS =====
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getFreight_UsedInRoute_ReturnsFreightData(MediaType mediaType) {
|
||||||
|
// given - a freight that is used in routes
|
||||||
|
final var freightId = 1;
|
||||||
|
|
||||||
|
// when - retrieving the freight
|
||||||
|
// then - should return freight data (FreightService provides data to other services)
|
||||||
|
freightsClient.get().uri("/{id}", freightId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package ua.com.dxrkness.integration;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
import ua.com.dxrkness.service.PopulateService;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.BDDAssertions.then;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@NullMarked
|
||||||
|
class SubResourcesAndFilteringTest {
|
||||||
|
private static final int AMOUNT = 100;
|
||||||
|
@Autowired
|
||||||
|
private PopulateService populateService;
|
||||||
|
|
||||||
|
private RestTestClient vehiclesClient;
|
||||||
|
private RestTestClient routesClient;
|
||||||
|
private RestTestClient freightsClient;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(WebApplicationContext context) {
|
||||||
|
populateService.populate(AMOUNT);
|
||||||
|
vehiclesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/vehicles").build();
|
||||||
|
routesClient = RestTestClient.bindToApplicationContext(context).baseUrl("/routes").build();
|
||||||
|
freightsClient = RestTestClient.bindToApplicationContext(context).baseUrl("/freights").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void teardown() {
|
||||||
|
populateService.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<MediaType> mediaTypes() {
|
||||||
|
return Stream.of(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getById_VehicleNotFound_Returns404WithErrorResponse(MediaType mediaType) {
|
||||||
|
// given
|
||||||
|
final var vehicleId = -1;
|
||||||
|
|
||||||
|
// when
|
||||||
|
// then
|
||||||
|
vehiclesClient.get().uri("/{id}", vehicleId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getById_RouteNotFound_Returns404WithErrorResponse(MediaType mediaType) {
|
||||||
|
// given
|
||||||
|
final var routeId = -1;
|
||||||
|
|
||||||
|
// when
|
||||||
|
// then
|
||||||
|
routesClient.get().uri("/{id}", routeId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mediaTypes")
|
||||||
|
void getById_FreightNotFound_Returns404WithErrorResponse(MediaType mediaType) {
|
||||||
|
// given
|
||||||
|
final var freightId = -1;
|
||||||
|
|
||||||
|
// when
|
||||||
|
// then
|
||||||
|
freightsClient.get().uri("/{id}", freightId)
|
||||||
|
.accept(mediaType)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.consumeWith(res -> {
|
||||||
|
String body = res.getResponseBody();
|
||||||
|
BDDAssertions.then(body).isNotNull();
|
||||||
|
BDDAssertions.then(body).contains("Not Found");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
package ua.com.dxrkness.client;
|
|
||||||
|
|
||||||
import org.apache.http.client.methods.*;
|
|
||||||
import org.apache.http.entity.StringEntity;
|
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
|
||||||
import org.apache.http.impl.client.HttpClients;
|
|
||||||
import org.apache.http.util.EntityUtils;
|
|
||||||
import tools.jackson.databind.ObjectMapper;
|
|
||||||
import tools.jackson.dataformat.xml.XmlMapper;
|
|
||||||
import ua.com.dxrkness.model.Freight;
|
|
||||||
import ua.com.dxrkness.model.Route;
|
|
||||||
import ua.com.dxrkness.model.Vehicle;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ApplicationClient {
|
|
||||||
private static final String BASE_URL = "http://localhost:8080";
|
|
||||||
private static final ObjectMapper jsonMapper = new ObjectMapper();
|
|
||||||
private static final XmlMapper xmlMapper = new XmlMapper();
|
|
||||||
|
|
||||||
static void main() {
|
|
||||||
try (CloseableHttpClient client = HttpClients.createDefault()) {
|
|
||||||
testVehicles(client);
|
|
||||||
testFreights(client);
|
|
||||||
testRoutes(client);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("error: " + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testVehicles(CloseableHttpClient client) throws Exception {
|
|
||||||
System.out.println("--- VEHICLES ---");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/vehicles"), "GET all vehicles");
|
|
||||||
|
|
||||||
Vehicle newVehicle = new Vehicle(0, "Mercedes", "Actros", "AA1234BB", 2023, 18000, Vehicle.Status.AVAILABLE);
|
|
||||||
String vehicleXml = xmlMapper.writeValueAsString(newVehicle);
|
|
||||||
HttpPost post = new HttpPost(BASE_URL + "/vehicles");
|
|
||||||
post.setEntity(new StringEntity(vehicleXml, "UTF-8"));
|
|
||||||
post.setHeader("Content-Type", "application/xml");
|
|
||||||
post.setHeader("Accept", "application/xml");
|
|
||||||
String createdVehicle = execute(client, post, "POST new vehicle");
|
|
||||||
|
|
||||||
var get = new HttpGet(BASE_URL + "/vehicles/0");
|
|
||||||
get.setHeader("Content-Type", "application/xml");
|
|
||||||
execute(client, get, "GET vehicle by ID=0");
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/vehicles/999"), "GET vehicle by ID=999 (not found)");
|
|
||||||
|
|
||||||
Vehicle updateVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.MAINTENANCE);
|
|
||||||
String updateJson = jsonMapper.writeValueAsString(updateVehicle);
|
|
||||||
HttpPut put = new HttpPut(BASE_URL + "/vehicles/0");
|
|
||||||
put.setEntity(new StringEntity(updateJson, "UTF-8"));
|
|
||||||
put.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, put, "PUT update vehicle ID=0");
|
|
||||||
|
|
||||||
Vehicle patchVehicle = new Vehicle(1, "Volvo", "FH16", "BB5678CC", 2024, 20000, Vehicle.Status.IN_TRANSIT);
|
|
||||||
String patchJson = jsonMapper.writeValueAsString(patchVehicle);
|
|
||||||
HttpPatch patch = new HttpPatch(BASE_URL + "/vehicles/0");
|
|
||||||
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
|
|
||||||
patch.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, patch, "PATCH update vehicle ID=0");
|
|
||||||
|
|
||||||
execute(client, new HttpDelete(BASE_URL + "/vehicles/999"), "DELETE vehicle ID=999 (not found)");
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testFreights(CloseableHttpClient client) throws Exception {
|
|
||||||
System.out.println("--- FREIGHTS ---");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/freights"), "GET all freights");
|
|
||||||
|
|
||||||
Freight.Dimensions dims = new Freight.Dimensions(120, 100, 200);
|
|
||||||
Freight newFreight = new Freight(0, "Electronics", "Laptops and monitors", 500, dims, Freight.Status.PENDING);
|
|
||||||
String freightJson = jsonMapper.writeValueAsString(newFreight);
|
|
||||||
HttpPost post = new HttpPost(BASE_URL + "/freights");
|
|
||||||
post.setEntity(new StringEntity(freightJson, "UTF-8"));
|
|
||||||
post.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, post, "POST new freight");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/freights/0"), "GET freight by ID=1");
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/freights/999"), "GET freight by ID=999 (not found)");
|
|
||||||
|
|
||||||
Freight updateFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.IN_TRANSIT);
|
|
||||||
String updateJson = jsonMapper.writeValueAsString(updateFreight);
|
|
||||||
HttpPut put = new HttpPut(BASE_URL + "/freights/0");
|
|
||||||
put.setEntity(new StringEntity(updateJson, "UTF-8"));
|
|
||||||
put.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, put, "PUT update freight ID=1");
|
|
||||||
|
|
||||||
Freight patchFreight = new Freight(1, "Furniture", "Office desks", 800, dims, Freight.Status.DELIVERED);
|
|
||||||
String patchJson = jsonMapper.writeValueAsString(patchFreight);
|
|
||||||
HttpPatch patch = new HttpPatch(BASE_URL + "/freights/0");
|
|
||||||
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
|
|
||||||
patch.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, patch, "PATCH update freight ID=1");
|
|
||||||
|
|
||||||
execute(client, new HttpDelete(BASE_URL + "/freights/999"), "DELETE freight ID=999 (not found)");
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testRoutes(CloseableHttpClient client) throws Exception {
|
|
||||||
System.out.println("--- ROUTES ---");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/routes"), "GET all routes");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/routes?vehicleId=0"), "GET routes by vehicleId=1");
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/routes?freightId=0"), "GET routes by freightId=1");
|
|
||||||
|
|
||||||
Route newRoute = new Route(0, 0, List.of(0L, 1L), "Kyiv", "Lviv", 540.0, 8.5, Route.Status.PLANNED);
|
|
||||||
String routeJson = jsonMapper.writeValueAsString(newRoute);
|
|
||||||
HttpPost post = new HttpPost(BASE_URL + "/routes");
|
|
||||||
post.setEntity(new StringEntity(routeJson, "UTF-8"));
|
|
||||||
post.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, post, "POST new route");
|
|
||||||
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/routes/0"), "GET route by ID=0");
|
|
||||||
execute(client, new HttpGet(BASE_URL + "/routes/999"), "GET route by ID=999 (not found)");
|
|
||||||
|
|
||||||
Route updateRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.IN_PROGRESS);
|
|
||||||
String updateJson = jsonMapper.writeValueAsString(updateRoute);
|
|
||||||
HttpPut put = new HttpPut(BASE_URL + "/routes/0");
|
|
||||||
put.setEntity(new StringEntity(updateJson, "UTF-8"));
|
|
||||||
put.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, put, "PUT update route ID=0");
|
|
||||||
|
|
||||||
Route patchRoute = new Route(1, 1, List.of(1L), "Kyiv", "Odesa", 480.0, 7.0, Route.Status.COMPLETED);
|
|
||||||
String patchJson = jsonMapper.writeValueAsString(patchRoute);
|
|
||||||
HttpPatch patch = new HttpPatch(BASE_URL + "/routes/0");
|
|
||||||
patch.setEntity(new StringEntity(patchJson, "UTF-8"));
|
|
||||||
patch.setHeader("Content-Type", "application/json");
|
|
||||||
execute(client, patch, "PATCH update route ID=0");
|
|
||||||
|
|
||||||
execute(client, new HttpDelete(BASE_URL + "/routes/999"), "DELETE route ID=999 (not found)");
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String execute(CloseableHttpClient client, HttpUriRequest request, String description) {
|
|
||||||
try {
|
|
||||||
System.out.println("[" + request.getMethod() + "] " + description);
|
|
||||||
CloseableHttpResponse response = client.execute(request);
|
|
||||||
int statusCode = response.getStatusLine().getStatusCode();
|
|
||||||
String body = response.getEntity() != null ? EntityUtils.toString(response.getEntity()) : "";
|
|
||||||
|
|
||||||
System.out.println("Status: " + statusCode);
|
|
||||||
if (!body.isEmpty()) {
|
|
||||||
System.out.println("Response: " + body);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode >= 400) {
|
|
||||||
System.err.println("ERROR: Request failed with status " + statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
return body;
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("EXCEPTION: " + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
System.out.println();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
@NullMarked
|
|
||||||
package ua.com.dxrkness;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package ua.com.dxrkness.service;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import ua.com.dxrkness.model.Freight;
|
|
||||||
import ua.com.dxrkness.repository.FreightRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public final class FreightService {
|
|
||||||
private final FreightRepository repo;
|
|
||||||
|
|
||||||
public FreightService(FreightRepository repo) {
|
|
||||||
this.repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Freight> getAll() {
|
|
||||||
return repo.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Freight add(Freight freight) {
|
|
||||||
int id = repo.add(freight);
|
|
||||||
return new Freight(id, freight.name(), freight.description(), freight.weightKg(), freight.dimensions(), freight.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Freight getById(long id) {
|
|
||||||
return repo.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Freight update(long id, Freight newFreight) {
|
|
||||||
newFreight = new Freight(id, newFreight.name(), newFreight.description(), newFreight.weightKg(), newFreight.dimensions(), newFreight.status());
|
|
||||||
return repo.update(id, newFreight);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Freight delete(long id) {
|
|
||||||
return repo.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package ua.com.dxrkness.service;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import ua.com.dxrkness.model.Freight;
|
|
||||||
import ua.com.dxrkness.model.Route;
|
|
||||||
import ua.com.dxrkness.model.Vehicle;
|
|
||||||
import ua.com.dxrkness.repository.FreightRepository;
|
|
||||||
import ua.com.dxrkness.repository.RouteRepository;
|
|
||||||
import ua.com.dxrkness.repository.VehicleRepository;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@NullMarked
|
|
||||||
public class PopulateService {
|
|
||||||
private final FreightRepository freightRepository;
|
|
||||||
private final RouteRepository routeRepository;
|
|
||||||
private final VehicleRepository vehicleRepository;
|
|
||||||
|
|
||||||
public PopulateService(FreightRepository freightRepository, RouteRepository routeRepository, VehicleRepository vehicleRepository) {
|
|
||||||
this.freightRepository = freightRepository;
|
|
||||||
this.routeRepository = routeRepository;
|
|
||||||
this.vehicleRepository = vehicleRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populate(int vals){
|
|
||||||
for (int i = 0; i < vals; i++) {
|
|
||||||
var f = new Freight(i, "", "", 0, null, null);
|
|
||||||
freightRepository.add(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < vals; i++) {
|
|
||||||
var v = new Vehicle(i, "", "", "", 0, 0, null);
|
|
||||||
vehicleRepository.add(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < vals; i++)
|
|
||||||
routeRepository.add(new Route(i, i, List.of((long)i), "", "", 0., 0., null));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
routeRepository.deleteAll();
|
|
||||||
freightRepository.deleteAll();
|
|
||||||
vehicleRepository.deleteAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package ua.com.dxrkness.service;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import ua.com.dxrkness.model.Route;
|
|
||||||
import ua.com.dxrkness.repository.RouteRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public final class RouteService {
|
|
||||||
private final RouteRepository repo;
|
|
||||||
|
|
||||||
public RouteService(RouteRepository repo) {
|
|
||||||
this.repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Route> getAll() {
|
|
||||||
return repo.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Route getById(long id) {
|
|
||||||
return repo.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Route add(Route route) {
|
|
||||||
int id = repo.add(route);
|
|
||||||
return new Route(id,
|
|
||||||
route.vehicleId(),
|
|
||||||
route.freightId(),
|
|
||||||
route.startLocation(),
|
|
||||||
route.endLocation(),
|
|
||||||
route.distanceKm(),
|
|
||||||
route.estimatedDurationHours(),
|
|
||||||
route.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Route getByFreightId(long freightId) {
|
|
||||||
return repo.getByFreightId(freightId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Route> getByVehicleId(long vehicleId) {
|
|
||||||
return repo.getByVehicleId(vehicleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Route update(long id, Route newRoute) {
|
|
||||||
newRoute = new Route(id,
|
|
||||||
newRoute.vehicleId(),
|
|
||||||
newRoute.freightId(),
|
|
||||||
newRoute.startLocation(),
|
|
||||||
newRoute.endLocation(),
|
|
||||||
newRoute.distanceKm(),
|
|
||||||
newRoute.estimatedDurationHours(),
|
|
||||||
newRoute.status());
|
|
||||||
return repo.update(id, newRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Route delete(long id) {
|
|
||||||
return repo.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package ua.com.dxrkness.service;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import ua.com.dxrkness.model.Vehicle;
|
|
||||||
import ua.com.dxrkness.repository.VehicleRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public final class VehicleService {
|
|
||||||
private final VehicleRepository repo;
|
|
||||||
|
|
||||||
public VehicleService(VehicleRepository repo) {
|
|
||||||
this.repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Vehicle> getAll() {
|
|
||||||
return repo.getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vehicle add(Vehicle veh) {
|
|
||||||
int id = repo.add(veh);
|
|
||||||
return new Vehicle(id, veh.brand(), veh.model(), veh.licensePlate(), veh.year(), veh.capacityKg(), veh.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Vehicle getById(long id) {
|
|
||||||
return repo.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vehicle update(long id, Vehicle newVehicle) {
|
|
||||||
newVehicle = new Vehicle(id,
|
|
||||||
newVehicle.brand(),
|
|
||||||
newVehicle.model(),
|
|
||||||
newVehicle.licensePlate(),
|
|
||||||
newVehicle.year(),
|
|
||||||
newVehicle.capacityKg(),
|
|
||||||
newVehicle.status());
|
|
||||||
return repo.update(id, newVehicle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Vehicle delete(long id) {
|
|
||||||
return repo.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
spring:
|
|
||||||
application:
|
|
||||||
name: itroi
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
plugins {
|
||||||
|
id("java")
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "ua.com.dxrkness"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// spring
|
||||||
|
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||||
|
implementation(platform(libs.spring.cloud.dependencies))
|
||||||
|
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-aop:4.0.0-M2")
|
||||||
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
implementation(libs.spring.boot.starter.web.test)
|
||||||
|
|
||||||
|
// http client
|
||||||
|
implementation(libs.apache.http.client)
|
||||||
|
|
||||||
|
// openapi docs
|
||||||
|
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||||
|
|
||||||
|
// testing
|
||||||
|
testImplementation(platform(libs.junit.bom))
|
||||||
|
testImplementation(libs.junit.jupiter)
|
||||||
|
testRuntimeOnly(libs.junit.platform.launcher)
|
||||||
|
|
||||||
|
implementation(libs.jspecify)
|
||||||
|
|
||||||
|
implementation(project(":models"))
|
||||||
|
implementation(project(":shared"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ua.com.dxrkness;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class VehicleServiceApp {
|
||||||
|
static void main(String[] args) {
|
||||||
|
SpringApplication.run(VehicleServiceApp.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ua.com.dxrkness.aspect;
|
||||||
|
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class LoggingAspect {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
|
||||||
|
|
||||||
|
@Value("${server.port}")
|
||||||
|
private String port;
|
||||||
|
|
||||||
|
@Before("execution(* ua.com.dxrkness.controller.VehicleController.*(..))")
|
||||||
|
public void logBeforeMethod(JoinPoint joinPoint) {
|
||||||
|
String methodName = joinPoint.getSignature().getName();
|
||||||
|
logger.info(">>> [Instance Port: {}] - Calling method: {}", port, methodName);
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-36
@@ -2,14 +2,13 @@ package ua.com.dxrkness.controller;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.jspecify.annotations.NullMarked;
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import ua.com.dxrkness.dto.VehicleRequest;
|
||||||
import ua.com.dxrkness.model.Vehicle;
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
import ua.com.dxrkness.service.VehicleService;
|
import ua.com.dxrkness.service.VehicleService;
|
||||||
|
|
||||||
@@ -32,12 +31,16 @@ public class VehicleController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Retrieve all vehicles",
|
summary = "Retrieve all vehicles",
|
||||||
description = "Get a list of all vehicles"
|
description = "Get a list of all vehicles or filter by status"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles",
|
@ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@GetMapping(consumes = MediaType.ALL_VALUE)
|
@GetMapping(consumes = MediaType.ALL_VALUE)
|
||||||
public List<Vehicle> getAll() {
|
public List<Vehicle> getAll(
|
||||||
|
@Parameter(description = "Filter vehicles by status (AVAILABLE, IN_TRANSIT, MAINTENANCE)")
|
||||||
|
@RequestParam(name = "status", required = false) Vehicle.@Nullable Status status) {
|
||||||
|
if (status != null) {
|
||||||
|
return service.getByStatus(status);
|
||||||
|
}
|
||||||
return service.getAll();
|
return service.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,69 +48,65 @@ public class VehicleController {
|
|||||||
summary = "Retrieve a vehicle by ID",
|
summary = "Retrieve a vehicle by ID",
|
||||||
description = "Get a single vehicle by its ID"
|
description = "Get a single vehicle by its ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle found",
|
@ApiResponse(responseCode = "200", description = "Vehicle found")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
||||||
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Vehicle> getById(
|
public List<Vehicle> getById(
|
||||||
@Parameter(description = "ID of the vehicle to retrieve", required = true)
|
@Parameter(description = "ID of the vehicle to retrieve", required = true)
|
||||||
@PathVariable("id") long id) {
|
@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.getById(id));
|
return List.of(service.getById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Create a new vehicle",
|
summary = "Create a new vehicle",
|
||||||
description = "Add a new vehicle to the system"
|
description = "Add a new vehicle to the system"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully created",
|
@ApiResponse(responseCode = "200", description = "Vehicle successfully created")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<Vehicle> add(
|
public Vehicle add(
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
description = "Vehicle object to be created",
|
description = "Vehicle object to be created",
|
||||||
required = true,
|
required = true
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
)
|
||||||
@RequestBody Vehicle newVehicle) {
|
@RequestBody VehicleRequest newVehicle) {
|
||||||
return ResponseEntity.ok(service.add(newVehicle));
|
return service.add(newVehicle.toEntity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Update a vehicle (full)",
|
summary = "Update a vehicle (full)",
|
||||||
description = "Update all fields of an existing vehicle by ID"
|
description = "Update all fields of an existing vehicle by ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated",
|
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<Vehicle> update(
|
public Vehicle update(
|
||||||
@Parameter(description = "ID of the vehicle to update", required = true)
|
@Parameter(description = "ID of the vehicle to update", required = true)
|
||||||
@PathVariable("id") long id,
|
@PathVariable("id") long id,
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
description = "Updated vehicle object",
|
description = "Updated vehicle object",
|
||||||
required = true,
|
required = true
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
)
|
||||||
@RequestBody Vehicle newVehicle) {
|
@RequestBody VehicleRequest newVehicle) {
|
||||||
return ResponseEntity.ok(service.update(id, newVehicle));
|
return service.update(id, newVehicle.toEntity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Update a vehicle (partial)",
|
summary = "Update a vehicle (partial)",
|
||||||
description = "Update specific fields of an existing vehicle by ID"
|
description = "Update specific fields of an existing vehicle by ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated",
|
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
|
||||||
@PatchMapping("/{id}")
|
@PatchMapping("/{id}")
|
||||||
public ResponseEntity<Vehicle> updatePatch(
|
public Vehicle updatePatch(
|
||||||
@Parameter(description = "ID of the vehicle to update", required = true)
|
@Parameter(description = "ID of the vehicle to update", required = true)
|
||||||
@PathVariable("id") long id,
|
@PathVariable("id") long id,
|
||||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
description = "Vehicle object with fields to update",
|
description = "Vehicle object with fields to update",
|
||||||
required = true,
|
required = true
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
)
|
||||||
@RequestBody Vehicle newVehicle) {
|
@RequestBody VehicleRequest newVehicle) {
|
||||||
return ResponseEntity.ok(service.update(id, newVehicle));
|
return service.update(id, newVehicle.toEntity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -115,13 +114,12 @@ public class VehicleController {
|
|||||||
summary = "Delete a vehicle",
|
summary = "Delete a vehicle",
|
||||||
description = "Delete a vehicle by its ID"
|
description = "Delete a vehicle by its ID"
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Vehicle successfully deleted",
|
@ApiResponse(responseCode = "200", description = "Vehicle successfully deleted")
|
||||||
content = @Content(schema = @Schema(implementation = Vehicle.class)))
|
|
||||||
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
@ApiResponse(responseCode = "404", description = "Vehicle not found")
|
||||||
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
|
||||||
public ResponseEntity<Vehicle> delete(
|
public Vehicle delete(
|
||||||
@Parameter(description = "ID of the vehicle to delete", required = true)
|
@Parameter(description = "ID of the vehicle to delete", required = true)
|
||||||
@PathVariable("id") long id) {
|
@PathVariable("id") long id) {
|
||||||
return ResponseEntity.ofNullable(service.delete(id));
|
return service.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package ua.com.dxrkness.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import ua.com.dxrkness.exception.VehicleNotFoundException;
|
||||||
|
import ua.com.dxrkness.model.Vehicle;
|
||||||
|
import ua.com.dxrkness.repository.VehicleRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public final class VehicleService {
|
||||||
|
private final VehicleRepository repo;
|
||||||
|
|
||||||
|
public VehicleService(VehicleRepository repo) {
|
||||||
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Vehicle> getAll() {
|
||||||
|
return repo.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Vehicle> getByStatus(Vehicle.Status status) {
|
||||||
|
return repo.getAll().stream()
|
||||||
|
.filter(v -> v.status() == status)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vehicle add(Vehicle veh) {
|
||||||
|
var newVehicle = new Vehicle(repo.lastId(), veh.brand(), veh.model(), veh.licensePlate(), veh.year(), veh.capacityKg(), veh.status());
|
||||||
|
repo.add(newVehicle);
|
||||||
|
return newVehicle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vehicle getById(long id) {
|
||||||
|
Vehicle vehicle = repo.getById(id);
|
||||||
|
if (vehicle == null) {
|
||||||
|
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return vehicle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vehicle update(long id, Vehicle newVehicle) {
|
||||||
|
newVehicle = new Vehicle(id,
|
||||||
|
newVehicle.brand(),
|
||||||
|
newVehicle.model(),
|
||||||
|
newVehicle.licensePlate(),
|
||||||
|
newVehicle.year(),
|
||||||
|
newVehicle.capacityKg(),
|
||||||
|
newVehicle.status());
|
||||||
|
return repo.update(id, newVehicle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vehicle delete(long id) {
|
||||||
|
var deleted = repo.delete(id);
|
||||||
|
if (deleted == null) {
|
||||||
|
throw new VehicleNotFoundException("Vehicle with ID " + id + " not found.");
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: vehicle-service
|
||||||
|
server:
|
||||||
|
port: 8081
|
||||||
|
eureka:
|
||||||
|
client:
|
||||||
|
service-url:
|
||||||
|
defaultZone: http://localhost:8070/eureka
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
["Клієнт успішно запускається та спрямовує всі запити + + + на API Gateway","Клієнт демонструє успішний обмін даними (GET/POST) + + + з мінімум двома публічними сервісами","Коректна обробка вхідних даних: Клієнт відображає дані, отримані з мікросервісів, у зрозумілому для + + + користувача вигляді.","Реалізовано функціональність \"Перегляд (Read): Відображення повного списку замовлень та списку + + + ресторанів","Реалізовано функціональність \"Створення (Create)\": Наявність форми для створення нової сутності, яка + + успішно відправляє POST-запит.","Реалізовано функціональність \"Редагування/Видалення (Update/Delete)\" для однієї + + ключової сутності.","Складна агрегація: Клієнт демонструє агрегацію + даних з кількох сервісів.","Користувацька взаємодія: Реалізовано елементи динамічної взаємодії (наприклад, перегляд деталей + + замовлення).","Обробка каскадних операцій: Клієнт демонструє виклик операції, яка запускає каскад внутрішніх + міжсервісних викликів на бекенді.","Обробка помилок (UX): Реалізовано механізм дружнього сповіщення користувача при отриманні + + + помилки від Gateway (HTTP 4xx/5xx).","Валідація вводу (Frontend): Реалізовано базову перевірку вхідних даних на стороні клієнта перед + + відправкою запиту.","Захист від дублювання (Ідемпотентність): Реалізовано механізм запобігання дублюванню для + неідемпотентних запитів.","Дизайн та Юзабіліті: Інтерфейс є інтуїтивно зрозумілим, має логічну навігацію та адекватний + візуальний дизайн."]
|
||||||
Reference in New Issue
Block a user