This commit is contained in:
2025-12-22 20:17:02 +02:00
parent 9fca6d81a2
commit 8ad2bc0d2d
19 changed files with 964 additions and 31 deletions

View File

@@ -9,6 +9,13 @@ spring:
locator:
enabled: true
lower-case-service-id: true
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
- '*'
allowedHeaders: "*"
application:
name: api-gateway
eureka:

View File

@@ -9,7 +9,11 @@
},
"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",
},
},
@@ -18,6 +22,8 @@
"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=="],
@@ -98,6 +104,38 @@
"@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=="],
@@ -126,6 +164,8 @@
"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=="],
@@ -134,9 +174,9 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"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": ["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@1.3.2", "", { "dependencies": { "@rollup/plugin-node-resolve": "^15.2.3", "flowbite": "^2.0.0" } }, "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g=="],
"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=="],
@@ -144,6 +184,8 @@
"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=="],
@@ -152,6 +194,8 @@
"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=="],
@@ -208,12 +252,16 @@
"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=="],
@@ -224,6 +272,22 @@
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"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=="],
"@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=="],
}
}

View File

@@ -1,10 +1,16 @@
<!doctype html>
<html lang="en">
<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>

View File

@@ -10,7 +10,11 @@
},
"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": {

View File

@@ -1,7 +1,29 @@
<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";
</script>
<main>
<Router>
<Header />
</main>
<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>

196
client/src/Freights.svelte Normal file
View File

@@ -0,0 +1,196 @@
<script>
import { onMount } from "svelte";
import {
Button,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell,
Modal,
Label,
Input,
Select,
Badge,
Heading,
Helper,
} from "flowbite-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: "PENDING",
});
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) {
console.error("Fetch error:", 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) {
console.error("Save error:", err);
}
}
async function deleteFreight(id) {
await fetch(`${BASE_URL}/${id}`, { method: "DELETE" });
fetchFreights();
}
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: "PENDING",
};
}
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" },
};
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">Item Name</Label>
<Input bind:value={formData.name} placeholder="e.g. Electronics" />
</div>
<div class="col-span-2">
<Label class="mb-2">Description</Label>
<Input bind:value={formData.description} placeholder="Cargo details..." />
</div>
<div>
<Label class="mb-2">Weight (kg)</Label>
<Input type="number" bind:value={formData.weight_kg} />
</div>
<div>
<Label class="mb-2">Status</Label>
<Select
items={Object.keys(statusMap).map((k) => ({
value: k,
name: statusMap[k].label,
}))}
bind:value={formData.status}
/>
</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>Length (cm.)</Helper>
<Input type="number" bind:value={formData.dimensions.length_cm} />
</div>
<div>
<Helper>Width (cm.)</Helper>
<Input type="number" bind:value={formData.dimensions.width_cm} />
</div>
<div>
<Helper>Height (cm.)</Helper>
<Input type="number" bind:value={formData.dimensions.height_cm} />
</div>
</div>
</div>
</div>
<div class="flex justify-end space-x-3 mt-6">
<Button color="alternative" onclick={closeModal}>Cancel</Button>
<Button color="blue" onclick={saveFreight}>Confirm</Button>
</div>
</Modal>

View File

@@ -1,8 +1,9 @@
<script>
import { Navbar, NavBrand, NavUl, NavLi } from "flowbite-svelte";
import { Link } from "svelte5-router";
</script>
<header>
<header class="fixed top-0 left-0 right-0 z-50">
<Navbar>
<NavBrand href="/">
<span
@@ -11,9 +12,9 @@
>
</NavBrand>
<NavUl>
<NavLi href="/freights">Freights</NavLi>
<NavLi href="/vehicles">Vehicles</NavLi>
<NavLi href="/routes">Routes</NavLi>
<NavLi><Link to="/freights">Freights</Link></NavLi>
<NavLi><Link to="/vehicles">Vehicles</Link></NavLi>
<NavLi><Link to="/routes">Routes</Link></NavLi>
</NavUl>
</Navbar>
</header>

292
client/src/Routes.svelte Normal file
View File

@@ -0,0 +1,292 @@
<script>
import { onMount } from "svelte";
import {
Button,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell,
Modal,
Label,
Input,
Select,
Badge,
Heading,
MultiSelect,
Helper,
} from "flowbite-svelte";
let routes = $state([]);
let vehicles = $state([]); // For dropdown
let freights = $state([]); // For dropdown
let isModalOpen = $state(false);
let editingId = $state(null);
let formData = $state({
vehicle_id: "",
freight_id: [],
start_location: "",
end_location: "",
distance_km: 0,
estimated_duration_hours: 0,
status: "PLANNED",
});
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) {
console.error("Load error:", err);
}
}
let errors = $derived.by(() => {
const e = {};
if (!formData.vehicle_id) e.vehicle_id = "Vehicle 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;
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
if (res.ok) {
await fetchData();
closeModal();
}
}
async function deleteRoute(id) {
await fetch(`${BASE_URL}/${id}`, { method: "DELETE" });
fetchData();
}
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: "PLANNED",
};
}
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 class="mb-2">Assign Vehicle</Label>
<Select
items={vehicles}
bind:value={formData.vehicleId}
placeholder="Select vehicle..."
/>
</div>
<div>
<Label class="mb-2">Status</Label>
<Select
items={Object.keys(statusMap).map((k) => ({
value: k,
name: statusMap[k].label,
}))}
bind:value={formData.status}
/>
</div>
<div class="col-span-2">
<Label class="mb-2">Freights (Multi-select)</Label>
<MultiSelect items={freights} bind:value={formData.freight_id} />
</div>
<div>
<Label class="mb-2">Start Location</Label>
<Input bind:value={formData.start_location} placeholder="City A" />
</div>
<div>
<Label class="mb-2">End Location</Label>
<Input bind:value={formData.end_location} placeholder="City B" />
</div>
<div>
<Label class="mb-2">Distance (km)</Label>
<Input type="number" bind:value={formData.distance_km} />
</div>
<div>
<Label class="mb-2">Est. Time (hours)</Label>
<Input
type="number"
step="0.5"
bind:value={formData.estimated_duration_hours}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<Label color={errors.vehicle_id ? "red" : "base"} class="mb-2"
>Assign Vehicle</Label
>
<Select
items={vehicles}
bind:value={formData.vehicle_id}
color={errors.vehicle_id ? "red" : "base"}
/>
{#if errors.vehicle_id}<Helper class="mt-2" color="red"
>{errors.vehicle_id}</Helper
>{/if}
</div>
<div class="col-span-2">
<Label color={errors.freight_id ? "red" : "base"} class="mb-2"
>Freights</Label
>
<MultiSelect items={freights} bind:value={formData.freight_id} />
{#if errors.freight_id}<Helper class="mt-2" color="red"
>{errors.freight_id}</Helper
>{/if}
</div>
<div>
<Label color={errors.start_location ? "red" : "base"} class="mb-2"
>Start Location</Label
>
<Input
bind:value={formData.start_location}
color={errors.start_location ? "red" : "base"}
/>
{#if errors.start_location}<Helper class="mt-2" color="red"
>{errors.start_location}</Helper
>{/if}
</div>
<div>
<Label color={errors.distance_km ? "red" : "base"} class="mb-2"
>Distance (km)</Label
>
<Input
type="number"
bind:value={formData.distance_km}
color={errors.distance_km ? "red" : "base"}
/>
{#if errors.distance_km}<Helper class="mt-2" color="red"
>{errors.distance_km}</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}>Save Route</Button
>
</div>
</Modal>

224
client/src/Vehicles.svelte Normal file
View File

@@ -0,0 +1,224 @@
<script>
import { onMount } from "svelte";
import {
Button,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
TableHead,
TableHeadCell,
Modal,
Label,
Input,
Select,
Badge,
Heading,
} from "flowbite-svelte";
let vehicles = $state([]);
let isModalOpen = $state(false);
let editingId = $state(null);
let formData = $state({
brand: "",
model: "",
license_plate: "",
year: 2024,
capacity_kg: 0,
status: "AVAILABLE",
});
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);
vehicles = await res.json();
} catch (err) {
console.error("Error fetching vehicles:", err);
}
}
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();
}
} catch (err) {
console.error("Error saving vehicle:", err);
}
}
async function deleteVehicle(id) {
try {
const res = await fetch(`${BASE_URL}/${id}`, { method: "DELETE" });
if (res.ok) fetchVehicles();
} catch (err) {
console.error("Error deleting vehicle:", err);
}
}
function openModal(vehicle = null) {
if (vehicle) {
editingId = vehicle.id;
formData = { ...vehicle };
} else {
editingId = null;
formData = {
brand: "",
model: "",
license_plate: "",
year: 2024,
capacity_kg: 0,
status: "AVAILABLE",
};
}
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";
}
}
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>
<Modal
title={editingId ? "Update Vehicle" : "Add Vehicle"}
bind:open={isModalOpen}
size="sm"
autoclose={false}
>
<div class="space-y-4">
<div>
<Label for="brand" class="mb-2">Make</Label>
<Input
type="text"
id="brand"
bind:value={formData.brand}
placeholder="e.g. Mercedes"
required
/>
</div>
<div>
<Label for="model" class="mb-2">Model</Label>
<Input
type="text"
id="model"
bind:value={formData.model}
placeholder="e.g. Actros"
required
/>
</div>
<div class="grid grid-cols-3 gap-4">
<div>
<Label for="plate" class="mb-2">License Plate</Label>
<Input
type="text"
id="plate"
bind:value={formData.license_plate}
placeholder="AA1234BB"
/>
</div>
<div>
<Label for="year" class="mb-2">Year</Label>
<Input type="number" id="year" bind:value={formData.year} />
</div>
<div>
<Label for="capacity" class="mb-2">Capacity (kg.)</Label>
<Input type="number" id="capacity" bind:value={formData.capacity_kg} />
</div>
</div>
<div>
<Label for="status" class="mb-2">Status</Label>
<Select items={statusOptions} bind:value={formData.status} />
</div>
<div class="flex justify-end space-x-3 pt-4">
<Button color="alternative" onclick={closeModal}>Cancel</Button>
<Button color="blue" onclick={saveVehicle}
>{editingId ? "Update" : "Create"}</Button
>
</div>
</div>
</Modal>

46
client/src/app.css Normal file
View File

@@ -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;
}

View File

@@ -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
};

View File

@@ -1,7 +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: [svelte()],
});
export default defineConfig({ plugins: [tailwindcss(), svelte()] });

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import ua.com.dxrkness.dto.FreightRequest;
import ua.com.dxrkness.model.Freight;
import ua.com.dxrkness.service.FreightService;
@@ -74,8 +75,8 @@ public class FreightController {
description = "Invalid freight data (e.g., weight exceeds vehicle capacity)"
)
@PostMapping
public Freight add(@RequestBody Freight newFreight) {
return service.add(newFreight);
public Freight add(@RequestBody FreightRequest newFreight) {
return service.add(newFreight.toEntity());
}
@Operation(
@@ -91,8 +92,8 @@ public class FreightController {
description = "Invalid input"
)
@PutMapping("/{id}")
public Freight update(@PathVariable("id") long id, @RequestBody Freight newFreight) {
return service.update(id, newFreight);
public Freight update(@PathVariable("id") long id, @RequestBody FreightRequest newFreight) {
return service.update(id, newFreight.toEntity());
}
@Operation(
@@ -108,8 +109,8 @@ public class FreightController {
description = "Invalid input"
)
@PatchMapping("/{id}")
public Freight updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) {
return service.update(id, newFreight);
public Freight updatePatch(@PathVariable("id") long id, @RequestBody FreightRequest newFreight) {
return service.update(id, newFreight.toEntity());
}
@Operation(

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -10,6 +10,7 @@ import org.springframework.http.MediaType;
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.Vehicle;
@@ -153,8 +154,8 @@ public class RouteController {
description = "Route object to be created. Must include valid vehicleId and freightId list.",
required = true
)
@RequestBody Route newRoute) {
return routeService.add(newRoute);
@RequestBody RouteRequest newRoute) {
return routeService.add(newRoute.toEntity());
}
@Operation(
@@ -177,8 +178,8 @@ public class RouteController {
description = "Updated route object",
required = true
)
@RequestBody Route newRoute) {
return routeService.update(id, newRoute);
@RequestBody RouteRequest newRoute) {
return routeService.update(id, newRoute.toEntity());
}
@Operation(
@@ -201,8 +202,8 @@ public class RouteController {
description = "Route object with fields to update",
required = true
)
@RequestBody Route newRoute) {
return routeService.update(id, newRoute);
@RequestBody RouteRequest newRoute) {
return routeService.update(id, newRoute.toEntity());
}

View File

@@ -8,6 +8,7 @@ import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import ua.com.dxrkness.dto.VehicleRequest;
import ua.com.dxrkness.model.Vehicle;
import ua.com.dxrkness.service.VehicleService;
@@ -68,8 +69,8 @@ public class VehicleController {
description = "Vehicle object to be created",
required = true
)
@RequestBody Vehicle newVehicle) {
return service.add(newVehicle);
@RequestBody VehicleRequest newVehicle) {
return service.add(newVehicle.toEntity());
}
@Operation(
@@ -86,8 +87,8 @@ public class VehicleController {
description = "Updated vehicle object",
required = true
)
@RequestBody Vehicle newVehicle) {
return service.update(id, newVehicle);
@RequestBody VehicleRequest newVehicle) {
return service.update(id, newVehicle.toEntity());
}
@Operation(
@@ -104,8 +105,8 @@ public class VehicleController {
description = "Vehicle object with fields to update",
required = true
)
@RequestBody Vehicle newVehicle) {
return service.update(id, newVehicle);
@RequestBody VehicleRequest newVehicle) {
return service.update(id, newVehicle.toEntity());
}

View File

@@ -0,0 +1 @@
["Клієнт успішно запускається та спрямовує всі запити + + + на API Gateway","Клієнт демонструє успішний обмін даними (GET/POST) + + + з мінімум двома публічними сервісами","Коректна обробка вхідних даних: Клієнт відображає дані, отримані з мікросервісів, у зрозумілому для + + + користувача вигляді.","Реалізовано функціональність \"Перегляд (Read): Відображення повного списку замовлень та списку + + + ресторанів","Реалізовано функціональність \"Створення (Create)\": Наявність форми для створення нової сутності, яка + + успішно відправляє POST-запит.","Реалізовано функціональність \"Редагування/Видалення (Update/Delete)\" для однієї + + ключової сутності.","Складна агрегація: Клієнт демонструє агрегацію + даних з кількох сервісів.","Користувацька взаємодія: Реалізовано елементи динамічної взаємодії (наприклад, перегляд деталей + + замовлення).","Обробка каскадних операцій: Клієнт демонструє виклик операції, яка запускає каскад внутрішніх + міжсервісних викликів на бекенді.","Обробка помилок (UX): Реалізовано механізм дружнього сповіщення користувача при отриманні + + + помилки від Gateway (HTTP 4xx/5xx).","Валідація вводу (Frontend): Реалізовано базову перевірку вхідних даних на стороні клієнта перед + + відправкою запиту.","Захист від дублювання (Ідемпотентність): Реалізовано механізм запобігання дублюванню для + неідемпотентних запитів.","Дизайн та Юзабіліті: Інтерфейс є інтуїтивно зрозумілим, має логічну навігацію та адекватний + візуальний дизайн."]