Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
617650929a | ||
|
|
b70585f01d | ||
|
|
f7a193d1c5 | ||
|
|
0040a5c33b | ||
|
|
fb9567eee5 | ||
|
|
3419f3d4c9 | ||
|
|
d1237bf1d2 | ||
|
|
d4b48b05ee | ||
|
|
38e1063ac5 | ||
|
|
04a9f197e8 | ||
|
|
27b7cecfdc | ||
|
|
e814aabcc8 | ||
|
|
4c8a3d5dfc | ||
|
|
187c6b0f99 | ||
|
|
24c7ec4917 | ||
|
|
0d61826c70 | ||
|
|
364f711a97 | ||
|
|
777cb74574 | ||
|
|
52addba909 | ||
|
|
81250953eb | ||
|
|
c4803ef650 | ||
|
|
0f97a49e26 | ||
|
|
5a974bac8f | ||
|
|
98380a06b9 | ||
|
|
10f01a9346 | ||
|
|
7caa45a1fb | ||
|
|
a22845add8 | ||
|
|
e8e7c4f642 | ||
|
|
da7a729321 | ||
|
|
55c2628c75 | ||
|
|
75d1d5f7fa | ||
|
|
2622d9f01e | ||
|
|
a7407b8e06 | ||
|
|
dadcf97bec | ||
|
|
d2a3a22ddf | ||
|
|
2a145bbbe7 | ||
|
|
fbe0861c29 | ||
|
|
c1f62f66e1 | ||
|
|
698156cf00 | ||
|
|
8d73fe3ed1 | ||
|
|
052a0a5d03 | ||
|
|
e66fe9b40e | ||
|
|
8a71dad977 | ||
|
|
45ed773119 | ||
|
|
a0cc120fec | ||
|
|
61fbc8e16b | ||
|
|
c1993d27b2 | ||
|
|
e7249df23f | ||
|
|
925d9eef73 | ||
|
|
35b2c9539a | ||
|
|
9f6cc616ad | ||
|
|
9edb0e6ce8 | ||
|
|
5e7af075cb | ||
|
|
6dac42bbbe | ||
|
|
981045da2f | ||
|
|
35779d2b3d | ||
|
|
4b24f433a5 | ||
|
|
dcf50fb8af | ||
|
|
cc4d56ae4c |
+1458
-268
File diff suppressed because it is too large
Load Diff
Generated
+190
-157
@@ -14,6 +14,7 @@
|
|||||||
"@vueuse/integrations": "^14.3.0",
|
"@vueuse/integrations": "^14.3.0",
|
||||||
"@vueuse/router": "^14.3.0",
|
"@vueuse/router": "^14.3.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"gemory": "file:",
|
"gemory": "file:",
|
||||||
"loglevel": "^1.8.1",
|
"loglevel": "^1.8.1",
|
||||||
"loglevel-plugin-prefix": "^0.8.4",
|
"loglevel-plugin-prefix": "^0.8.4",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-chartjs": "^5.3.4",
|
||||||
"vue-router": "^5.0.7"
|
"vue-router": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -229,14 +231,20 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
"@tybys/wasm-util": "^0.10.2"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -532,49 +540,49 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
|
||||||
"integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
|
"integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
"enhanced-resolve": "^5.21.0",
|
"enhanced-resolve": "5.21.6",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.7.0",
|
||||||
"lightningcss": "1.32.0",
|
"lightningcss": "1.32.0",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"source-map-js": "^1.2.1",
|
"source-map-js": "^1.2.1",
|
||||||
"tailwindcss": "4.3.0"
|
"tailwindcss": "4.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
|
||||||
"integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
|
"integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 20"
|
"node": ">= 20"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tailwindcss/oxide-android-arm64": "4.3.0",
|
"@tailwindcss/oxide-android-arm64": "4.3.1",
|
||||||
"@tailwindcss/oxide-darwin-arm64": "4.3.0",
|
"@tailwindcss/oxide-darwin-arm64": "4.3.1",
|
||||||
"@tailwindcss/oxide-darwin-x64": "4.3.0",
|
"@tailwindcss/oxide-darwin-x64": "4.3.1",
|
||||||
"@tailwindcss/oxide-freebsd-x64": "4.3.0",
|
"@tailwindcss/oxide-freebsd-x64": "4.3.1",
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
|
"@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
|
"@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
|
||||||
"@tailwindcss/oxide-linux-x64-musl": "4.3.0",
|
"@tailwindcss/oxide-linux-x64-musl": "4.3.1",
|
||||||
"@tailwindcss/oxide-wasm32-wasi": "4.3.0",
|
"@tailwindcss/oxide-wasm32-wasi": "4.3.1",
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
|
"@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
|
||||||
"integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
|
"integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -589,9 +597,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
|
||||||
"integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
|
"integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -606,9 +614,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
|
||||||
"integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
|
"integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -623,9 +631,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
|
||||||
"integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
|
"integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -640,9 +648,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
|
||||||
"integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
|
"integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -657,9 +665,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
|
||||||
"integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
|
"integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -677,9 +685,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
|
||||||
"integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
|
"integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -697,9 +705,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
|
||||||
"integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
|
"integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -717,9 +725,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
|
||||||
"integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
|
"integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -737,9 +745,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
|
||||||
"integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
|
"integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
|
||||||
"bundleDependencies": [
|
"bundleDependencies": [
|
||||||
"@napi-rs/wasm-runtime",
|
"@napi-rs/wasm-runtime",
|
||||||
"@emnapi/core",
|
"@emnapi/core",
|
||||||
@@ -759,7 +767,7 @@
|
|||||||
"@emnapi/runtime": "^1.10.0",
|
"@emnapi/runtime": "^1.10.0",
|
||||||
"@emnapi/wasi-threads": "^1.2.1",
|
"@emnapi/wasi-threads": "^1.2.1",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.4",
|
"@napi-rs/wasm-runtime": "^1.1.4",
|
||||||
"@tybys/wasm-util": "^0.10.1",
|
"@tybys/wasm-util": "^0.10.2",
|
||||||
"tslib": "^2.8.1"
|
"tslib": "^2.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -767,9 +775,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
|
||||||
"integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
|
"integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -784,9 +792,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
|
||||||
"integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
|
"integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -801,15 +809,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/vite": {
|
"node_modules/@tailwindcss/vite": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
|
||||||
"integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==",
|
"integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/node": "4.3.0",
|
"@tailwindcss/node": "4.3.1",
|
||||||
"@tailwindcss/oxide": "4.3.0",
|
"@tailwindcss/oxide": "4.3.1",
|
||||||
"tailwindcss": "4.3.0"
|
"tailwindcss": "4.3.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
@@ -857,9 +865,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.9.1",
|
"version": "25.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
|
||||||
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
"integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1066,13 +1074,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
|
||||||
"integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==",
|
"integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.3",
|
"@babel/parser": "^7.29.7",
|
||||||
"@vue/shared": "3.5.35",
|
"@vue/shared": "3.5.38",
|
||||||
"entities": "^7.0.1",
|
"entities": "^7.0.1",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
@@ -1085,26 +1093,26 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
|
||||||
"integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==",
|
"integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.5.35",
|
"@vue/compiler-core": "3.5.38",
|
||||||
"@vue/shared": "3.5.35"
|
"@vue/shared": "3.5.38"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
|
||||||
"integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==",
|
"integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.3",
|
"@babel/parser": "^7.29.7",
|
||||||
"@vue/compiler-core": "3.5.35",
|
"@vue/compiler-core": "3.5.38",
|
||||||
"@vue/compiler-dom": "3.5.35",
|
"@vue/compiler-dom": "3.5.38",
|
||||||
"@vue/compiler-ssr": "3.5.35",
|
"@vue/compiler-ssr": "3.5.38",
|
||||||
"@vue/shared": "3.5.35",
|
"@vue/shared": "3.5.38",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.15",
|
||||||
@@ -1118,13 +1126,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
|
||||||
"integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==",
|
"integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.35",
|
"@vue/compiler-dom": "3.5.38",
|
||||||
"@vue/shared": "3.5.35"
|
"@vue/shared": "3.5.38"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-api": {
|
"node_modules/@vue/devtools-api": {
|
||||||
@@ -1161,9 +1169,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/language-core": {
|
"node_modules/@vue/language-core": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.5.tgz",
|
||||||
"integrity": "sha512-X6p+7nfY7vVT6dQwUJ+v0Jfq/lwIfhL2jMi91dQ3ln4hnlGXlxsDu/FNkeyHYgvYtyQy18ZX76IZy7X4diDbiQ==",
|
"integrity": "sha512-UkKu5nhX89fg4VhlG/FOeI10G3cj/7radKT/cy9BT4Q9qJmJlSTAc/dP63Xqs29aypN4f39xUV6PsLNk/dcD6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1177,53 +1185,53 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
|
||||||
"integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==",
|
"integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.5.35"
|
"@vue/shared": "3.5.38"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-core": {
|
"node_modules/@vue/runtime-core": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
|
||||||
"integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==",
|
"integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.35",
|
"@vue/reactivity": "3.5.38",
|
||||||
"@vue/shared": "3.5.35"
|
"@vue/shared": "3.5.38"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-dom": {
|
"node_modules/@vue/runtime-dom": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
|
||||||
"integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==",
|
"integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.35",
|
"@vue/reactivity": "3.5.38",
|
||||||
"@vue/runtime-core": "3.5.35",
|
"@vue/runtime-core": "3.5.38",
|
||||||
"@vue/shared": "3.5.35",
|
"@vue/shared": "3.5.38",
|
||||||
"csstype": "^3.2.3"
|
"csstype": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/server-renderer": {
|
"node_modules/@vue/server-renderer": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
|
||||||
"integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==",
|
"integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.5.35",
|
"@vue/compiler-ssr": "3.5.38",
|
||||||
"@vue/shared": "3.5.35"
|
"@vue/shared": "3.5.38"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "3.5.35"
|
"vue": "3.5.38"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
|
||||||
"integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==",
|
"integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vueuse/components": {
|
"node_modules/@vueuse/components": {
|
||||||
@@ -1360,9 +1368,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.16.0",
|
"version": "8.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@@ -1440,9 +1448,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.16.1",
|
"version": "1.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz",
|
||||||
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
|
"integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.16.0",
|
"follow-redirects": "^1.16.0",
|
||||||
@@ -1483,6 +1491,18 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||||
@@ -1604,9 +1624,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.22.1",
|
"version": "5.21.6",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
|
||||||
"integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==",
|
"integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1745,16 +1765,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
"hasown": "^2.0.2",
|
"hasown": "^2.0.4",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.35"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -2387,15 +2407,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/obug": {
|
"node_modules/obug": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
|
||||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
"integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/sxzz",
|
"https://github.com/sponsors/sxzz",
|
||||||
"https://opencollective.com/debug"
|
"https://opencollective.com/debug"
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-browserify": {
|
"node_modules/path-browserify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -2636,9 +2659,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
|
||||||
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
|
"integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2764,9 +2787,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.15",
|
"version": "8.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||||
"integrity": "sha512-qpgllRxrLqwsMAGRdLhsEr9bepaOQk1rxH1xT2coBXLaEB/bfkqQj1j7RMxwMfnYrvO1ZnFMiwX+wBVgnsyn0g==",
|
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2952,16 +2975,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.35",
|
"version": "3.5.38",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
|
||||||
"integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
|
"integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.35",
|
"@vue/compiler-dom": "3.5.38",
|
||||||
"@vue/compiler-sfc": "3.5.35",
|
"@vue/compiler-sfc": "3.5.38",
|
||||||
"@vue/runtime-dom": "3.5.35",
|
"@vue/runtime-dom": "3.5.38",
|
||||||
"@vue/server-renderer": "3.5.35",
|
"@vue/server-renderer": "3.5.38",
|
||||||
"@vue/shared": "3.5.35"
|
"@vue/shared": "3.5.38"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
@@ -2972,6 +2995,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-chartjs": {
|
||||||
|
"version": "5.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.4.tgz",
|
||||||
|
"integrity": "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz",
|
||||||
@@ -3055,14 +3088,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue-tsc": {
|
"node_modules/vue-tsc": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.5.tgz",
|
||||||
"integrity": "sha512-SWUEG7YRUeDJHT7Xsuhf02elYX2gxPzzAII7OxDAh4KNOr4QHQ0Lls0YfnaO5GNd560CwVa2HTfdqmA5MqvRqQ==",
|
"integrity": "sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/typescript": "2.4.28",
|
"@volar/typescript": "2.4.28",
|
||||||
"@vue/language-core": "3.3.3"
|
"@vue/language-core": "3.3.5"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vue-tsc": "bin/vue-tsc.js"
|
"vue-tsc": "bin/vue-tsc.js"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@vueuse/integrations": "^14.3.0",
|
"@vueuse/integrations": "^14.3.0",
|
||||||
"@vueuse/router": "^14.3.0",
|
"@vueuse/router": "^14.3.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"gemory": "file:",
|
"gemory": "file:",
|
||||||
"loglevel": "^1.8.1",
|
"loglevel": "^1.8.1",
|
||||||
"loglevel-plugin-prefix": "^0.8.4",
|
"loglevel-plugin-prefix": "^0.8.4",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-chartjs": "^5.3.4",
|
||||||
"vue-router": "^5.0.7"
|
"vue-router": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+16
-1
@@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed} from 'vue';
|
import {computed, watchEffect} from 'vue';
|
||||||
import {RouterView, useRoute} from 'vue-router';
|
import {RouterView, useRoute} from 'vue-router';
|
||||||
import {Sidebar} from './sidebar';
|
import {Sidebar} from './sidebar';
|
||||||
import {ConfirmModal} from '@/confirm';
|
import {ConfirmModal} from '@/confirm';
|
||||||
|
import {toast, ToastContainer} from '@/toast';
|
||||||
|
import {onActivitiesProcessed} from '@/activity';
|
||||||
|
import {useAuthStore} from '@/auth';
|
||||||
import {routeNames} from '@/routes';
|
import {routeNames} from '@/routes';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -10,6 +13,17 @@ const route = useRoute();
|
|||||||
const hideSidebar = computed(() => {
|
const hideSidebar = computed(() => {
|
||||||
return route.name === routeNames.callback || route.name === routeNames.about;
|
return route.name === routeNames.callback || route.name === routeNames.about;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
watchEffect(onCleanup => {
|
||||||
|
if (!authStore.isAuthenticated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const unsubscribe = onActivitiesProcessed(() => toast.info('New activities have been processed'));
|
||||||
|
|
||||||
|
onCleanup(unsubscribe);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -23,6 +37,7 @@ const hideSidebar = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<ConfirmModal />
|
<ConfirmModal />
|
||||||
|
<ToastContainer />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {computedAsync} from "@vueuse/core";
|
||||||
|
import {ActivitySourceResponse, ActivitySourceResponseTypeEnum} from "@/generated/mammon";
|
||||||
|
import {CharacterLabel, useCharactersStore} from "@/characters";
|
||||||
|
import {CorporationLabel, useCorporationsStore} from "@/corporations";
|
||||||
|
import {LedgerLabel, useLedgersStore} from "@/ledger";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
source: ActivitySourceResponse;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
size: 32
|
||||||
|
});
|
||||||
|
|
||||||
|
const charactersStore = useCharactersStore();
|
||||||
|
const corporationsStore = useCorporationsStore();
|
||||||
|
const ledgersStore = useLedgersStore();
|
||||||
|
|
||||||
|
const character = computedAsync(async () => {
|
||||||
|
if (props.source.type === ActivitySourceResponseTypeEnum.Character && props.source.characterId) {
|
||||||
|
return await charactersStore.findById(props.source.characterId);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
|
const corporation = computedAsync(async () => {
|
||||||
|
if (props.source.type === ActivitySourceResponseTypeEnum.Corporation && props.source.corporationId) {
|
||||||
|
return await corporationsStore.findById(props.source.corporationId);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
|
const ledger = computed(() =>
|
||||||
|
props.source.type === ActivitySourceResponseTypeEnum.Ledger && props.source.ledgerId
|
||||||
|
? ledgersStore.findById(props.source.ledgerId)
|
||||||
|
: undefined);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CharacterLabel v-if="source.type === ActivitySourceResponseTypeEnum.Character && character" :character="character" :size="size" />
|
||||||
|
<CorporationLabel v-else-if="source.type === ActivitySourceResponseTypeEnum.Corporation && source.corporationId" :corporation-id="source.corporationId" :corporation="corporation" :division="source.division" :size="size" />
|
||||||
|
<LedgerLabel v-else-if="ledger" :ledger="ledger" link />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import log from "loglevel";
|
||||||
|
import {activityApi} from "@/mammon";
|
||||||
|
|
||||||
|
const RECONNECT_DELAY_MILLIS = 3_000;
|
||||||
|
const FRAME_SEPARATOR = "\n\n";
|
||||||
|
const PROCESSED_EVENT = "event:processed";
|
||||||
|
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
let controller: AbortController | undefined;
|
||||||
|
let reconnectTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
const notifyProcessed = () => listeners.forEach(listener => listener());
|
||||||
|
|
||||||
|
const scheduleReconnect = () => {
|
||||||
|
if (listeners.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reconnectTimer = setTimeout(() => connect(), RECONNECT_DELAY_MILLIS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const consume = async (body: ReadableStream<Uint8Array>) => {
|
||||||
|
const reader = body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
for (; ;) {
|
||||||
|
const {value, done} = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buffer += decoder.decode(value, {stream: true});
|
||||||
|
|
||||||
|
let boundary = buffer.indexOf(FRAME_SEPARATOR);
|
||||||
|
|
||||||
|
while (boundary !== -1) {
|
||||||
|
const frame = buffer.slice(0, boundary);
|
||||||
|
buffer = buffer.slice(boundary + FRAME_SEPARATOR.length);
|
||||||
|
|
||||||
|
if (frame.split("\n").some(line => line.trim() === PROCESSED_EVENT)) {
|
||||||
|
notifyProcessed();
|
||||||
|
}
|
||||||
|
boundary = buffer.indexOf(FRAME_SEPARATOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = async (): Promise<void> => {
|
||||||
|
if (listeners.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const signal = controller?.signal;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await activityApi.streamProcessing({
|
||||||
|
responseType: "stream",
|
||||||
|
adapter: "fetch",
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
await consume(response.data as unknown as ReadableStream<Uint8Array>);
|
||||||
|
scheduleReconnect();
|
||||||
|
} catch (error) {
|
||||||
|
if (listeners.size === 0 || signal?.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.debug("Activity processing SSE error, reconnecting", error);
|
||||||
|
scheduleReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = undefined;
|
||||||
|
}
|
||||||
|
controller?.abort();
|
||||||
|
controller = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onActivitiesProcessed = (listener: () => void): (() => void) => {
|
||||||
|
listeners.add(listener);
|
||||||
|
|
||||||
|
if (!controller) {
|
||||||
|
controller = new AbortController();
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (listeners.delete(listener) && listeners.size === 0) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export {default as SourceLabel} from './SourceLabel.vue';
|
||||||
|
export {onActivitiesProcessed} from './activitiesProcessed';
|
||||||
|
export {useActivitiesProcessed} from './useActivitiesProcessed';
|
||||||
|
export {useProcessedResource} from './useProcessedResource';
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import {onUnmounted} from "vue";
|
||||||
|
import {onActivitiesProcessed} from "./activitiesProcessed";
|
||||||
|
|
||||||
|
export const useActivitiesProcessed = (listener: () => void): (() => void) => {
|
||||||
|
const unsubscribe = onActivitiesProcessed(listener);
|
||||||
|
onUnmounted(unsubscribe);
|
||||||
|
return unsubscribe;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {ref, type Ref, watchEffect} from "vue";
|
||||||
|
import {useActivitiesProcessed} from "./useActivitiesProcessed";
|
||||||
|
|
||||||
|
export const useProcessedResource = <T>(loader: () => Promise<T>, initial: T): Ref<T> => {
|
||||||
|
const data = ref(initial) as Ref<T>;
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
data.value = await loader();
|
||||||
|
};
|
||||||
|
|
||||||
|
watchEffect(load);
|
||||||
|
useActivitiesProcessed(load);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {CharacterResponse} from "@/generated/mammon";
|
||||||
|
import {
|
||||||
|
addCharacter as addCharacterRequest,
|
||||||
|
fetchMe,
|
||||||
|
mammonLoginUrl,
|
||||||
|
postLogout,
|
||||||
|
refreshAccessToken,
|
||||||
|
setOnAuthExpired
|
||||||
|
} from "@/mammon";
|
||||||
|
import {setAccessToken} from "./token";
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const userId = ref<string | null>(null);
|
||||||
|
const characters = ref<CharacterResponse[]>([]);
|
||||||
|
const isAuthenticated = ref(false);
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
setAccessToken(null);
|
||||||
|
userId.value = null;
|
||||||
|
characters.value = [];
|
||||||
|
isAuthenticated.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh = async (): Promise<boolean> => {
|
||||||
|
const token = await refreshAccessToken();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
isAuthenticated.value = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetch = async (): Promise<void> => {
|
||||||
|
const me = await fetchMe();
|
||||||
|
|
||||||
|
userId.value = me.userId;
|
||||||
|
characters.value = me.characters;
|
||||||
|
isAuthenticated.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = (): void => {
|
||||||
|
window.location.assign(mammonLoginUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCharacter = async (): Promise<void> => {
|
||||||
|
await addCharacterRequest();
|
||||||
|
window.location.assign(mammonLoginUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await postLogout();
|
||||||
|
} finally {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bootstrap = async (): Promise<void> => {
|
||||||
|
setOnAuthExpired(() => {
|
||||||
|
clear();
|
||||||
|
login();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (await refresh()) {
|
||||||
|
await fetch().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {userId, characters, isAuthenticated, refresh, fetch, login, addCharacter, logout, bootstrap};
|
||||||
|
})
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './token';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
let accessToken: string | null = null;
|
||||||
|
|
||||||
|
export const getAccessToken = (): string | null => accessToken;
|
||||||
|
|
||||||
|
export const setAccessToken = (token: string | null): void => {
|
||||||
|
accessToken = token;
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {activityApi, characterApi} from "@/mammon";
|
import {characterApi} from "@/mammon";
|
||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {CharacterResponse} from "@/generated/mammon";
|
import {CharacterResponse} from "@/generated/mammon";
|
||||||
@@ -18,11 +18,9 @@ export const useCharactersStore = defineStore('characters', () => {
|
|||||||
return character;
|
return character;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reloadActivities = (characterId: number): Promise<void> => activityApi.fetchNewActivitiesForCharacter(characterId) as Promise<void>;
|
|
||||||
|
|
||||||
const refresh = () => characterApi.findAllCharacters().then(response => characters.value = response.data);
|
const refresh = () => characterApi.findAllCharacters().then(response => characters.value = response.data);
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
return {characters, findById, reloadActivities, refresh};
|
return {characters, findById};
|
||||||
})
|
})
|
||||||
@@ -14,7 +14,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
autoClose: true
|
autoClose: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = defineModel<boolean>('open', {default: false});
|
||||||
const root = ref<HTMLElement | null>(null);
|
const root = ref<HTMLElement | null>(null);
|
||||||
const floating = ref<HTMLElement | null>(null);
|
const floating = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
@@ -41,12 +41,11 @@ useEventListener('keyup', e => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="root" class="dropdown" :class="{'dropdown-open': isOpen, 'dropdown-close': !isOpen}" v-on-click-outside="[doAutoClose, { ignore: [floating] }]">
|
<div ref="root" class="dropdown" :class="{'dropdown-open': isOpen, 'dropdown-close': !isOpen}" v-on-click-outside="[doAutoClose, { ignore: [floating] }]">
|
||||||
<button @click="isOpen = !isOpen" class="cursor-pointer">
|
<button @click="isOpen = !isOpen" class="flex items-center cursor-pointer border-none bg-transparent hover:bg-transparent p-0">
|
||||||
<Transition
|
<Transition
|
||||||
|
mode="out-in"
|
||||||
enter-active-class="transition-transform"
|
enter-active-class="transition-transform"
|
||||||
enter-from-class="rotate-180"
|
enter-from-class="rotate-180">
|
||||||
leave-active-class="hidden"
|
|
||||||
leave-to-class="rotate-180">
|
|
||||||
<ChevronDownIcon v-if="!isOpen" class="chevron" />
|
<ChevronDownIcon v-if="!isOpen" class="chevron" />
|
||||||
<ChevronUpIcon v-else class="chevron" />
|
<ChevronUpIcon v-else class="chevron" />
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ min?: number; max?: number }>(), {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const from = defineModel<number>('from', {default: 0});
|
||||||
|
const to = defineModel<number>('to', {default: 0});
|
||||||
|
|
||||||
|
const container = ref<HTMLElement>();
|
||||||
|
|
||||||
|
const dragFill = (event: PointerEvent) => {
|
||||||
|
const rect = container.value?.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!rect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = (props.max - props.min) || 1;
|
||||||
|
const width = to.value - from.value;
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startFrom = from.value;
|
||||||
|
|
||||||
|
const onMove = (moveEvent: PointerEvent) => {
|
||||||
|
const delta = Math.round(((moveEvent.clientX - startX) / rect.width) * span);
|
||||||
|
const newFrom = Math.min(Math.max(startFrom + delta, props.min), props.max - width);
|
||||||
|
|
||||||
|
from.value = newFrom;
|
||||||
|
to.value = newFrom + width;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUp = () => {
|
||||||
|
window.removeEventListener('pointermove', onMove);
|
||||||
|
window.removeEventListener('pointerup', onUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', onMove);
|
||||||
|
window.addEventListener('pointerup', onUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(from, value => {
|
||||||
|
if (value > to.value) {
|
||||||
|
to.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(to, value => {
|
||||||
|
if (value < from.value) {
|
||||||
|
from.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fillStyle = computed(() => {
|
||||||
|
const span = (props.max - props.min) || 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: `${((from.value - props.min) / span) * 100}%`,
|
||||||
|
width: `${((to.value - from.value) / span) * 100}%`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="container" class="dual-range relative h-6 select-none">
|
||||||
|
<div class="absolute inset-x-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-gray-700 pointer-events-none"></div>
|
||||||
|
<div class="absolute top-1/2 h-2 -translate-y-1/2 rounded-full bg-emerald-500 cursor-grab active:cursor-grabbing"
|
||||||
|
:style="fillStyle" @pointerdown="dragFill"></div>
|
||||||
|
<input type="range" :min="min" :max="max" v-model.number="from"
|
||||||
|
class="absolute inset-0 w-full h-full m-0 bg-transparent appearance-none pointer-events-none" />
|
||||||
|
<input type="range" :min="min" :max="max" v-model.number="to"
|
||||||
|
class="absolute inset-0 w-full h-full m-0 bg-transparent appearance-none pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/style.css";
|
||||||
|
|
||||||
|
.dual-range input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
@apply w-4 h-4 rounded-full bg-emerald-500 border-2 border-gray-200 cursor-pointer pointer-events-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-range input[type="range"]::-moz-range-thumb {
|
||||||
|
@apply w-4 h-4 rounded-full bg-emerald-500 border-2 border-gray-200 cursor-pointer pointer-events-auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts" generic="T">
|
<script setup lang="ts" generic="T">
|
||||||
import {vOnClickOutside} from '@vueuse/components';
|
import {vOnClickOutside} from '@vueuse/components';
|
||||||
import {useVirtualList} from '@vueuse/core';
|
import {useVirtualList} from '@vueuse/core';
|
||||||
import {computed, ref} from 'vue';
|
import {computed, ref, watch} from 'vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: T[];
|
items: T[];
|
||||||
@@ -15,6 +15,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
const modelValue = defineModel<T>();
|
const modelValue = defineModel<T>();
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
|
|
||||||
|
watch(() => props.items, items => {
|
||||||
|
if (items.length > 0) {
|
||||||
|
isOpen.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
const currentIndex = ref(-1);
|
const currentIndex = ref(-1);
|
||||||
const {list, scrollTo, containerProps, wrapperProps} = useVirtualList(computed(() => props.items), {
|
const {list, scrollTo, containerProps, wrapperProps} = useVirtualList(computed(() => props.items), {
|
||||||
itemHeight: () => props.itemHeight,
|
itemHeight: () => props.itemHeight,
|
||||||
@@ -44,15 +50,15 @@ const submit = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div @click="isOpen = true" v-on-click-outside="() => isOpen = false">
|
<div class="relative" @click="isOpen = true" v-on-click-outside="() => isOpen = false">
|
||||||
<div class="fake-input" @keyup.enter="submit" @keyup.down="moveDown" @keyup.up="moveUp">
|
<div class="fake-input" @keyup.enter="submit" @keyup.down="moveDown" @keyup.up="moveUp">
|
||||||
<slot name="input" :value="modelValue" />
|
<slot name="input" :value="modelValue" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isOpen && items.length" class="z-20 absolute">
|
<div v-if="isOpen && items.length" class="z-20 absolute w-full">
|
||||||
<div v-bind="containerProps" class="rounded-b" style="height: 300px">
|
<div v-bind="containerProps" class="rounded-b" style="height: 300px">
|
||||||
<div v-bind="wrapperProps">
|
<div v-bind="wrapperProps">
|
||||||
<div v-for="s in list" :key="s.index"
|
<div v-for="s in list" :key="s.index"
|
||||||
class="hover:bg-slate-700 cursor-pointer"
|
class="hover:bg-slate-700 cursor-pointer overflow-hidden"
|
||||||
:class="s.index === currentIndex ? 'bg-emerald-500' : 'bg-slate-500'"
|
:class="s.index === currentIndex ? 'bg-emerald-500' : 'bg-slate-500'"
|
||||||
@click.stop="select(s.data)">
|
@click.stop="select(s.data)">
|
||||||
<slot name="item" :item="s.data" />
|
<slot name="item" :item="s.data" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as ClipboardButton } from './ClipboardButton.vue';
|
export { default as ClipboardButton } from './ClipboardButton.vue';
|
||||||
export { default as Dropdown } from './Dropdown.vue';
|
export { default as Dropdown } from './Dropdown.vue';
|
||||||
|
export { default as DualRangeSlider } from './DualRangeSlider.vue';
|
||||||
export { default as LoadingSpinner } from './LoadingSpinner.vue';
|
export { default as LoadingSpinner } from './LoadingSpinner.vue';
|
||||||
export { default as Modal } from './Modal.vue';
|
export { default as Modal } from './Modal.vue';
|
||||||
export { default as ProgressBar } from './ProgressBar.vue';
|
export { default as ProgressBar } from './ProgressBar.vue';
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe('useSort', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Hides ignored columns', () => {
|
test('Hides ignored columns', () => {
|
||||||
const { showColumn } = useSort(array, { ignoredColums: ['key1'] })
|
const { showColumn } = useSort(array, { ignoredColumns: ['key1'] })
|
||||||
|
|
||||||
expect(showColumn('key1')).toBe(false)
|
expect(showColumn('key1')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type SortDirection = "asc" | "desc";
|
|||||||
export type UseSortOptions = {
|
export type UseSortOptions = {
|
||||||
defaultSortKey?: string;
|
defaultSortKey?: string;
|
||||||
defaultSortDirection?: SortDirection;
|
defaultSortDirection?: SortDirection;
|
||||||
ignoredColums?: MaybeRefOrGetter<string[]>;
|
ignoredColumns?: MaybeRefOrGetter<string[]>;
|
||||||
headerComponent?: HeaderComponent;
|
headerComponent?: HeaderComponent;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export const useSort = <T>(array: MaybeRefOrGetter<T[]>, options?: UseSortOption
|
|||||||
sortKey.value = key;
|
sortKey.value = key;
|
||||||
sortDirection.value = direction;
|
sortDirection.value = direction;
|
||||||
};
|
};
|
||||||
const showColumn = (sortKey: string) => !toValue(options?.ignoredColums)?.includes(sortKey);
|
const showColumn = (sortKey: string) => !toValue(options?.ignoredColumns)?.includes(sortKey);
|
||||||
const headerProps = computed(() => ({
|
const headerProps = computed(() => ({
|
||||||
onSort: sortBy,
|
onSort: sortBy,
|
||||||
showColumn,
|
showColumn,
|
||||||
|
|||||||
@@ -8,20 +8,27 @@ const open = defineModel('open', { default: false });
|
|||||||
|
|
||||||
const { width, height } = useSharedWindowSize();
|
const { width, height } = useSharedWindowSize();
|
||||||
const mainDiv = ref<HTMLDivElement | null>(null);
|
const mainDiv = ref<HTMLDivElement | null>(null);
|
||||||
const { top, left } = useElementBounding(mainDiv);
|
const { top, bottom, left, right } = useElementBounding(mainDiv);
|
||||||
|
|
||||||
const positions = computed(() => {
|
const positions = computed(() => [
|
||||||
if (top.value < height.value / 2) {
|
top.value < height.value / 2 ? 'top' : 'bottom',
|
||||||
if (left.value < width.value / 2) {
|
left.value < width.value / 2 ? 'left' : 'right',
|
||||||
return ['top', 'left'];
|
]);
|
||||||
}
|
|
||||||
return ['top', 'right'];
|
const floatingStyle = computed(() => {
|
||||||
|
const style: Record<string, string> = {};
|
||||||
|
if (positions.value.includes('top')) {
|
||||||
|
style.top = `${bottom.value}px`;
|
||||||
|
} else {
|
||||||
|
style.bottom = `${height.value - top.value}px`;
|
||||||
}
|
}
|
||||||
if (left.value < width.value / 2) {
|
if (positions.value.includes('left')) {
|
||||||
return ['bottom', 'left'];
|
style.left = `${left.value}px`;
|
||||||
|
} else {
|
||||||
|
style.right = `${width.value - right.value}px`;
|
||||||
}
|
}
|
||||||
return ['bottom', 'right'];
|
return style;
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -35,12 +42,18 @@ const positions = computed(() => {
|
|||||||
<div v-element-hover="(h: boolean) => open = h" class="m-auto header">
|
<div v-element-hover="(h: boolean) => open = h" class="m-auto header">
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="open" class="m-auto">
|
<Teleport to="body">
|
||||||
<div class="z-10 relative">
|
<div v-if="open" class="tooltip-floating" :style="floatingStyle">
|
||||||
<div class="absolute">
|
<slot />
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/style.css";
|
||||||
|
|
||||||
|
.tooltip-floating {
|
||||||
|
@apply fixed z-30;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useAutoRefresh } from './useAutoRefresh';
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {onUnmounted, ref} from "vue";
|
||||||
|
|
||||||
|
const DEFAULT_INTERVAL = 1_000;
|
||||||
|
|
||||||
|
export const useAutoRefresh = (callback: () => void | Promise<void>, interval: number = DEFAULT_INTERVAL) => {
|
||||||
|
const active = ref(false);
|
||||||
|
let timer: ReturnType<typeof setInterval> | undefined;
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = undefined;
|
||||||
|
}
|
||||||
|
active.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
if (active.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active.value = true;
|
||||||
|
timer = setInterval(callback, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggle = () => active.value ? stop() : start();
|
||||||
|
|
||||||
|
onUnmounted(stop);
|
||||||
|
|
||||||
|
return {active, start, stop, toggle};
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {Corporation} from "./corporations.ts";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
corporationId: number;
|
||||||
|
corporation?: Corporation | null;
|
||||||
|
division?: number | null;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
size: 32
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex">
|
||||||
|
<img class="me-2" :src="`https://images.evetech.net/corporations/${corporationId}/logo?size=${size}`" />
|
||||||
|
<span>{{ corporation?.name ?? 'Corporation' }}<template v-if="division"> · Division {{ division }}</template></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {corporationApi} from "@/mammon";
|
||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {CorporationResponse} from "@/generated/mammon";
|
||||||
|
|
||||||
|
export type Corporation = CorporationResponse
|
||||||
|
|
||||||
|
export const useCorporationsStore = defineStore('corporations', () => {
|
||||||
|
const corporations = ref<Record<number, Corporation | null>>({});
|
||||||
|
const inFlight = new Map<number, Promise<Corporation | undefined>>();
|
||||||
|
|
||||||
|
const findById = async (corporationId: number): Promise<Corporation | undefined> => {
|
||||||
|
const cached = corporations.value[corporationId];
|
||||||
|
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = inFlight.get(corporationId);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
request = corporationApi.findCorporation(corporationId)
|
||||||
|
.then(response => {
|
||||||
|
corporations.value[corporationId] = response.data;
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
corporations.value[corporationId] = null;
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.finally(() => inFlight.delete(corporationId));
|
||||||
|
inFlight.set(corporationId, request);
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {corporations, findById};
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './corporations.ts'
|
||||||
|
|
||||||
|
export {default as CorporationLabel} from './CorporationLabel.vue';
|
||||||
+2602
-547
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import {Modal} from "@/components";
|
|||||||
import LedgerLabel from "./LedgerLabel.vue";
|
import LedgerLabel from "./LedgerLabel.vue";
|
||||||
import {PlusIcon, TrashIcon} from '@heroicons/vue/24/outline';
|
import {PlusIcon, TrashIcon} from '@heroicons/vue/24/outline';
|
||||||
import LedgerSelect from "@/ledger/LedgerSelect.vue";
|
import LedgerSelect from "@/ledger/LedgerSelect.vue";
|
||||||
|
import {toast} from "@/toast";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ledgerId?: string;
|
ledgerId?: string;
|
||||||
@@ -59,18 +60,16 @@ const title = computed(() => {
|
|||||||
|
|
||||||
const create = () => {
|
const create = () => {
|
||||||
if (type.value === LedgerTypes.Main) {
|
if (type.value === LedgerTypes.Main) {
|
||||||
ledgersStore.createMain({name: name.value})
|
return ledgersStore.createMain({name: name.value})
|
||||||
} else {
|
|
||||||
ledgersStore.createCombined({name: name.value, memberLedgerIds: members.value.map(l => l.ledgerId)})
|
|
||||||
}
|
}
|
||||||
|
return ledgersStore.createCombined({name: name.value, memberLedgerIds: members.value.map(l => l.ledgerId)})
|
||||||
}
|
}
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
if (type.value === LedgerTypes.Main) {
|
if (type.value === LedgerTypes.Main) {
|
||||||
ledgersStore.updateMain(props.ledgerId, {name: name.value})
|
return ledgersStore.updateMain(props.ledgerId, {name: name.value})
|
||||||
} else {
|
|
||||||
ledgersStore.updateCombined(props.ledgerId, {name: name.value, memberLedgerIds: members.value.map(l => l.ledgerId)})
|
|
||||||
}
|
}
|
||||||
|
return ledgersStore.updateCombined(props.ledgerId, {name: name.value, memberLedgerIds: members.value.map(l => l.ledgerId)})
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
@@ -78,12 +77,11 @@ const save = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCreating.value) {
|
const creating = isCreating.value;
|
||||||
create();
|
const action = creating ? create() : update();
|
||||||
} else {
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
modalOpen.value = false;
|
modalOpen.value = false;
|
||||||
|
action.then(() => toast.success(creating ? 'Ledger created' : 'Ledger updated'));
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ open });
|
defineExpose({ open });
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {describe, expect, test} from 'vitest';
|
||||||
|
import {appraiseItemBalances} from './balanceAppraisal';
|
||||||
|
import type {MarketTypePrice} from '@/market';
|
||||||
|
|
||||||
|
const price = (id: number, buy: number, sell: number): MarketTypePrice => ({
|
||||||
|
type: {id, name: `type-${id}`} as MarketTypePrice['type'],
|
||||||
|
buy,
|
||||||
|
sell,
|
||||||
|
orderCount: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('appraiseItemBalances', () => {
|
||||||
|
test('sums quantity times buy and sell across stacks', () => {
|
||||||
|
const result = appraiseItemBalances(
|
||||||
|
[{typeId: 1, quantity: 2}, {typeId: 2, quantity: 3}],
|
||||||
|
[price(1, 10, 12), price(2, 100, 110)],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({buy: 2 * 10 + 3 * 100, sell: 2 * 12 + 3 * 110});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores stacks with no resolved price', () => {
|
||||||
|
const result = appraiseItemBalances(
|
||||||
|
[{typeId: 1, quantity: 2}, {typeId: 99, quantity: 5}],
|
||||||
|
[price(1, 10, 12)],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({buy: 20, sell: 24});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns zero totals for no items', () => {
|
||||||
|
expect(appraiseItemBalances([], [])).toEqual({buy: 0, sell: 0});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {ItemBalanceResponse} from "@/generated/mammon";
|
||||||
|
import {MarketTypePrice} from "@/market";
|
||||||
|
|
||||||
|
export interface ItemBalanceAppraisal {
|
||||||
|
buy: number;
|
||||||
|
sell: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appraiseItemBalances = (
|
||||||
|
itemBalances: ItemBalanceResponse[],
|
||||||
|
prices: MarketTypePrice[],
|
||||||
|
): ItemBalanceAppraisal => {
|
||||||
|
const priceByTypeId = new Map(prices.map(p => [p.type.id, p]));
|
||||||
|
|
||||||
|
return itemBalances.reduce<ItemBalanceAppraisal>((totals, {typeId, quantity}) => {
|
||||||
|
const price = priceByTypeId.get(typeId);
|
||||||
|
|
||||||
|
if (price) {
|
||||||
|
totals.buy += quantity * price.buy;
|
||||||
|
totals.sell += quantity * price.sell;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totals;
|
||||||
|
}, {buy: 0, sell: 0});
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './ledger';
|
export * from './ledger';
|
||||||
|
export * from './balanceAppraisal';
|
||||||
|
|
||||||
export {default as LedgerLabel} from './LedgerLabel.vue';
|
export {default as LedgerLabel} from './LedgerLabel.vue';
|
||||||
export {default as LedgerSelect} from './LedgerSelect.vue';
|
export {default as LedgerSelect} from './LedgerSelect.vue';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {defineStore} from "pinia";
|
|||||||
import {computed, ref, triggerRef} from "vue";
|
import {computed, ref, triggerRef} from "vue";
|
||||||
import {ledgerApi, transactionApi} from "@/mammon";
|
import {ledgerApi, transactionApi} from "@/mammon";
|
||||||
import {useRouteParams} from "@vueuse/router";
|
import {useRouteParams} from "@vueuse/router";
|
||||||
|
import {onActivitiesProcessed} from "@/activity";
|
||||||
|
|
||||||
export const LedgerTypes = {
|
export const LedgerTypes = {
|
||||||
Main: 'MAIN',
|
Main: 'MAIN',
|
||||||
@@ -71,7 +72,9 @@ export const useLedgersStore = defineStore('ledgers', () => {
|
|||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
return {ledgers, findById, findAllById, createMain, createCombined, updateMain, updateCombined, refresh};
|
onActivitiesProcessed(refresh);
|
||||||
|
|
||||||
|
return {ledgers, findById, findAllById, createMain, createCombined, updateMain, updateCombined};
|
||||||
})
|
})
|
||||||
|
|
||||||
const getLedgerId = (ledger: Ledger | string): string => typeof ledger == 'string' ? ledger : ledger.ledgerId;
|
const getLedgerId = (ledger: Ledger | string): string => typeof ledger == 'string' ? ledger : ledger.ledgerId;
|
||||||
|
|||||||
+19
-7
@@ -1,9 +1,10 @@
|
|||||||
import { createPinia } from 'pinia';
|
import {createPinia} from 'pinia';
|
||||||
import { createApp } from 'vue';
|
import {createApp} from 'vue';
|
||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import {createRouter, createWebHistory} from 'vue-router';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { initLogger } from './logger';
|
import {useAuthStore} from './auth';
|
||||||
import { routes } from './routes';
|
import {initLogger} from './logger';
|
||||||
|
import {routeNames, routes} from './routes';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
initLogger();
|
initLogger();
|
||||||
@@ -16,6 +17,17 @@ const router = createRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
app.use(router);
|
|
||||||
|
|
||||||
app.mount('#app');
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
await authStore.bootstrap();
|
||||||
|
|
||||||
|
router.beforeEach(to => {
|
||||||
|
if (!to.meta.public && !authStore.isAuthenticated) {
|
||||||
|
return {name: routeNames.home};
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(router);
|
||||||
|
app.mount('#app');
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
import {logResource} from "@/service";
|
import {logResource} from "@/service";
|
||||||
import axios from "axios";
|
import {getAccessToken, setAccessToken} from "@/auth/token";
|
||||||
|
import {toast} from "@/toast";
|
||||||
|
import axios, {InternalAxiosRequestConfig} from "axios";
|
||||||
import {
|
import {
|
||||||
AcquisitionApi,
|
AcquisitionApi,
|
||||||
ActivityApi,
|
ActivityApi,
|
||||||
|
AuthApi,
|
||||||
CharacterApi,
|
CharacterApi,
|
||||||
CharacterRuleBookApi,
|
CorporationApi,
|
||||||
LedgerApi,
|
LedgerApi,
|
||||||
|
LocationApi,
|
||||||
MarketApi,
|
MarketApi,
|
||||||
ProcessingApi,
|
MeResponse,
|
||||||
|
ReprocessingApi,
|
||||||
RuleBookApi,
|
RuleBookApi,
|
||||||
|
RuleScriptApi,
|
||||||
TransactionApi
|
TransactionApi
|
||||||
} from "@/generated/mammon";
|
} from "@/generated/mammon";
|
||||||
|
|
||||||
export const mammonUrl = import.meta.env.VITE_MAMMON_URL;
|
export const mammonUrl = import.meta.env.VITE_MAMMON_URL;
|
||||||
export const mammonAddCharacterUrl = mammonUrl + "oauth2/authorization/esi"
|
export const mammonLoginUrl = mammonUrl + "oauth2/authorization/esi"
|
||||||
|
|
||||||
const mammonAxiosInstance = axios.create({
|
const mammonAxiosInstance = axios.create({
|
||||||
baseURL: mammonUrl,
|
baseURL: mammonUrl,
|
||||||
@@ -24,12 +30,90 @@ const mammonAxiosInstance = axios.create({
|
|||||||
})
|
})
|
||||||
logResource(mammonAxiosInstance)
|
logResource(mammonAxiosInstance)
|
||||||
|
|
||||||
|
const credentialedClient = axios.create({
|
||||||
|
baseURL: mammonUrl,
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
logResource(credentialedClient)
|
||||||
|
|
||||||
|
mammonAxiosInstance.interceptors.request.use(config => {
|
||||||
|
const token = getAccessToken();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
})
|
||||||
|
|
||||||
|
const authApi = new AuthApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
|
const credentialedAuthApi = new AuthApi(undefined, mammonUrl, credentialedClient);
|
||||||
|
|
||||||
|
let onAuthExpired: () => void = () => {};
|
||||||
|
|
||||||
|
export const setOnAuthExpired = (callback: () => void): void => {
|
||||||
|
onAuthExpired = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshing: Promise<string | null> | null = null;
|
||||||
|
|
||||||
|
export const refreshAccessToken = (): Promise<string | null> => {
|
||||||
|
if (!refreshing) {
|
||||||
|
refreshing = credentialedAuthApi.refresh()
|
||||||
|
.then(response => {
|
||||||
|
setAccessToken(response.data.accessToken);
|
||||||
|
return response.data.accessToken;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setAccessToken(null);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refreshing = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return refreshing;
|
||||||
|
}
|
||||||
|
|
||||||
|
mammonAxiosInstance.interceptors.response.use(response => response, async error => {
|
||||||
|
const original = error.config as (InternalAxiosRequestConfig & { _retried?: boolean }) | undefined;
|
||||||
|
|
||||||
|
if (error.response?.status === 401 && original && !original._retried) {
|
||||||
|
original._retried = true;
|
||||||
|
|
||||||
|
const token = await refreshAccessToken();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
original.headers.Authorization = `Bearer ${token}`;
|
||||||
|
return mammonAxiosInstance(original);
|
||||||
|
}
|
||||||
|
onAuthExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = error.response?.status;
|
||||||
|
|
||||||
|
if (status !== 401 && status !== 403) {
|
||||||
|
toast.error('Something went wrong');
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchMe = (): Promise<MeResponse> =>
|
||||||
|
authApi.me().then(response => response.data);
|
||||||
|
|
||||||
|
export const addCharacter = (): Promise<void> =>
|
||||||
|
authApi.addCharacter({withCredentials: true}).then(() => undefined);
|
||||||
|
|
||||||
|
export const postLogout = (): Promise<void> =>
|
||||||
|
credentialedAuthApi.logout().then(() => undefined);
|
||||||
|
|
||||||
export const ledgerApi = new LedgerApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const ledgerApi = new LedgerApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const transactionApi = new TransactionApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const transactionApi = new TransactionApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const characterApi = new CharacterApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const characterApi = new CharacterApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
|
export const corporationApi = new CorporationApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const ruleBookApi = new RuleBookApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const ruleBookApi = new RuleBookApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const characterRuleBookApi = new CharacterRuleBookApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const ruleScriptApi = new RuleScriptApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const activityApi = new ActivityApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const activityApi = new ActivityApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const processingApi = new ProcessingApi(undefined, mammonUrl, mammonAxiosInstance);
|
|
||||||
export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance);
|
export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
|
export const reprocessingApi = new ReprocessingApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
|
export const locationApi = new LocationApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { describe, expect, test } from 'vitest'
|
|
||||||
import { RegionalMarketCache } from './RegionalMarketCache'
|
|
||||||
|
|
||||||
describe('RegionalMarketCache', () => {
|
|
||||||
test('should cache and retrieve values', async () => {
|
|
||||||
const cache = new RegionalMarketCache<string>(1000)
|
|
||||||
|
|
||||||
cache.set(1, 1, 'test')
|
|
||||||
expect(cache.get(1, 1)).toBe('test')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should remove values', async () => {
|
|
||||||
const cache = new RegionalMarketCache<string>(1000)
|
|
||||||
|
|
||||||
cache.set(1, 1, 'test')
|
|
||||||
cache.remove(1, 1)
|
|
||||||
expect(cache.get(1, 1)).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should compute values if absent', async () => {
|
|
||||||
const cache = new RegionalMarketCache<string>(1000)
|
|
||||||
const value = await cache.computeIfAbsent(1, 1, () => Promise.resolve('test'))
|
|
||||||
|
|
||||||
expect(value).toBe('test')
|
|
||||||
expect(cache.get(1, 1)).toBe('test')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should expire values', async () => {
|
|
||||||
const cache = new RegionalMarketCache<string>(1)
|
|
||||||
|
|
||||||
cache.set(1, 1, 'test')
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 10))
|
|
||||||
expect(cache.get(1, 1)).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
class CacheEntry<T> {
|
|
||||||
public value: T;
|
|
||||||
public expiration: Date;
|
|
||||||
|
|
||||||
constructor(value: T, expiration: Date) {
|
|
||||||
this.value = value;
|
|
||||||
this.expiration = expiration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExpirationSupplier<T> = (v: T) => Date;
|
|
||||||
|
|
||||||
export class RegionalMarketCache<T> {
|
|
||||||
private cache: Record<number, Record<number, CacheEntry<T>>>;
|
|
||||||
private expirationSupplier: (v: T) => Date;
|
|
||||||
|
|
||||||
constructor(expiration: ExpirationSupplier<T> | number) {
|
|
||||||
this.cache = {};
|
|
||||||
this.expirationSupplier = expiration instanceof Function ? expiration : () => new Date(Date.now() + expiration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get(regionId: number, typeId: number): T | undefined {
|
|
||||||
const entry = this.cache[regionId]?.[typeId];
|
|
||||||
|
|
||||||
if (entry && entry.expiration > new Date()) {
|
|
||||||
return entry.value;
|
|
||||||
}
|
|
||||||
this.remove(regionId, typeId);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set(regionId: number, typeId: number, value: T): void {
|
|
||||||
this.cache[regionId] = this.cache[regionId] ?? {};
|
|
||||||
this.cache[regionId][typeId] = new CacheEntry(value, this.expirationSupplier(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public remove(regionId: number, typeId: number): void {
|
|
||||||
delete this.cache[regionId]?.[typeId];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async computeIfAbsent(regionId: number, typeId: number, supplier: () => (Promise<T> | T)): Promise<T> {
|
|
||||||
let value = this.get(regionId, typeId);
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
value = await supplier();
|
|
||||||
this.set(regionId, typeId, value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -7,4 +7,7 @@ export type AcquiredType = Omit<RawAcquiredType, 'type'> & {
|
|||||||
sell: number
|
sell: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const acquiredTypesToSorted = <T extends {date: Date} = AcquiredType>(array: T[], reverse?: boolean) => array.toSorted((a, b) => reverse ? b.date.getTime() - a.date.getTime() : a.date.getTime() - b.date.getTime())
|
export const acquiredTypesToSorted = <T extends {date: Date} = AcquiredType>(array: T[], reverse?: boolean) => array.toSorted((a, b) => reverse ? b.date.getTime() - a.date.getTime() : a.date.getTime() - b.date.getTime())
|
||||||
|
|
||||||
|
export const toEveDatetimeInput = (date: Date) => date.toISOString().slice(0, 16)
|
||||||
|
export const fromEveDatetimeInput = (value: string) => new Date(`${value}Z`)
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {LoadingSpinner, Tooltip} from '@/components';
|
|
||||||
import {formatIsk} from '@/formaters';
|
|
||||||
import {getHistory, getHistoryQuartils} from '@/market';
|
|
||||||
import {ArrowTrendingDownIcon, ArrowTrendingUpIcon} from '@heroicons/vue/24/outline';
|
|
||||||
import {computedAsync} from '@vueuse/core';
|
|
||||||
import {ref, watchEffect} from 'vue';
|
|
||||||
|
|
||||||
const trendingScale = 3;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id: number;
|
|
||||||
buy: number;
|
|
||||||
sell: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const open = ref(false);
|
|
||||||
|
|
||||||
const q1 = ref(0);
|
|
||||||
const median = ref(0);
|
|
||||||
const q3 = ref(0);
|
|
||||||
const lineColor = ref('');
|
|
||||||
const history = computedAsync(() => getHistory(props.id), []);
|
|
||||||
|
|
||||||
watchEffect(async () => {
|
|
||||||
if (!open.value || !props.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const quartils = getHistoryQuartils(history.value);
|
|
||||||
|
|
||||||
q1.value = quartils.q1;
|
|
||||||
median.value = quartils.median;
|
|
||||||
q3.value = quartils.q3;
|
|
||||||
|
|
||||||
if (props.buy >= quartils.q3) {
|
|
||||||
lineColor.value = 'line-blue';
|
|
||||||
} else if (props.sell >= quartils.q3) {
|
|
||||||
lineColor.value = 'line-green';
|
|
||||||
} else {
|
|
||||||
lineColor.value = '';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Tooltip v-model:open="open" class="tooltip">
|
|
||||||
<template #header>
|
|
||||||
<LoadingSpinner v-if="history.length < trendingScale" />
|
|
||||||
<ArrowTrendingUpIcon v-else-if="history[0].average > history[trendingScale - 1].average" />
|
|
||||||
<ArrowTrendingDownIcon v-else />
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<div class="bg-slate-500 -left-1/2 relative tooltip-content" v-if="history.length > 0">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Q1</th>
|
|
||||||
<th>Median</th>
|
|
||||||
<th>Q3</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr :class="lineColor">
|
|
||||||
<td class="text-right text-nowrap">{{ formatIsk(q1) }}</td>
|
|
||||||
<td class="text-right text-nowrap">{{ formatIsk(median) }}</td>
|
|
||||||
<td class="text-right text-nowrap">{{ formatIsk(q3) }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Tooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@reference "@/style.css";
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
@apply ms-auto;
|
|
||||||
>:deep(div.header) {
|
|
||||||
@apply btn-icon px-2;
|
|
||||||
}
|
|
||||||
&.open {
|
|
||||||
&.tooltip-top>:deep(div.header) {
|
|
||||||
@apply rounded-t-md bg-slate-600;
|
|
||||||
}
|
|
||||||
&.tooltip-bottom {
|
|
||||||
.tooltip-content {
|
|
||||||
bottom: 79px;
|
|
||||||
}
|
|
||||||
>:deep(div.header) {
|
|
||||||
@apply rounded-b-md bg-slate-600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {LoadingSpinner, Tooltip} from '@/components';
|
||||||
|
import {formatIsk} from '@/formaters';
|
||||||
|
import {getQuartiles, type HistoryQuartiles, MarketTrendIcon} from '@/market';
|
||||||
|
import {computedAsync} from '@vueuse/core';
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: number;
|
||||||
|
buy: number;
|
||||||
|
sell: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const quartiles = computedAsync<HistoryQuartiles | null>(
|
||||||
|
() => props.id ? getQuartiles(props.id) : null,
|
||||||
|
null,
|
||||||
|
loading,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineColor = computed(() => {
|
||||||
|
if (!quartiles.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (props.buy >= quartiles.value.q3) {
|
||||||
|
return 'line-blue';
|
||||||
|
}
|
||||||
|
if (props.sell >= quartiles.value.q3) {
|
||||||
|
return 'line-green';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Tooltip v-model:open="open" class="tooltip">
|
||||||
|
<template #header>
|
||||||
|
<LoadingSpinner v-if="loading || !quartiles" />
|
||||||
|
<MarketTrendIcon v-else :trend="quartiles.trend" />
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div class="bg-slate-500 tooltip-content" v-if="quartiles">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Q1</th>
|
||||||
|
<th>Median</th>
|
||||||
|
<th>Q3</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr :class="lineColor">
|
||||||
|
<td class="text-right text-nowrap">{{ formatIsk(quartiles.q1) }}</td>
|
||||||
|
<td class="text-right text-nowrap">{{ formatIsk(quartiles.median) }}</td>
|
||||||
|
<td class="text-right text-nowrap">{{ formatIsk(quartiles.q3) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/style.css";
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
@apply ms-auto;
|
||||||
|
&.open {
|
||||||
|
&.tooltip-top>:deep(div.header) {
|
||||||
|
@apply rounded-t-md bg-slate-600;
|
||||||
|
}
|
||||||
|
&.tooltip-bottom>:deep(div.header) {
|
||||||
|
@apply rounded-b-md bg-slate-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tooltip>:deep(div.header) {
|
||||||
|
@apply btn-icon px-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {SliderCheckbox} from '@/components';
|
||||||
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||||
import {MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore} from "@/market";
|
import {getListedSellTypeIds, MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore} from "@/market";
|
||||||
import {MinusIcon, PlusIcon} from '@heroicons/vue/24/outline';
|
import {MinusIcon, PlusIcon} from '@heroicons/vue/24/outline';
|
||||||
import {useStorage} from '@vueuse/core';
|
import {useStorage} from '@vueuse/core';
|
||||||
import {computed, ref} from 'vue';
|
import {computed, ref, watch} from 'vue';
|
||||||
import {AcquiredType} from './AcquiredType';
|
import {AcquiredType} from './AcquiredType';
|
||||||
import AcquisitionQuantilsTooltip from './AcquisitionQuantilsTooltip.vue';
|
import AcquisitionQuartilesTooltip from './AcquisitionQuartilesTooltip.vue';
|
||||||
import {formatEveDate, formatIsk, percentFormater} from "@/formaters.ts";
|
import {formatEveDate, formatIsk, percentFormater} from "@/formaters.ts";
|
||||||
|
import {Ledger, LedgerLabel, useLedgersStore} from "@/ledger";
|
||||||
|
|
||||||
type Result = {
|
type Result = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -20,6 +22,8 @@ type Result = {
|
|||||||
precentProfit: number;
|
precentProfit: number;
|
||||||
iskProfit: number;
|
iskProfit: number;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
ledger?: Ledger;
|
||||||
|
ledgerName: string;
|
||||||
acquisitions: AcquiredType[];
|
acquisitions: AcquiredType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +31,7 @@ interface Props {
|
|||||||
items?: AcquiredType[];
|
items?: AcquiredType[];
|
||||||
infoOnly?: boolean;
|
infoOnly?: boolean;
|
||||||
showAll?: boolean;
|
showAll?: boolean;
|
||||||
ignoredColums?: string[] | string;
|
ignoredColumns?: string[] | string;
|
||||||
defaultSortKey?: string;
|
defaultSortKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,26 +44,42 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
items: () => [],
|
items: () => [],
|
||||||
infoOnly: false,
|
infoOnly: false,
|
||||||
showAll: false,
|
showAll: false,
|
||||||
ignoredColums: () => [],
|
ignoredColumns: () => [],
|
||||||
defaultSortKey: 'precentProfit',
|
defaultSortKey: 'precentProfit',
|
||||||
});
|
});
|
||||||
defineEmits<Emits>();
|
defineEmits<Emits>();
|
||||||
|
|
||||||
const columnsToIgnore = computed(() => {
|
const columnsToIgnore = computed(() => {
|
||||||
const ic = typeof props.ignoredColums === 'string' ? [props.ignoredColums] : props.ignoredColums;
|
const ignoredColumns = typeof props.ignoredColumns === 'string' ? [props.ignoredColumns] : [...props.ignoredColumns];
|
||||||
|
|
||||||
if (props.infoOnly && !ic.includes('buttons')) {
|
if (props.infoOnly && !ignoredColumns.includes('buttons')) {
|
||||||
return [...ic, 'buttons'];
|
ignoredColumns.push('buttons');
|
||||||
}
|
}
|
||||||
return ic;
|
if (!props.showAll && !ignoredColumns.includes('ledger')) {
|
||||||
|
ignoredColumns.push('ledger');
|
||||||
|
}
|
||||||
|
if (ignoredColumns.includes('ledger')) {
|
||||||
|
ignoredColumns.push('ledgerName');
|
||||||
|
}
|
||||||
|
return ignoredColumns;
|
||||||
});
|
});
|
||||||
|
|
||||||
const marketTaxStore = useMarketTaxStore();
|
const marketTaxStore = useMarketTaxStore();
|
||||||
|
const ledgersStore = useLedgersStore();
|
||||||
|
|
||||||
|
const listedTypeIds = ref(new Set<number>());
|
||||||
|
watch(() => props.items, async () => {
|
||||||
|
listedTypeIds.value = await getListedSellTypeIds();
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const threshold = useStorage('market-acquisition-threshold', 10);
|
const threshold = useStorage('market-acquisition-threshold', 10);
|
||||||
const filter = ref("");
|
const filter = ref("");
|
||||||
const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() => {
|
const unlistedOnly = ref(false);
|
||||||
const filteredItems = props.items.filter(r => r.type.name.toLowerCase().includes(filter.value.toLowerCase()));
|
const matchesFilter = (r: AcquiredType) =>
|
||||||
|
r.type.name.toLowerCase().includes(filter.value.toLowerCase())
|
||||||
|
&& (!unlistedOnly.value || !listedTypeIds.value.has(r.type.id));
|
||||||
|
const results = computed<Result[]>(() => {
|
||||||
|
const filteredItems = props.items.filter(matchesFilter);
|
||||||
|
|
||||||
if (props.showAll) {
|
if (props.showAll) {
|
||||||
return filteredItems.map(r => {
|
return filteredItems.map(r => {
|
||||||
@@ -77,6 +97,8 @@ const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() =>
|
|||||||
precentProfit,
|
precentProfit,
|
||||||
iskProfit: r.price * precentProfit * r.remaining,
|
iskProfit: r.price * precentProfit * r.remaining,
|
||||||
date: r.date,
|
date: r.date,
|
||||||
|
ledger: ledgersStore.findById(r.ledgerId),
|
||||||
|
ledgerName: ledgersStore.findById(r.ledgerId)?.name ?? '',
|
||||||
acquisitions: [r]
|
acquisitions: [r]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -109,14 +131,16 @@ const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() =>
|
|||||||
precentProfit,
|
precentProfit,
|
||||||
iskProfit: price * precentProfit * totalRemaining,
|
iskProfit: price * precentProfit * totalRemaining,
|
||||||
date: first.date,
|
date: first.date,
|
||||||
|
ledgerName: '',
|
||||||
acquisitions: group
|
acquisitions: group
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return list;
|
return list;
|
||||||
}), {
|
});
|
||||||
|
const { sortedArray, headerProps, showColumn } = useSort<Result>(results, {
|
||||||
defaultSortKey: props.defaultSortKey,
|
defaultSortKey: props.defaultSortKey,
|
||||||
defaultSortDirection: 'desc',
|
defaultSortDirection: 'desc',
|
||||||
ignoredColums: columnsToIgnore
|
ignoredColumns: columnsToIgnore
|
||||||
})
|
})
|
||||||
const getLineColor = (result: Result) => {
|
const getLineColor = (result: Result) => {
|
||||||
if (result.precentProfit >= (threshold.value / 100)) {
|
if (result.precentProfit >= (threshold.value / 100)) {
|
||||||
@@ -159,6 +183,9 @@ const total = computed(() => {
|
|||||||
<div class="flex" v-if="!infoOnly">
|
<div class="flex" v-if="!infoOnly">
|
||||||
<div class="flex justify-self-end mb-2 mt-4 ms-auto">
|
<div class="flex justify-self-end mb-2 mt-4 ms-auto">
|
||||||
<TaxInput />
|
<TaxInput />
|
||||||
|
<div class="end flex items-center" title="Show only acquisitions with no open sell order">
|
||||||
|
<SliderCheckbox class="me-1" v-model="unlistedOnly" /> Unlisted only
|
||||||
|
</div>
|
||||||
<div class="end">
|
<div class="end">
|
||||||
<span>Profit Threshold: </span>
|
<span>Profit Threshold: </span>
|
||||||
<input type="number" min="0" max="1000" step="1" v-model="threshold" />
|
<input type="number" min="0" max="1000" step="1" v-model="threshold" />
|
||||||
@@ -176,6 +203,7 @@ const total = computed(() => {
|
|||||||
<SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="buy">Buy</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="buy">Buy</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="sell">Sell</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="sell">Sell</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="ledgerName">Ledger</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="date">Bought at</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="date">Bought at</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="price">Bought Price</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="price">Bought Price</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="remaining">Remaining Amount</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="remaining">Remaining Amount</SortableHeader>
|
||||||
@@ -189,11 +217,14 @@ const total = computed(() => {
|
|||||||
<td v-if="showColumn('name')">
|
<td v-if="showColumn('name')">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<MarketTypeLabel :id="r.data.type.id" :name="r.data.name" />
|
<MarketTypeLabel :id="r.data.type.id" :name="r.data.name" />
|
||||||
<AcquisitionQuantilsTooltip :id="r.data.type.id" :buy="r.data.buy" :sell="r.data.sell" />
|
<AcquisitionQuartilesTooltip :id="r.data.type.id" :buy="r.data.buy" :sell="r.data.sell" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="showColumn('buy')" class="text-right">{{ formatIsk(r.data.buy) }}</td>
|
<td v-if="showColumn('buy')" class="text-right">{{ formatIsk(r.data.buy) }}</td>
|
||||||
<td v-if="showColumn('sell')" class="text-right">{{ formatIsk(r.data.sell) }}</td>
|
<td v-if="showColumn('sell')" class="text-right">{{ formatIsk(r.data.sell) }}</td>
|
||||||
|
<td v-if="showColumn('ledger')">
|
||||||
|
<LedgerLabel v-if="r.data.ledger" :ledger="r.data.ledger" link />
|
||||||
|
</td>
|
||||||
<td v-if="showColumn('date')" class="text-right">{{ formatEveDate(r.data.date) }}</td>
|
<td v-if="showColumn('date')" class="text-right">{{ formatEveDate(r.data.date) }}</td>
|
||||||
<td v-if="showColumn('price')" class="text-right">{{ formatIsk(r.data.price) }}</td>
|
<td v-if="showColumn('price')" class="text-right">{{ formatIsk(r.data.price) }}</td>
|
||||||
<td v-if="showColumn('remaining')" class="text-right">{{ r.data.remaining }}/{{ r.data.quantity }}</td>
|
<td v-if="showColumn('remaining')" class="text-right">{{ r.data.remaining }}/{{ r.data.quantity }}</td>
|
||||||
@@ -214,9 +245,12 @@ const total = computed(() => {
|
|||||||
<td v-if="showColumn('sell')">
|
<td v-if="showColumn('sell')">
|
||||||
<template v-if="!showColumn('name') && !showColumn('buy')">Total</template>
|
<template v-if="!showColumn('name') && !showColumn('buy')">Total</template>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="showColumn('date')">
|
<td v-if="showColumn('ledger')">
|
||||||
<template v-if="!showColumn('name') && !showColumn('buy') && !showColumn('sell')">Total</template>
|
<template v-if="!showColumn('name') && !showColumn('buy') && !showColumn('sell')">Total</template>
|
||||||
</td>
|
</td>
|
||||||
|
<td v-if="showColumn('date')">
|
||||||
|
<template v-if="!showColumn('name') && !showColumn('buy') && !showColumn('sell') && !showColumn('ledger')">Total</template>
|
||||||
|
</td>
|
||||||
<td v-if="showColumn('price')" class="text-right">
|
<td v-if="showColumn('price')" class="text-right">
|
||||||
<template v-if="total.sameItem">
|
<template v-if="total.sameItem">
|
||||||
{{ formatIsk(total.price) }}
|
{{ formatIsk(total.price) }}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {getMarketTypes, getPrices, MarketTypePrice} from "@/market";
|
||||||
|
import {ref, watch} from 'vue';
|
||||||
|
import {ArrowPathIcon} from '@heroicons/vue/24/outline';
|
||||||
|
import {useAutoRefresh} from '@/composables';
|
||||||
|
import {MarketLocationInput, useMarketLocationStore} from '@/market/location';
|
||||||
|
import {AcquiredType} from './AcquiredType';
|
||||||
|
import {RawAcquiredType} from './acquisition';
|
||||||
|
import AcquisitionResultTable from './AcquisitionResultTable.vue';
|
||||||
|
import BuyModal from './BuyModal.vue';
|
||||||
|
import SellModal from './SellModal.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: RawAcquiredType[];
|
||||||
|
ignoredColumns?: string[] | string;
|
||||||
|
ledgerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const buyModal = ref<typeof BuyModal>();
|
||||||
|
const sellModal = ref<typeof SellModal>();
|
||||||
|
|
||||||
|
const marketLocationStore = useMarketLocationStore();
|
||||||
|
const enriched = ref<AcquiredType[]>([]);
|
||||||
|
|
||||||
|
const refresh = async (itms: RawAcquiredType[] = props.items) => {
|
||||||
|
if (itms.length === 0) {
|
||||||
|
enriched.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = await getMarketTypes([...new Set(itms.map(i => i.type))]);
|
||||||
|
const prices = await getPrices(types, marketLocationStore.location.id);
|
||||||
|
|
||||||
|
enriched.value = itms.map(i => {
|
||||||
|
const price = prices.find(p => p.type.id === i.type) as MarketTypePrice;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...i,
|
||||||
|
type: price.type,
|
||||||
|
buy: price.buy,
|
||||||
|
sell: price.sell
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch([() => props.items, () => marketLocationStore.location], ([itms]) => refresh(itms), {immediate: true});
|
||||||
|
|
||||||
|
const {active: autoRefresh, toggle: toggleAutoRefresh} = useAutoRefresh(() => refresh(), 5 * 60 * 1000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="ms-auto flex items-center gap-2">
|
||||||
|
<MarketLocationInput v-model="marketLocationStore.location" />
|
||||||
|
<div class="flex">
|
||||||
|
<button class="rounded-e-none" @click="refresh()">Refresh</button>
|
||||||
|
<button class="rounded-s-none border-s-0 flex items-center" :class="{ 'bg-slate-700': autoRefresh }" title="Auto refresh" @click="toggleAutoRefresh">
|
||||||
|
<ArrowPathIcon class="w-5 h-5" :class="{ 'animate-spin': autoRefresh }" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="enriched.length > 0">
|
||||||
|
<AcquisitionResultTable :items="enriched" :ignoredColumns="ignoredColumns" @buy="(types, price, buy, sell) => buyModal?.open(types[0].type, { 'Price': price, 'Buy': buy, 'Sell': sell }, props.ledgerId)" @sell="types => sellModal?.open(types, props.ledgerId)" />
|
||||||
|
<BuyModal ref="buyModal" />
|
||||||
|
<SellModal ref="sellModal" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -2,21 +2,35 @@
|
|||||||
import {Modal} from '@/components';
|
import {Modal} from '@/components';
|
||||||
import {formatIsk} from '@/formaters';
|
import {formatIsk} from '@/formaters';
|
||||||
import {MarketType, MarketTypeLabel} from '@/market';
|
import {MarketType, MarketTypeLabel} from '@/market';
|
||||||
import {ref} from 'vue';
|
import {Ledger, LedgerSelect, isMain, useLedgersStore} from '@/ledger';
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
import {fromEveDatetimeInput, toEveDatetimeInput} from './AcquiredType';
|
||||||
import {useAcquiredTypesStore} from './acquisition';
|
import {useAcquiredTypesStore} from './acquisition';
|
||||||
|
import {toast} from '@/toast';
|
||||||
|
|
||||||
const acquiredTypesStore = useAcquiredTypesStore();
|
const acquiredTypesStore = useAcquiredTypesStore();
|
||||||
|
const ledgersStore = useLedgersStore();
|
||||||
|
|
||||||
|
const mainLedgers = computed(() => ledgersStore.ledgers.filter(isMain));
|
||||||
|
|
||||||
const modalOpen = ref<boolean>(false);
|
const modalOpen = ref<boolean>(false);
|
||||||
const type = ref<MarketType>();
|
const type = ref<MarketType>();
|
||||||
const suggestions = ref<Record<string, number>>({});
|
const suggestions = ref<Record<string, number>>({});
|
||||||
const price = ref(1000000);
|
const price = ref(1000000);
|
||||||
const count = ref(1);
|
const count = ref(1);
|
||||||
|
const date = ref('');
|
||||||
|
const contextLedgerId = ref<string>();
|
||||||
|
const ledger = ref<Ledger>();
|
||||||
|
|
||||||
const open = (t: MarketType, s?: Record<string, number> | number) => {
|
const ledgerId = computed(() => contextLedgerId.value ?? ledger.value?.ledgerId);
|
||||||
|
|
||||||
|
const open = (t: MarketType, s?: Record<string, number> | number, ledgerId?: string) => {
|
||||||
type.value = t;
|
type.value = t;
|
||||||
count.value = 1;
|
count.value = 1;
|
||||||
|
date.value = toEveDatetimeInput(new Date());
|
||||||
|
contextLedgerId.value = ledgerId;
|
||||||
|
ledger.value = undefined;
|
||||||
|
|
||||||
if (typeof s === 'number') {
|
if (typeof s === 'number') {
|
||||||
suggestions.value = {};
|
suggestions.value = {};
|
||||||
price.value = s;
|
price.value = s;
|
||||||
@@ -29,7 +43,7 @@ const open = (t: MarketType, s?: Record<string, number> | number) => {
|
|||||||
}
|
}
|
||||||
modalOpen.value = true;
|
modalOpen.value = true;
|
||||||
}
|
}
|
||||||
const add = () => {
|
const add = async () => {
|
||||||
const id = type.value?.id;
|
const id = type.value?.id;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -37,8 +51,15 @@ const add = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
acquiredTypesStore.addAcquiredType(id, count.value, price.value);
|
const lid = ledgerId.value;
|
||||||
|
|
||||||
|
if (!lid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await acquiredTypesStore.addAcquiredType(id, count.value, price.value, lid, fromEveDatetimeInput(date.value));
|
||||||
modalOpen.value = false;
|
modalOpen.value = false;
|
||||||
|
toast.success('Acquisition added');
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ open });
|
defineExpose({ open });
|
||||||
@@ -61,7 +82,15 @@ defineExpose({ open });
|
|||||||
<span>Count: </span>
|
<span>Count: </span>
|
||||||
<input class="ms-2" type="number" min="0" step="1" v-model="count" @keyup.enter="add" />
|
<input class="ms-2" type="number" min="0" step="1" v-model="count" @keyup.enter="add" />
|
||||||
</div>
|
</div>
|
||||||
<button class="mb-auto" @click="add">Add</button>
|
<div class="flex me-2 mb-auto">
|
||||||
|
<span>Eve Time: </span>
|
||||||
|
<input class="ms-2" type="datetime-local" v-model="date" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!contextLedgerId" class="flex me-2 mb-auto">
|
||||||
|
<span>Ledger: </span>
|
||||||
|
<LedgerSelect class="ms-2" v-model="ledger" :ledgers="mainLedgers" />
|
||||||
|
</div>
|
||||||
|
<button class="mb-auto" :disabled="!ledgerId" @click="add">Add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Modal } from '@/components';
|
import { Modal } from '@/components';
|
||||||
import { MarketType, MarketTypeLabel } from '@/market';
|
import { MarketType, MarketTypeLabel } from '@/market';
|
||||||
import { ref } from 'vue';
|
import { Ledger, LedgerSelect, isMain, useLedgersStore } from '@/ledger';
|
||||||
import { AcquiredType, acquiredTypesToSorted } from './AcquiredType';
|
import { computed, ref } from 'vue';
|
||||||
|
import { AcquiredType, acquiredTypesToSorted, fromEveDatetimeInput, toEveDatetimeInput } from './AcquiredType';
|
||||||
import { useAcquiredTypesStore } from './acquisition';
|
import { useAcquiredTypesStore } from './acquisition';
|
||||||
|
import { toast } from '@/toast';
|
||||||
|
|
||||||
|
|
||||||
const acquiredTypesStore = useAcquiredTypesStore();
|
const acquiredTypesStore = useAcquiredTypesStore();
|
||||||
|
const ledgersStore = useLedgersStore();
|
||||||
|
|
||||||
|
const mainLedgers = computed(() => ledgersStore.ledgers.filter(isMain));
|
||||||
|
|
||||||
const modalOpen = ref<boolean>(false);
|
const modalOpen = ref<boolean>(false);
|
||||||
const type = ref<MarketType>();
|
const type = ref<MarketType>();
|
||||||
const count = ref(1);
|
const count = ref(1);
|
||||||
|
const date = ref('');
|
||||||
const types = ref<AcquiredType[]>([]);
|
const types = ref<AcquiredType[]>([]);
|
||||||
|
const contextLedgerId = ref<string>();
|
||||||
|
const ledger = ref<Ledger>();
|
||||||
|
|
||||||
const open = (t: AcquiredType[]) => {
|
const ledgerId = computed(() => contextLedgerId.value ?? ledger.value?.ledgerId);
|
||||||
|
|
||||||
|
const open = (t: AcquiredType[], ledgerId?: string) => {
|
||||||
if (t.length === 0) {
|
if (t.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -21,26 +31,38 @@ const open = (t: AcquiredType[]) => {
|
|||||||
types.value = acquiredTypesToSorted(t);
|
types.value = acquiredTypesToSorted(t);
|
||||||
type.value = t[0].type;
|
type.value = t[0].type;
|
||||||
count.value = 1;
|
count.value = 1;
|
||||||
|
date.value = toEveDatetimeInput(new Date());
|
||||||
|
contextLedgerId.value = ledgerId;
|
||||||
|
ledger.value = undefined;
|
||||||
modalOpen.value = true;
|
modalOpen.value = true;
|
||||||
}
|
}
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
if (!types.value) {
|
if (types.value.length === 0) {
|
||||||
modalOpen.value = false;
|
modalOpen.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = count.value;
|
const lid = ledgerId.value;
|
||||||
|
|
||||||
for (const type of types.value) {
|
if (!lid) {
|
||||||
const remaining = type.remaining;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await acquiredTypesStore.removeAcquiredType(type.id, c);
|
const datetime = fromEveDatetimeInput(date.value);
|
||||||
c -= remaining;
|
let remaining = count.value;
|
||||||
if (c <= 0) {
|
|
||||||
|
for (const t of types.value) {
|
||||||
|
if (remaining <= 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quantity = Math.min(remaining, t.remaining);
|
||||||
|
|
||||||
|
await acquiredTypesStore.removeAcquiredType(t.id, quantity, lid, datetime);
|
||||||
|
remaining -= quantity;
|
||||||
}
|
}
|
||||||
modalOpen.value = false;
|
modalOpen.value = false;
|
||||||
|
toast.success('Consumption added');
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ open });
|
defineExpose({ open });
|
||||||
@@ -61,7 +83,15 @@ defineExpose({ open });
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="mb-auto" @click="remove">Remove</button>
|
<div class="flex me-2 mb-auto">
|
||||||
|
<span>Eve Time: </span>
|
||||||
|
<input class="ms-2" type="datetime-local" v-model="date" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!contextLedgerId" class="flex me-2 mb-auto">
|
||||||
|
<span>Ledger: </span>
|
||||||
|
<LedgerSelect class="ms-2" v-model="ledger" :ledgers="mainLedgers" />
|
||||||
|
</div>
|
||||||
|
<button class="mb-auto" :disabled="!ledgerId" @click="remove">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import {acquisitionApi} from "@/mammon";
|
import {acquisitionApi, activityApi} from "@/mammon";
|
||||||
import {AcquisitionResponse, AcquisitionResponseSourceEnum} from "@/generated/mammon";
|
import {AcquisitionResponse, ActivitySourceResponse} from "@/generated/mammon";
|
||||||
|
import {onActivitiesProcessed} from "@/activity";
|
||||||
export type AcquiredTypeSource = 'bo' | 'so' | 'prod' | 'misc';
|
|
||||||
|
|
||||||
export type RawAcquiredType = {
|
export type RawAcquiredType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
ledgerId: string;
|
||||||
type: number;
|
type: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
remaining: number;
|
remaining: number;
|
||||||
price: number;
|
price: number;
|
||||||
date: Date;
|
date: Date;
|
||||||
source: AcquisitionResponseSourceEnum;
|
source: ActivitySourceResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toAcquiredType = (a: AcquisitionResponse): RawAcquiredType => ({
|
export const toAcquiredType = (a: AcquisitionResponse): RawAcquiredType => ({
|
||||||
id: a.acquisitionId,
|
id: a.acquisitionId,
|
||||||
|
ledgerId: a.ledgerId,
|
||||||
type: a.marketTypeId,
|
type: a.marketTypeId,
|
||||||
quantity: a.quantity,
|
quantity: a.quantity,
|
||||||
remaining: a.remaining,
|
remaining: a.remaining,
|
||||||
@@ -30,14 +31,41 @@ export const useAcquiredTypesStore = defineStore('market-acquisition', () => {
|
|||||||
|
|
||||||
const types = computed(() => acquiredTypes.value.filter(item => item.remaining > 0));
|
const types = computed(() => acquiredTypes.value.filter(item => item.remaining > 0));
|
||||||
|
|
||||||
// Display-only: the backend exposes no write endpoint yet, so buy/sell are no-ops.
|
const addAcquiredType = (type: number, quantity: number, price: number, ledgerId: string, datetime: Date) =>
|
||||||
const addAcquiredType = async (_type: number, _quantity: number, _price: number, _source?: AcquiredTypeSource) => {};
|
activityApi.acquire({
|
||||||
const removeAcquiredType = async (_id: string, _quantity: number) => {};
|
source: { type: 'LEDGER', characterId: null, ledgerId },
|
||||||
|
marketTypeId: type,
|
||||||
|
quantity,
|
||||||
|
unitPrice: price,
|
||||||
|
datetime: datetime.toISOString(),
|
||||||
|
taxes: null,
|
||||||
|
description: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeAcquiredType = (id: string, quantity: number, ledgerId: string, datetime: Date) => {
|
||||||
|
const acquisition = acquiredTypes.value.find(a => a.id === id);
|
||||||
|
|
||||||
|
if (!acquisition) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return activityApi.consume({
|
||||||
|
source: { type: 'LEDGER', characterId: null, ledgerId },
|
||||||
|
marketTypeId: acquisition.type,
|
||||||
|
quantity,
|
||||||
|
unitPrice: 0,
|
||||||
|
datetime: datetime.toISOString(),
|
||||||
|
taxes: null,
|
||||||
|
description: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const refresh = () => acquisitionApi.findAllAcquisitions()
|
const refresh = () => acquisitionApi.findAllAcquisitions()
|
||||||
.then(response => acquiredTypes.value = response.data.map(toAcquiredType));
|
.then(response => acquiredTypes.value = response.data.map(toAcquiredType));
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
return { acquiredTypes: types, addAcquiredType, removeAcquiredType, refresh };
|
onActivitiesProcessed(refresh);
|
||||||
|
|
||||||
|
return { acquiredTypes: types, addAcquiredType, removeAcquiredType };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * from './AcquiredType';
|
|||||||
export * from './acquisition';
|
export * from './acquisition';
|
||||||
|
|
||||||
export { default as AcquisitionResultTable } from './AcquisitionResultTable.vue';
|
export { default as AcquisitionResultTable } from './AcquisitionResultTable.vue';
|
||||||
|
export { default as AcquisitionsPanel } from './AcquisitionsPanel.vue';
|
||||||
export { default as BuyModal } from './BuyModal.vue';
|
export { default as BuyModal } from './BuyModal.vue';
|
||||||
export { default as SellModal } from './SellModal.vue';
|
export { default as SellModal } from './SellModal.vue';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||||
|
import {MarketTypeLabel} from '@/market/type';
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import IskLabel from '../IskLabel.vue';
|
||||||
|
import {AppraisalItemValue} from './appraise';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
result?: AppraisalItemValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
result: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sortedArray, headerProps } = useSort(computed(() => props.result), {
|
||||||
|
defaultSortKey: 'sell',
|
||||||
|
defaultSortDirection: 'desc'
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalBuy = computed(() => props.result.reduce((sum, r) => sum + r.buy, 0));
|
||||||
|
const totalSell = computed(() => props.result.reduce((sum, r) => sum + r.sell, 0));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VirtualScrollTable :list="sortedArray" :itemHeight="33" :footerHeight="33" bottom="1rem">
|
||||||
|
<template #default="{ list }">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="quantity">Qty</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="buyUnit">Buy/unit</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="sellUnit">Sell/unit</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="buy">Buy total</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="sell">Sell total</SortableHeader>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in list" :key="r.data.typeID">
|
||||||
|
<td>
|
||||||
|
<MarketTypeLabel :id="r.data.typeID" :name="r.data.name" />
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ r.data.quantity.toLocaleString() }}</td>
|
||||||
|
<td class="text-right"><IskLabel :amount="r.data.buyUnit" :colored="false" /></td>
|
||||||
|
<td class="text-right"><IskLabel :amount="r.data.sellUnit" :colored="false" /></td>
|
||||||
|
<td class="text-right"><IskLabel :amount="r.data.buy" :colored="false" /></td>
|
||||||
|
<td class="text-right"><IskLabel :amount="r.data.sell" :colored="false" /></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="font-semibold">Total</td>
|
||||||
|
<td class="text-right"><IskLabel :amount="totalBuy" :colored="false" /></td>
|
||||||
|
<td class="text-right"><IskLabel :amount="totalSell" :colored="false" /></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<span>No items found</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VirtualScrollTable>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||||
|
import {MarketTypeLabel} from '@/market/type';
|
||||||
|
import {computed} from 'vue';
|
||||||
|
import {MarketLocation} from '../location';
|
||||||
|
import IskLabel from '../IskLabel.vue';
|
||||||
|
import {ComparedItem} from './compare';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
result?: ComparedItem[];
|
||||||
|
locations?: MarketLocation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
result: () => [],
|
||||||
|
locations: () => []
|
||||||
|
});
|
||||||
|
|
||||||
|
const buyKey = (id: number): `buy:${number}` => `buy:${id}`;
|
||||||
|
const sellKey = (id: number): `sell:${number}` => `sell:${id}`;
|
||||||
|
|
||||||
|
type SortableItem = ComparedItem & Record<`buy:${number}` | `sell:${number}`, number>;
|
||||||
|
|
||||||
|
const rows = computed<SortableItem[]>(() => props.result.map(item => {
|
||||||
|
const sortable = {...item} as SortableItem;
|
||||||
|
|
||||||
|
props.locations.forEach(l => {
|
||||||
|
const price = item.prices[l.id];
|
||||||
|
sortable[buyKey(l.id)] = price?.buy ?? 0;
|
||||||
|
sortable[sellKey(l.id)] = price?.sell ?? 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortable;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {sortedArray, headerProps} = useSort(rows, {
|
||||||
|
defaultSortKey: 'name',
|
||||||
|
defaultSortDirection: 'asc'
|
||||||
|
});
|
||||||
|
|
||||||
|
const bestBuy = (row: ComparedItem): number | undefined => {
|
||||||
|
let best: number | undefined;
|
||||||
|
let bestValue = -Infinity;
|
||||||
|
props.locations.forEach(l => {
|
||||||
|
const cell = row.prices[l.id];
|
||||||
|
if (cell && cell.buy > bestValue) {
|
||||||
|
bestValue = cell.buy;
|
||||||
|
best = l.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return bestValue > 0 ? best : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bestSell = (row: ComparedItem): number | undefined => {
|
||||||
|
let best: number | undefined;
|
||||||
|
let bestValue = -Infinity;
|
||||||
|
props.locations.forEach(l => {
|
||||||
|
const cell = row.prices[l.id];
|
||||||
|
if (cell && cell.sell > bestValue) {
|
||||||
|
bestValue = cell.sell;
|
||||||
|
best = l.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return bestValue > 0 ? best : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totals = computed(() => new Map(props.locations.map(l => [l.id, props.result.reduce((acc, row) => {
|
||||||
|
const cell = row.prices[l.id];
|
||||||
|
return {
|
||||||
|
buy: acc.buy + (cell?.buy ?? 0),
|
||||||
|
sell: acc.sell + (cell?.sell ?? 0),
|
||||||
|
};
|
||||||
|
}, {buy: 0, sell: 0})])));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VirtualScrollTable :list="sortedArray" :itemHeight="33" :headerHeight="66" :footerHeight="33" bottom="1rem">
|
||||||
|
<template #default="{ list }">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="name" rowspan="2">Item</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" sortKey="quantity" rowspan="2">Qty</SortableHeader>
|
||||||
|
<th v-for="location in locations" :key="location.id" colspan="2" class="text-center border-l border-slate-500" :title="location.name">
|
||||||
|
{{ location.systemName }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<template v-for="location in locations" :key="location.id">
|
||||||
|
<SortableHeader v-bind="headerProps" :sortKey="buyKey(location.id)" class="border-l border-slate-500">Buy</SortableHeader>
|
||||||
|
<SortableHeader v-bind="headerProps" :sortKey="sellKey(location.id)">Sell</SortableHeader>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in list" :key="r.data.typeId">
|
||||||
|
<td>
|
||||||
|
<MarketTypeLabel :id="r.data.typeId" :name="r.data.name" />
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ r.data.quantity.toLocaleString() }}</td>
|
||||||
|
<template v-for="location in locations" :key="location.id">
|
||||||
|
<td class="text-right border-l border-slate-500" :class="r.data.prices[location.id] ? (bestBuy(r.data) === location.id ? 'bg-emerald-500' : 'bg-amber-900') : ''">
|
||||||
|
<IskLabel v-if="r.data.prices[location.id]" :amount="r.data.prices[location.id].buy" :colored="false" />
|
||||||
|
<span v-else>-</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right" :class="r.data.prices[location.id] ? (bestSell(r.data) === location.id ? 'bg-emerald-500' : 'bg-amber-900') : ''">
|
||||||
|
<IskLabel v-if="r.data.prices[location.id]" :amount="r.data.prices[location.id].sell" :colored="false" />
|
||||||
|
<span v-else>-</span>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="font-semibold">Total</td>
|
||||||
|
<template v-for="location in locations" :key="location.id">
|
||||||
|
<td class="text-right border-l border-slate-500"><IskLabel :amount="totals.get(location.id)?.buy ?? 0" :colored="false" /></td>
|
||||||
|
<td class="text-right"><IskLabel :amount="totals.get(location.id)?.sell ?? 0" :colored="false" /></td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<span>No items found</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VirtualScrollTable>
|
||||||
|
</template>
|
||||||
@@ -8,4 +8,4 @@ export type MarketTypePrice = {
|
|||||||
orderCount: number;
|
orderCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PriceGetter = (types: MarketType[]) => Promise<MarketTypePrice[]>;
|
export type PriceGetter = (types: MarketType[], locationId?: number) => Promise<MarketTypePrice[]>;
|
||||||
|
|||||||
@@ -1,47 +1,17 @@
|
|||||||
import {defineStore} from 'pinia';
|
|
||||||
import {RegionalMarketCache} from '../RegionalMarketCache';
|
|
||||||
import {jitaId} from '../market';
|
|
||||||
import {MarketType} from "../type";
|
import {MarketType} from "../type";
|
||||||
import {MarketTypePrice} from './MarketTypePrice';
|
import {MarketTypePrice} from './MarketTypePrice';
|
||||||
import {getMammonPrices} from './mammon';
|
import {getMammonPrices} from './mammon';
|
||||||
|
|
||||||
const CACHE_DURATION = 1000 * 60 * 5; // 5 minutes
|
|
||||||
const BATCH_SIZE = 100;
|
const BATCH_SIZE = 100;
|
||||||
|
|
||||||
export const useApraisalStore = defineStore('appraisal', () => {
|
export const getPrice = async (type: MarketType, locationId?: number): Promise<MarketTypePrice> => (await getPrices([type], locationId))[0];
|
||||||
const cache: RegionalMarketCache<MarketTypePrice> = new RegionalMarketCache(CACHE_DURATION);
|
|
||||||
|
|
||||||
const getPricesUncached = getMammonPrices;
|
export const getPrices = async (types: MarketType[], locationId?: number): Promise<MarketTypePrice[]> => {
|
||||||
|
const batches: Promise<MarketTypePrice[]>[] = [];
|
||||||
|
|
||||||
const getPrice = async (type: MarketType, regionId?: number): Promise<MarketTypePrice> => (await getPrices([type], regionId))[0];
|
for (let i = 0; i < types.length; i += BATCH_SIZE) {
|
||||||
const getPrices = async (types: MarketType[], regionId?: number): Promise<MarketTypePrice[]> => {
|
batches.push(getMammonPrices(types.slice(i, i + BATCH_SIZE), locationId));
|
||||||
const cached: MarketTypePrice[] = [];
|
}
|
||||||
const uncached: MarketType[] = [];
|
|
||||||
const rId = regionId ?? jitaId;
|
|
||||||
|
|
||||||
types.forEach(t => {
|
return (await Promise.all(batches)).flat();
|
||||||
const cachedPrice = cache.get(rId, t.id);
|
};
|
||||||
|
|
||||||
if (cachedPrice) {
|
|
||||||
cached.push(cachedPrice);
|
|
||||||
} else {
|
|
||||||
uncached.push(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uncached.length > 0) {
|
|
||||||
const batches: Promise<MarketTypePrice[]>[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
|
|
||||||
batches.push(getPricesUncached(uncached.slice(i, i + BATCH_SIZE)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const prices = (await Promise.all(batches)).flat();
|
|
||||||
|
|
||||||
prices.forEach(p => cache.set(rId, p.type.id, p));
|
|
||||||
return [ ...cached, ...prices ];
|
|
||||||
}
|
|
||||||
return cached;
|
|
||||||
};
|
|
||||||
return { getPrice, getPrices };
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {marketApi} from '@/mammon';
|
||||||
|
import {getMarketTypes} from '../type';
|
||||||
|
|
||||||
|
export type AppraisalItemValue = {
|
||||||
|
typeID: number;
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
buy: number;
|
||||||
|
sell: number;
|
||||||
|
buyUnit: number;
|
||||||
|
sellUnit: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appraise = async (raw?: string, locationId?: number): Promise<AppraisalItemValue[]> => {
|
||||||
|
if (!raw?.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await marketApi.pastePrices(raw, locationId).then(r => r.data);
|
||||||
|
const names = await getMarketTypes(results.map(r => r.marketTypeId))
|
||||||
|
.then(types => new Map(types.map(t => [t.id, t.name])));
|
||||||
|
|
||||||
|
return results.map(r => ({
|
||||||
|
typeID: r.marketTypeId,
|
||||||
|
name: names.get(r.marketTypeId) ?? '',
|
||||||
|
quantity: r.quantity,
|
||||||
|
buy: r.buy,
|
||||||
|
sell: r.sell,
|
||||||
|
buyUnit: r.quantity ? r.buy / r.quantity : 0,
|
||||||
|
sellUnit: r.quantity ? r.sell / r.quantity : 0,
|
||||||
|
}));
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {marketApi} from '@/mammon';
|
||||||
|
import {getMarketTypes, MarketType} from '../type';
|
||||||
|
import {MarketTypePrice} from './MarketTypePrice';
|
||||||
|
|
||||||
|
export type LocationPrice = {
|
||||||
|
buyUnit: number;
|
||||||
|
sellUnit: number;
|
||||||
|
buy: number;
|
||||||
|
sell: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComparedItem = {
|
||||||
|
typeId: number;
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
volume: number;
|
||||||
|
prices: Record<number, LocationPrice>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PricesForLocation = (types: MarketType[], locationId: number) => Promise<MarketTypePrice[]>;
|
||||||
|
|
||||||
|
export const compareAppraisal = async (
|
||||||
|
raw: string,
|
||||||
|
locationIds: number[],
|
||||||
|
getPrices: PricesForLocation
|
||||||
|
): Promise<ComparedItem[]> => {
|
||||||
|
if (!raw?.trim() || locationIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stacks = await marketApi.parseStacks(raw).then(r => r.data);
|
||||||
|
|
||||||
|
if (stacks.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantities = new Map(stacks.map(s => [s.marketTypeId, s.quantity]));
|
||||||
|
const types = await getMarketTypes(stacks.map(s => s.marketTypeId));
|
||||||
|
|
||||||
|
const rows = new Map<number, ComparedItem>(types.map(t => [t.id, {
|
||||||
|
typeId: t.id,
|
||||||
|
name: t.name,
|
||||||
|
quantity: quantities.get(t.id) ?? 0,
|
||||||
|
volume: t.volume,
|
||||||
|
prices: {},
|
||||||
|
}]));
|
||||||
|
|
||||||
|
const pricesByLocation = await Promise.all(
|
||||||
|
locationIds.map(async locationId => [locationId, await getPrices(types, locationId)] as const)
|
||||||
|
);
|
||||||
|
|
||||||
|
pricesByLocation.forEach(([locationId, prices]) => prices.forEach(p => {
|
||||||
|
const row = rows.get(p.type.id);
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
row.prices[locationId] = {
|
||||||
|
buyUnit: p.buy,
|
||||||
|
sellUnit: p.sell,
|
||||||
|
buy: p.buy * row.quantity,
|
||||||
|
sell: p.sell * row.quantity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...rows.values()];
|
||||||
|
};
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import {logResource} from '@/service';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
export const evepraisalAxiosInstance = axios.create({
|
|
||||||
baseURL: import.meta.env.VITE_EVEPRAISAL_URL,
|
|
||||||
headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
})
|
|
||||||
logResource(evepraisalAxiosInstance)
|
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
export * from './MarketTypePrice';
|
export * from './MarketTypePrice';
|
||||||
export * from './appraisal';
|
export * from './appraisal';
|
||||||
|
export * from './appraise';
|
||||||
|
export * from './compare';
|
||||||
|
|
||||||
|
export { default as AppraisalResultTable } from './AppraisalResultTable.vue';
|
||||||
|
export { default as CompareResultTable } from './CompareResultTable.vue';
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {marketApi} from '@/mammon/mammonService';
|
import {marketApi} from '@/mammon/mammonService';
|
||||||
import {MarketTypePrice, PriceGetter} from './MarketTypePrice';
|
import {MarketTypePrice, PriceGetter} from './MarketTypePrice';
|
||||||
|
|
||||||
export const getMammonPrices: PriceGetter = async types => {
|
export const getMammonPrices: PriceGetter = async (types, locationId) => {
|
||||||
if (types.length === 0) {
|
if (types.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const typesById = new Map(types.map(t => [t.id, t]));
|
const typesById = new Map(types.map(t => [t.id, t]));
|
||||||
const response = await marketApi.currentPrices(types.map(t => t.id));
|
const response = await marketApi.currentPrices(types.map(t => t.id), locationId);
|
||||||
|
|
||||||
return response.data.reduce<MarketTypePrice[]>((prices, p) => {
|
return response.data.reduce<MarketTypePrice[]>((prices, p) => {
|
||||||
const type = typesById.get(p.marketTypeId);
|
const type = typesById.get(p.marketTypeId);
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import { MarketHistory } from "@/market";
|
|
||||||
|
|
||||||
export type HistoryQuartils = {
|
|
||||||
totalVolume: number,
|
|
||||||
q1: number,
|
|
||||||
median: number,
|
|
||||||
q3: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getHistoryQuartils = (history: MarketHistory[], days?: number): HistoryQuartils => {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
const volumes = history
|
|
||||||
.flatMap(h => {
|
|
||||||
const volume = h.volume;
|
|
||||||
|
|
||||||
if (volume === 0 || (days && new Date(h.date).getTime() < now - days * 24 * 60 * 60 * 1000)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const e = estimateVolume(h);
|
|
||||||
|
|
||||||
return [[h.highest, e], [h.lowest, volume - e]];
|
|
||||||
})
|
|
||||||
.filter(h => h[1] > 0)
|
|
||||||
.sort((a, b) => a[0] - b[0]);
|
|
||||||
|
|
||||||
const totalVolume = volumes.reduce((acc, [_, v]) => acc + v, 0);
|
|
||||||
const quartilVolume = totalVolume / 4;
|
|
||||||
const quartils: [number, number, number] = [0, 0, 0];
|
|
||||||
|
|
||||||
let currentVolume = 0;
|
|
||||||
let quartil = 0;
|
|
||||||
|
|
||||||
for (const [price, volume] of volumes) {
|
|
||||||
currentVolume += volume;
|
|
||||||
|
|
||||||
if (currentVolume >= quartilVolume * (quartil + 1)) {
|
|
||||||
quartils[quartil] = price;
|
|
||||||
if (quartil === 2) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
quartil++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
totalVolume,
|
|
||||||
q1: quartils[0],
|
|
||||||
median: quartils[1],
|
|
||||||
q3: quartils[2],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const estimateVolume = (history: MarketHistory): number => {
|
|
||||||
if (history.volume === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.max(1, Math.round(history.volume * ((history.average - history.lowest) / (history.highest - history.lowest))));
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { MarketHistoryResponse } from "@/generated/mammon";
|
|
||||||
import { marketApi } from "@/mammon";
|
|
||||||
|
|
||||||
export type MarketHistory = MarketHistoryResponse;
|
|
||||||
|
|
||||||
export const getHistory = async (typeId: number): Promise<MarketHistory[]> =>
|
|
||||||
(await marketApi.findHistory(typeId)).data;
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {type MarketTrend, MarketTrends} from '@/market';
|
||||||
|
import {ArrowLongRightIcon, ArrowTrendingDownIcon, ArrowTrendingUpIcon} from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
trend: MarketTrend;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ArrowTrendingUpIcon v-if="trend === MarketTrends.Up" />
|
||||||
|
<ArrowTrendingDownIcon v-else-if="trend === MarketTrends.Down" />
|
||||||
|
<ArrowLongRightIcon v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/style.css";
|
||||||
|
|
||||||
|
svg {
|
||||||
|
@apply w-6 h-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {type MarketTrend, MarketTrends} from '@/market';
|
||||||
|
import {ArrowLongRightIcon, ArrowTrendingDownIcon, ArrowTrendingUpIcon} from '@heroicons/vue/24/outline';
|
||||||
|
|
||||||
|
const model = defineModel<MarketTrend[]>({ required: true });
|
||||||
|
|
||||||
|
const trends = [
|
||||||
|
{ trend: MarketTrends.Up, icon: ArrowTrendingUpIcon, title: 'Show items trending up' },
|
||||||
|
{ trend: MarketTrends.Flat, icon: ArrowLongRightIcon, title: 'Show items with a flat trend' },
|
||||||
|
{ trend: MarketTrends.Down, icon: ArrowTrendingDownIcon, title: 'Show items trending down' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const toggle = (trend: MarketTrend) => {
|
||||||
|
model.value = model.value.includes(trend)
|
||||||
|
? model.value.filter(t => t !== trend)
|
||||||
|
: [...model.value, trend];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="trend-filter">
|
||||||
|
<button
|
||||||
|
v-for="{ trend, icon, title } in trends"
|
||||||
|
:key="trend"
|
||||||
|
type="button"
|
||||||
|
:title="title"
|
||||||
|
:class="{ active: model.includes(trend) }"
|
||||||
|
@click="toggle(trend)"
|
||||||
|
>
|
||||||
|
<component :is="icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/style.css";
|
||||||
|
|
||||||
|
div.trend-filter {
|
||||||
|
@apply inline-flex rounded overflow-hidden border border-slate-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.trend-filter button {
|
||||||
|
@apply p-0.5 border-none rounded-none bg-slate-600 text-slate-100 hover:bg-slate-700 cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.trend-filter button.active {
|
||||||
|
@apply bg-emerald-500 hover:bg-emerald-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.trend-filter button :deep(svg) {
|
||||||
|
@apply w-6 h-6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
HistoryQuartilesResponse,
|
||||||
|
HistoryQuartilesResponseTrendEnum,
|
||||||
|
MarketScanResponseTrendEnum
|
||||||
|
} from "@/generated/mammon";
|
||||||
|
import {marketApi} from "@/mammon";
|
||||||
|
|
||||||
|
export type HistoryQuartiles = HistoryQuartilesResponse;
|
||||||
|
|
||||||
|
export type MarketTrend = MarketScanResponseTrendEnum | HistoryQuartilesResponseTrendEnum;
|
||||||
|
export const MarketTrends = MarketScanResponseTrendEnum as const;
|
||||||
|
|
||||||
|
export const getQuartiles = async (typeId: number, days?: number): Promise<HistoryQuartiles> => (await marketApi.findQuartiles(typeId, days)).data;
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from './MarketHistory';
|
export * from './history.ts';
|
||||||
export * from './HistoryQuartils';
|
|
||||||
|
export { default as MarketTrendIcon } from './MarketTrendIcon.vue';
|
||||||
|
export { default as TrendFilter } from './TrendFilter.vue';
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
export * from './RegionalMarketCache';
|
|
||||||
export * from './history';
|
export * from './history';
|
||||||
|
export * from './location';
|
||||||
|
export * from './order';
|
||||||
export * from './tax';
|
export * from './tax';
|
||||||
export * from './type';
|
export * from './type';
|
||||||
|
|
||||||
export * from './appraisal';
|
export * from './appraisal';
|
||||||
export * from './market';
|
|
||||||
|
|
||||||
export { default as IskLabel } from './IskLabel.vue';
|
export { default as IskLabel } from './IskLabel.vue';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {locationApi} from '@/mammon/mammonService';
|
||||||
|
import type {DockableLocationResponse} from '@/generated/mammon';
|
||||||
|
|
||||||
|
export type MarketLocation = DockableLocationResponse;
|
||||||
|
|
||||||
|
const cache = new Map<number, MarketLocation>();
|
||||||
|
|
||||||
|
const defaultHubIds = [
|
||||||
|
60003760, // Jita IV - Moon 4 - Caldari Navy Assembly Plant
|
||||||
|
60008494, // Amarr VIII (Oris) - Emperor Family Academy
|
||||||
|
60011866, // Dodixie IX - Moon 20 - Federation Navy Assembly Plant
|
||||||
|
60004588, // Rens VI - Moon 8 - Brutor Tribe Treasury
|
||||||
|
60005686, // Hek VIII - Moon 12 - Boundless Creation Factory
|
||||||
|
];
|
||||||
|
|
||||||
|
const hubRank = (id: number) => {
|
||||||
|
const rank = defaultHubIds.indexOf(id);
|
||||||
|
return rank < 0 ? defaultHubIds.length : rank;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDefaultHub = (id: number): boolean => defaultHubIds.includes(id);
|
||||||
|
|
||||||
|
export const searchMarketLocations = async (search: string): Promise<MarketLocation[]> => {
|
||||||
|
if (search.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const locations = await locationApi.searchLocations(search).then(r => r.data);
|
||||||
|
locations.forEach(l => cache.set(l.id, l));
|
||||||
|
return locations.toSorted((a, b) => hubRank(a.id) - hubRank(b.id));
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {watchDebounced} from '@vueuse/core';
|
||||||
|
import {ref, watch} from 'vue';
|
||||||
|
import {SelectInput} from '@/components';
|
||||||
|
import {isDefaultHub, MarketLocation, searchMarketLocations} from './MarketLocation';
|
||||||
|
|
||||||
|
const modelValue = defineModel<MarketLocation>();
|
||||||
|
|
||||||
|
const search = ref(modelValue.value?.name ?? '');
|
||||||
|
const suggestions = ref<MarketLocation[]>([]);
|
||||||
|
|
||||||
|
watch(() => modelValue.value, v => {
|
||||||
|
search.value = v?.name ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
watchDebounced(search, async value => {
|
||||||
|
const term = value.trim();
|
||||||
|
|
||||||
|
if (term.length < 3 || term === modelValue.value?.name) {
|
||||||
|
suggestions.value = [];
|
||||||
|
} else {
|
||||||
|
suggestions.value = await searchMarketLocations(term);
|
||||||
|
}
|
||||||
|
}, {debounce: 300});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectInput v-model="modelValue" :items="suggestions" class="w-96">
|
||||||
|
<template #input>
|
||||||
|
<input type="text" v-model="search" placeholder="Search a market location…" />
|
||||||
|
</template>
|
||||||
|
<template #item="{ item }">
|
||||||
|
<div class="px-1 whitespace-nowrap overflow-hidden text-ellipsis" :class="{'text-emerald-400': isDefaultHub(item.id)}">
|
||||||
|
{{ item.name }} <span class="text-slate-300">({{ item.regionName }})</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SelectInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/style.css";
|
||||||
|
input {
|
||||||
|
@apply w-full border-none bg-transparent block focus-visible:outline-none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './location';
|
||||||
|
export * from './MarketLocation';
|
||||||
|
export { default as MarketLocationInput } from './MarketLocationInput.vue';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {useLocalStorage} from "@vueuse/core";
|
||||||
|
import {defineStore} from "pinia";
|
||||||
|
import type {MarketLocation} from "./MarketLocation";
|
||||||
|
|
||||||
|
const defaultLocation: MarketLocation = {
|
||||||
|
id: 60003760,
|
||||||
|
name: 'Jita IV - Moon 4 - Caldari Navy Assembly Plant',
|
||||||
|
systemId: 30000142,
|
||||||
|
systemName: 'Jita',
|
||||||
|
regionId: 10000002,
|
||||||
|
regionName: 'The Forge',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMarketLocationStore = defineStore("marketLocation", () => {
|
||||||
|
const location = useLocalStorage<MarketLocation>("market-location", defaultLocation);
|
||||||
|
|
||||||
|
return {location};
|
||||||
|
});
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const jitaId = 10000002;
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './order';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {marketApi} from "@/mammon";
|
||||||
|
import {MarketOrderResponseDirectionEnum} from "@/generated/mammon";
|
||||||
|
|
||||||
|
export const getListedSellTypeIds = (): Promise<Set<number>> => marketApi.findAllMarketOrders()
|
||||||
|
.then(response => new Set(response.data
|
||||||
|
.filter(o => o.direction === MarketOrderResponseDirectionEnum.Sell)
|
||||||
|
.map(o => o.marketTypeId)));
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
import {SliderCheckbox} from '@/components';
|
import {SliderCheckbox} from '@/components';
|
||||||
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||||
import {formatIsk, percentFormater} from "@/formaters";
|
import {formatIsk, percentFormater} from "@/formaters";
|
||||||
import {MarketType, MarketTypeLabel} from "@/market";
|
import {MarketTrend, MarketTrendIcon, MarketTrends, MarketType, MarketTypeLabel, TrendFilter} from "@/market";
|
||||||
import {ShoppingCartIcon} from '@heroicons/vue/24/outline';
|
|
||||||
import {useStorage} from '@vueuse/core';
|
import {useStorage} from '@vueuse/core';
|
||||||
import {computed, ref} from 'vue';
|
import {computed, ref} from 'vue';
|
||||||
import {useAcquiredTypesStore} from '../acquisition';
|
import {useAcquiredTypesStore} from '../acquisition';
|
||||||
@@ -22,16 +21,13 @@ type Result = {
|
|||||||
acquisitions: number;
|
acquisitions: number;
|
||||||
profit: number;
|
profit: number;
|
||||||
score: number;
|
score: number;
|
||||||
|
trend: MarketTrend;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items?: ScanResult[];
|
items?: ScanResult[];
|
||||||
infoOnly?: boolean;
|
infoOnly?: boolean;
|
||||||
ignoredColums?: string[] | string;
|
ignoredColumns?: string[] | string;
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'buy', type: MarketType, buy: number, sell: number): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const scoreFormater = new Intl.NumberFormat("en-US", {
|
const scoreFormater = new Intl.NumberFormat("en-US", {
|
||||||
@@ -44,25 +40,25 @@ const volumeFormater = new Intl.NumberFormat("en-US", {
|
|||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
items: () => [],
|
items: () => [],
|
||||||
infoOnly: false,
|
infoOnly: false,
|
||||||
ignoredColums: () => []
|
ignoredColumns: () => []
|
||||||
});
|
});
|
||||||
defineEmits<Emits>();
|
|
||||||
|
|
||||||
const acquiredTypesStore = useAcquiredTypesStore();
|
const acquiredTypesStore = useAcquiredTypesStore();
|
||||||
|
|
||||||
const threshold = useStorage('market-scan-threshold', 10);
|
const threshold = useStorage('market-scan-threshold', 10);
|
||||||
const filter = ref("");
|
const filter = ref("");
|
||||||
const onlyCheap = ref(false);
|
const onlyCheap = ref(false);
|
||||||
const columnsToIgnore = computed(() => {
|
const trackedTrends = useStorage<MarketTrend[]>('market-scan-trends', [
|
||||||
const ic = typeof props.ignoredColums === 'string' ? [props.ignoredColums] : props.ignoredColums;
|
MarketTrends.Up,
|
||||||
|
MarketTrends.Flat,
|
||||||
if (props.infoOnly && !ic.includes('buttons')) {
|
MarketTrends.Down,
|
||||||
return [...ic, 'buttons'];
|
]);
|
||||||
}
|
const columnsToIgnore = computed(() =>
|
||||||
return ic;
|
typeof props.ignoredColumns === 'string' ? [props.ignoredColumns] : props.ignoredColumns
|
||||||
});
|
);
|
||||||
const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() => props.items
|
const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() => props.items
|
||||||
.filter(r => r.type.name.toLowerCase().includes(filter.value.toLowerCase()))
|
.filter(r => r.type.name.toLowerCase().includes(filter.value.toLowerCase()))
|
||||||
|
.filter(r => trackedTrends.value.includes(r.trend))
|
||||||
.map(r => {
|
.map(r => {
|
||||||
const acquisitions = columnsToIgnore.value.includes('acquisitions') ? 0 : acquiredTypesStore.acquiredTypes
|
const acquisitions = columnsToIgnore.value.includes('acquisitions') ? 0 : acquiredTypesStore.acquiredTypes
|
||||||
.filter(t => t.type === r.type.id)
|
.filter(t => t.type === r.type.id)
|
||||||
@@ -80,12 +76,13 @@ const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() =>
|
|||||||
totalVolume: r.totalVolume,
|
totalVolume: r.totalVolume,
|
||||||
acquisitions,
|
acquisitions,
|
||||||
profit: r.profit,
|
profit: r.profit,
|
||||||
score: r.score
|
score: r.score,
|
||||||
|
trend: r.trend,
|
||||||
};
|
};
|
||||||
}).filter(r => !onlyCheap.value || (r.buy <= r.q1 && r.profit >= (threshold.value / 100)))), {
|
}).filter(r => !onlyCheap.value || (r.buy <= r.q1 && r.profit >= (threshold.value / 100)))), {
|
||||||
defaultSortKey: 'score',
|
defaultSortKey: 'score',
|
||||||
defaultSortDirection: 'desc',
|
defaultSortDirection: 'desc',
|
||||||
ignoredColums: columnsToIgnore
|
ignoredColumns: columnsToIgnore
|
||||||
})
|
})
|
||||||
const getLineColor = (result: Result) => {
|
const getLineColor = (result: Result) => {
|
||||||
if (props.infoOnly) {
|
if (props.infoOnly) {
|
||||||
@@ -111,6 +108,9 @@ const getLineColor = (result: Result) => {
|
|||||||
<div class="end flex">
|
<div class="end flex">
|
||||||
<SliderCheckbox class="me-1" v-model="onlyCheap" /> Show only cheap items
|
<SliderCheckbox class="me-1" v-model="onlyCheap" /> Show only cheap items
|
||||||
</div>
|
</div>
|
||||||
|
<div class="end flex items-center">
|
||||||
|
<TrendFilter v-model="trackedTrends" />
|
||||||
|
</div>
|
||||||
<div class="end">
|
<div class="end">
|
||||||
<span>Filter: </span>
|
<span>Filter: </span>
|
||||||
<input type="search" class="w-96" v-model="filter" />
|
<input type="search" class="w-96" v-model="filter" />
|
||||||
@@ -131,13 +131,15 @@ const getLineColor = (result: Result) => {
|
|||||||
<SortableHeader v-bind="headerProps" sortKey="profit">Profit</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="profit">Profit</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="score">Score</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="score">Score</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="acquisitions">Acquisitions</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="acquisitions">Acquisitions</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="buttons" unsortable />
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="r in list" :key="r.data.typeID" :class="getLineColor(r.data)">
|
<tr v-for="r in list" :key="r.data.typeID" :class="getLineColor(r.data)">
|
||||||
<td v-if="showColumn('name')">
|
<td v-if="showColumn('name')">
|
||||||
|
<div class="flex">
|
||||||
<MarketTypeLabel :id="r.data.typeID" :name="r.data.name" />
|
<MarketTypeLabel :id="r.data.typeID" :name="r.data.name" />
|
||||||
|
<MarketTrendIcon class="ms-auto" :trend="r.data.trend" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="showColumn('buy')" class="text-right">{{ formatIsk(r.data.buy) }}</td>
|
<td v-if="showColumn('buy')" class="text-right">{{ formatIsk(r.data.buy) }}</td>
|
||||||
<td v-if="showColumn('sell')" class="text-right">{{ formatIsk(r.data.sell) }}</td>
|
<td v-if="showColumn('sell')" class="text-right">{{ formatIsk(r.data.sell) }}</td>
|
||||||
@@ -148,9 +150,6 @@ const getLineColor = (result: Result) => {
|
|||||||
<td v-if="showColumn('profit')" class="text-right">{{ percentFormater.format(r.data.profit) }}</td>
|
<td v-if="showColumn('profit')" class="text-right">{{ percentFormater.format(r.data.profit) }}</td>
|
||||||
<td v-if="showColumn('score')" class="text-right">{{ scoreFormater.format(r.data.score) }}</td>
|
<td v-if="showColumn('score')" class="text-right">{{ scoreFormater.format(r.data.score) }}</td>
|
||||||
<td v-if="showColumn('acquisitions')" class="text-right">{{ r.data.acquisitions }}</td>
|
<td v-if="showColumn('acquisitions')" class="text-right">{{ r.data.acquisitions }}</td>
|
||||||
<td v-if="showColumn('buttons')" class="text-right">
|
|
||||||
<button class="btn-icon me-1" title="Add acquisitions" @click="$emit('buy', r.data.type, r.data.buy, r.data.sell)"><ShoppingCartIcon /></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MarketType } from "@/market";
|
import {MarketTrend, MarketType} from "@/market";
|
||||||
import { MarketScanResponse } from "@/generated/mammon";
|
import {MarketScanResponse} from "@/generated/mammon";
|
||||||
|
|
||||||
export type ScanResult = {
|
export type ScanResult = {
|
||||||
type: MarketType;
|
type: MarketType;
|
||||||
@@ -11,6 +11,7 @@ export type ScanResult = {
|
|||||||
totalVolume: number;
|
totalVolume: number;
|
||||||
profit: number;
|
profit: number;
|
||||||
score: number;
|
score: number;
|
||||||
|
trend: MarketTrend;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toScanResult = (res: MarketScanResponse, type: MarketType): ScanResult => ({
|
export const toScanResult = (res: MarketScanResponse, type: MarketType): ScanResult => ({
|
||||||
@@ -23,4 +24,5 @@ export const toScanResult = (res: MarketScanResponse, type: MarketType): ScanRes
|
|||||||
totalVolume: res.totalVolume,
|
totalVolume: res.totalVolume,
|
||||||
profit: res.profit,
|
profit: res.profit,
|
||||||
score: res.score,
|
score: res.score,
|
||||||
|
trend: res.trend,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const marketTaxStore = useMarketTaxStore();
|
|||||||
</div>
|
</div>
|
||||||
<div class="end">
|
<div class="end">
|
||||||
<span>SCC: </span>
|
<span>SCC: </span>
|
||||||
<input type="number" min="3.6" max="8" step="0.01" v-model="marketTaxStore.scc" >
|
<input type="number" min="3.375" max="8" step="0.01" v-model="marketTaxStore.scc" >
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { defineStore } from "pinia";
|
|||||||
|
|
||||||
export const useMarketTaxStore = defineStore("marketTax", () => {
|
export const useMarketTaxStore = defineStore("marketTax", () => {
|
||||||
const brokerFee = useLocalStorage("market-brokerFee", 1.5);
|
const brokerFee = useLocalStorage("market-brokerFee", 1.5);
|
||||||
const scc = useLocalStorage("market-scc", 3.6);
|
const scc = useLocalStorage("market-scc", 3.375);
|
||||||
|
|
||||||
const applyTaxes = (price: number, sellOrder?: boolean) => sellOrder ? price * (1 - (brokerFee.value + scc.value) / 100) : price * (1 + brokerFee.value / 100);
|
const applyTaxes = (price: number, sellOrder?: boolean) => sellOrder ? price * (1 - (brokerFee.value + scc.value) / 100) : price * (1 + brokerFee.value / 100);
|
||||||
const calculateProfit = (buy: number, sell: number) => (applyTaxes(sell, true) / applyTaxes(buy)) - 1;
|
const calculateProfit = (buy: number, sell: number) => (applyTaxes(sell, true) / applyTaxes(buy)) - 1;
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ export const getMarketTypes = async (types: (string | number)[]): Promise<Market
|
|||||||
return ids.map(id => cache.get(id)).filter((t): t is MarketType => t !== undefined);
|
return ids.map(id => cache.get(id)).filter((t): t is MarketType => t !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchMarketTypes = async (_search: string): Promise<MarketType[]> => {
|
export const searchMarketTypes = async (search: string): Promise<MarketType[]> => {
|
||||||
return []
|
if (search.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const types = await marketApi.searchTypes(search).then(r => r.data);
|
||||||
|
types.forEach(t => cache.set(t.id, t));
|
||||||
|
return types;
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import {mammonAddCharacterUrl} from "@/mammon";
|
|
||||||
import {CharacterLabel, useCharactersStore} from "@/characters";
|
import {CharacterLabel, useCharactersStore} from "@/characters";
|
||||||
import {ArrowPathIcon} from '@heroicons/vue/24/outline';
|
import {useAuthStore} from "@/auth";
|
||||||
|
|
||||||
const charactersStore = useCharactersStore()
|
const charactersStore = useCharactersStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const addCharacter = () => {
|
const addCharacter = () => authStore.addCharacter();
|
||||||
window.location.replace(mammonAddCharacterUrl);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,7 +16,6 @@ const addCharacter = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-for="character in charactersStore.characters" :key="character.characterId" class="flex items-center mb-2">
|
<div v-for="character in charactersStore.characters" :key="character.characterId" class="flex items-center mb-2">
|
||||||
<CharacterLabel class="grow" :character="character" />
|
<CharacterLabel class="grow" :character="character" />
|
||||||
<button class="btn-icon" @click="charactersStore.reloadActivities(character.characterId)"><ArrowPathIcon /></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
+9
-1
@@ -1,6 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {useAuthStore} from '@/auth';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div class="flex flex-col items-center justify-center h-screen gap-4">
|
||||||
|
<h1 class="text-2xl">eveal</h1>
|
||||||
|
<button v-if="!authStore.isAuthenticated" @click="authStore.login()">
|
||||||
|
Login with EVE Online
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
+1
-21
@@ -1,27 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {RouterView} from 'vue-router';
|
import {RouterView} from 'vue-router';
|
||||||
import {EditLedgerModal, useLedgersStore} from "@/ledger";
|
|
||||||
import {ref} from "vue";
|
|
||||||
import {activityApi, processingApi} from "@/mammon";
|
|
||||||
|
|
||||||
const ledgersStore = useLedgersStore();
|
|
||||||
|
|
||||||
const editLedgerModal = ref<typeof EditLedgerModal>();
|
|
||||||
|
|
||||||
const processActivities = async () => {
|
|
||||||
await activityApi.fetchAllNewActivities();
|
|
||||||
await processingApi.processNewActivities();
|
|
||||||
await ledgersStore.refresh();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-4">
|
<RouterView />
|
||||||
<div class="mb-4 border-b-1 flex justify-end">
|
|
||||||
<button class="mb-2 ms-2" @click="processActivities">Process Activities</button>
|
|
||||||
<button class="mb-2 ms-2" @click="editLedgerModal?.open()">New Ledger</button>
|
|
||||||
</div>
|
|
||||||
<EditLedgerModal ref="editLedgerModal" />
|
|
||||||
<RouterView />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ import {routeNames} from '@/routes';
|
|||||||
<RouterLink to="/market/acquisitions" class="tab">
|
<RouterLink to="/market/acquisitions" class="tab">
|
||||||
<span>Acquisitions</span>
|
<span>Acquisitions</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink :to="{name: routeNames.appraise}" class="tab">
|
||||||
|
<span>Appraisal</span>
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink :to="{name: routeNames.compareAppraisal}" class="tab">
|
||||||
|
<span>Compare</span>
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-11
@@ -1,31 +1,36 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ReprocessInput, ReprocessItemValues, ReprocessResultTable, reprocess } from '@/reprocess';
|
import { useCharactersStore } from '@/characters';
|
||||||
|
import { ReprocessItemValues, ReprocessResultTable, reprocess } from '@/reprocess';
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const items = ref("");
|
const charactersStore = useCharactersStore();
|
||||||
const materials = ref("");
|
|
||||||
const efficiency = useStorage('reprocess-efficiency', 0.55);
|
const characterId = useStorage<number | undefined>('reprocess-character', undefined);
|
||||||
|
const items = ref('');
|
||||||
|
|
||||||
const result = ref<ReprocessItemValues[]>([]);
|
const result = ref<ReprocessItemValues[]>([]);
|
||||||
|
|
||||||
const send = async () => result.value = await reprocess(items.value, materials.value, efficiency.value);
|
const send = async () => result.value = await reprocess(characterId.value, items.value);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid mb-2 mt-4">
|
<div class="grid mb-2 mt-4">
|
||||||
<div class="justify-self-end">
|
<div class="justify-self-end">
|
||||||
<span>Reprocess efficiency: </span>
|
<span>Character: </span>
|
||||||
<input type="number" min="0" max="1" step="0.05" v-model="efficiency" />
|
<select v-model="characterId">
|
||||||
|
<option v-for="character in charactersStore.characters" :key="character.characterId" :value="character.characterId">{{ character.name }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-stretch">
|
<div class="flex items-stretch">
|
||||||
<ReprocessInput name="Item JSON" v-model="items" />
|
<div class="flex-1 mx-1">
|
||||||
<ReprocessInput name="Materials JSON" v-model="materials" />
|
<span>Items</span>
|
||||||
|
<textarea class="mt-1" v-model="items" placeholder="Paste an EVE inventory listing" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid my-2">
|
<div class="grid my-2">
|
||||||
<button class="justify-self-end" @click="send">Send</button>
|
<button class="justify-self-end" @click="send" :disabled="!characterId || !items.trim()">Send</button>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="result.length > 0">
|
<template v-if="result.length > 0">
|
||||||
<hr />
|
<hr />
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {RouterLink, RouterView} from "vue-router";
|
|
||||||
import {routeNames} from '@/routes';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="mt-4">
|
|
||||||
<div class="flex border-b-2 border-emerald-500">
|
|
||||||
<RouterLink :to="{ name: routeNames.listRuleBooks }" class="tab">
|
|
||||||
<span>Rule Books</span>
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink to="/characters/rules" class="tab">
|
|
||||||
<span>Characters Rules</span>
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
<RouterView />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -2,29 +2,31 @@
|
|||||||
|
|
||||||
import {findAllTransactionInLeger, useLedgerParam} from "@/ledger";
|
import {findAllTransactionInLeger, useLedgerParam} from "@/ledger";
|
||||||
import {computedAsync} from "@vueuse/core";
|
import {computedAsync} from "@vueuse/core";
|
||||||
import {TransactionResponse} from "@/generated/mammon";
|
import {ActivitySourceResponseTypeEnum, TransactionResponse} from "@/generated/mammon";
|
||||||
import {IskLabel} from "@/market";
|
import {IskLabel} from "@/market";
|
||||||
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
||||||
import {TransferList, TransferTypes} from "@/transaction";
|
import {TransferList, TransferTypes} from "@/transaction";
|
||||||
import {Dropdown} from "@/components";
|
import {Dropdown} from "@/components";
|
||||||
import {CharacterLabel, useCharactersStore} from "@/characters";
|
import {SourceLabel, useProcessedResource} from "@/activity";
|
||||||
|
import {useCharactersStore} from "@/characters";
|
||||||
import {formatEveDate} from "@/formaters.ts";
|
import {formatEveDate} from "@/formaters.ts";
|
||||||
|
|
||||||
const {ledgerId} = useLedgerParam();
|
const {ledgerId} = useLedgerParam();
|
||||||
const charactersStore = useCharactersStore();
|
const charactersStore = useCharactersStore();
|
||||||
|
|
||||||
const transactions = computedAsync<TransactionResponse[]>(async () => {
|
const transactions = useProcessedResource<TransactionResponse[]>(
|
||||||
if (ledgerId.value) {
|
() => ledgerId.value ? findAllTransactionInLeger(ledgerId.value) : Promise.resolve([]),
|
||||||
return await findAllTransactionInLeger(ledgerId.value);
|
[],
|
||||||
}
|
);
|
||||||
return [];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { sortedArray, headerProps } = useSort(computedAsync(() => Promise.all(transactions.value.map(async transaction => {
|
const { sortedArray, headerProps } = useSort(computedAsync(() => Promise.all(transactions.value.map(async transaction => {
|
||||||
const character = await charactersStore.findById(transaction.characterId);
|
const source = transaction.source;
|
||||||
|
const character = source.type === ActivitySourceResponseTypeEnum.Character && source.characterId
|
||||||
|
? await charactersStore.findById(source.characterId)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
character,
|
source,
|
||||||
characterName: character?.name ?? "",
|
sourceName: character?.name ?? (source.type === ActivitySourceResponseTypeEnum.Corporation ? "Corporation" : ""),
|
||||||
transactionId: transaction.transactionId,
|
transactionId: transaction.transactionId,
|
||||||
description: transaction.description,
|
description: transaction.description,
|
||||||
date: new Date(transaction.datetime),
|
date: new Date(transaction.datetime),
|
||||||
@@ -59,7 +61,7 @@ const getIskBalance = (transaction: TransactionResponse) => {
|
|||||||
<template #default="{ list }">
|
<template #default="{ list }">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="characterName">Character</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="sourceName">Source</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="date">Date</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="date">Date</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="balance">Isk Change</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="balance">Isk Change</SortableHeader>
|
||||||
<SortableHeader v-bind="headerProps" sortKey="description">Description</SortableHeader>
|
<SortableHeader v-bind="headerProps" sortKey="description">Description</SortableHeader>
|
||||||
@@ -68,7 +70,7 @@ const getIskBalance = (transaction: TransactionResponse) => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="t in list" :key="t.data.transactionId">
|
<tr v-for="t in list" :key="t.data.transactionId">
|
||||||
<td>
|
<td>
|
||||||
<CharacterLabel v-if="t.data.character" :character="t.data.character" />
|
<SourceLabel :source="t.data.source" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Dropdown class="transfer-dropdown">
|
<Dropdown class="transfer-dropdown">
|
||||||
|
|||||||
@@ -5,9 +5,18 @@ import {nextTick, ref} from "vue";
|
|||||||
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
|
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
|
||||||
import {IskLabel} from "@/market";
|
import {IskLabel} from "@/market";
|
||||||
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
||||||
|
import {activityApi} from "@/mammon";
|
||||||
|
import {toast} from "@/toast";
|
||||||
|
|
||||||
const ledgersStore = useLedgersStore();
|
const ledgersStore = useLedgersStore();
|
||||||
|
|
||||||
|
const editLedgerModal = ref<typeof EditLedgerModal>();
|
||||||
|
|
||||||
|
const processActivities = async () => {
|
||||||
|
toast.info('Processing started');
|
||||||
|
await activityApi.processNewActivities();
|
||||||
|
}
|
||||||
|
|
||||||
const { sortedArray, headerProps } = useSort<Ledger>(() => ledgersStore.ledgers);
|
const { sortedArray, headerProps } = useSort<Ledger>(() => ledgersStore.ledgers);
|
||||||
|
|
||||||
const editModal = ref<typeof EditLedgerModal>();
|
const editModal = ref<typeof EditLedgerModal>();
|
||||||
@@ -22,6 +31,13 @@ const openEdit = async (ledgerId: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="mb-4 border-b-1 flex justify-end">
|
||||||
|
<button class="mb-2 ms-2" @click="processActivities">Process Activities</button>
|
||||||
|
<button class="mb-2 ms-2" @click="editLedgerModal?.open()">New Ledger</button>
|
||||||
|
</div>
|
||||||
|
<EditLedgerModal ref="editLedgerModal" />
|
||||||
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
|
<VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
|
||||||
<template #default="{ list }">
|
<template #default="{ list }">
|
||||||
|
|||||||
@@ -1,21 +1,61 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {RouterLink, RouterView} from 'vue-router';
|
import {RouterLink, RouterView} from 'vue-router';
|
||||||
import {isMain, useLedgerParam} from "@/ledger";
|
import {appraiseItemBalances, getLedgerBalance, isMain, LedgerLabel, useLedgerParam} from "@/ledger";
|
||||||
import {routeNames} from "@/routes.ts";
|
import {routeNames} from "@/routes.ts";
|
||||||
|
import {getMarketTypes, getPrices, IskLabel} from "@/market";
|
||||||
|
import {useMarketLocationStore} from '@/market/location';
|
||||||
|
import {computedAsync} from "@vueuse/core";
|
||||||
|
|
||||||
const {ledgerId, ledger} = useLedgerParam();
|
const {ledgerId, ledger} = useLedgerParam();
|
||||||
|
const marketLocationStore = useMarketLocationStore();
|
||||||
|
|
||||||
|
const itemAppraisal = computedAsync(async () => {
|
||||||
|
if (!ledgerId.value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {itemBalances} = await getLedgerBalance(ledgerId.value);
|
||||||
|
|
||||||
|
if (itemBalances.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = await getMarketTypes([...new Set(itemBalances.map(i => i.typeId))]);
|
||||||
|
const prices = await getPrices(types, marketLocationStore.location.id);
|
||||||
|
const appraisal = appraiseItemBalances(itemBalances, prices);
|
||||||
|
|
||||||
|
return appraisal.buy === 0 && appraisal.sell === 0 ? undefined : appraisal;
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="ledger" class="mt-4">
|
<div v-if="ledger" class="mt-4">
|
||||||
<div class="flex border-b-2 border-emerald-500">
|
<div class="flex">
|
||||||
|
<div class="flex w-full">
|
||||||
|
<LedgerLabel :ledger="ledger" :link="false" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end items-baseline gap-3 w-full mb-2">
|
||||||
|
<span v-if="itemAppraisal" class="text-sm text-gray-400">
|
||||||
|
~<IskLabel :amount="itemAppraisal.buy" :colored="false" /> buy /
|
||||||
|
~<IskLabel :amount="itemAppraisal.sell" :colored="false" /> sell
|
||||||
|
</span>
|
||||||
|
<IskLabel :amount="ledger.balance" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex border-b-2 border-emerald-500 mt-4">
|
||||||
<RouterLink :to="{name: routeNames.viewLedgerBalance}" class="tab">
|
<RouterLink :to="{name: routeNames.viewLedgerBalance}" class="tab">
|
||||||
<span>Balance</span>
|
<span>Balance</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink v-if="isMain(ledger)" :to="{name: routeNames.listLedgerTransactions}" class="tab">
|
<RouterLink :to="{name: routeNames.listLedgerTransactions}" class="tab">
|
||||||
<span>Transactions</span>
|
<span>Transactions</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink :to="{name: routeNames.listLedgerAcquisitions}" class="tab">
|
||||||
|
<span>Acquisitions</span>
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink v-if="isMain(ledger)" :to="{name: routeNames.viewLedgerStatistics}" class="tab">
|
||||||
|
<span>Statistics</span>
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {AcquisitionsPanel, RawAcquiredType, toAcquiredType} from '@/market/acquisition';
|
||||||
|
import {useLedgerParam} from "@/ledger";
|
||||||
|
import {acquisitionApi} from "@/mammon";
|
||||||
|
import {useProcessedResource} from "@/activity";
|
||||||
|
|
||||||
|
const {ledgerId} = useLedgerParam();
|
||||||
|
|
||||||
|
const items = useProcessedResource<RawAcquiredType[]>(async () => {
|
||||||
|
if (!ledgerId.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = await acquisitionApi.findAllAcquisitions(ledgerId.value);
|
||||||
|
|
||||||
|
return data.map(toAcquiredType).filter(a => a.remaining > 0);
|
||||||
|
}, []);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-4">
|
||||||
|
<AcquisitionsPanel :items="items" :ledgerId="ledgerId" :ignoredColumns="['date', 'ledger']" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -2,17 +2,16 @@
|
|||||||
import {getLedgerBalance, useLedgerParam} from "@/ledger";
|
import {getLedgerBalance, useLedgerParam} from "@/ledger";
|
||||||
import {computedAsync} from "@vueuse/core";
|
import {computedAsync} from "@vueuse/core";
|
||||||
import {BalanceResponse} from "@/generated/mammon";
|
import {BalanceResponse} from "@/generated/mammon";
|
||||||
import {getMarketType, IskLabel, MarketTypeLabel} from "@/market";
|
import {getMarketType, MarketTypeLabel} from "@/market";
|
||||||
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
||||||
|
import {useProcessedResource} from "@/activity";
|
||||||
|
|
||||||
const {ledgerId} = useLedgerParam();
|
const {ledgerId} = useLedgerParam();
|
||||||
|
|
||||||
const balance = computedAsync<BalanceResponse>(async () => {
|
const balance = useProcessedResource<BalanceResponse | undefined>(
|
||||||
if (ledgerId.value) {
|
() => ledgerId.value ? getLedgerBalance(ledgerId.value) : Promise.resolve(undefined),
|
||||||
return await getLedgerBalance(ledgerId.value);
|
undefined,
|
||||||
}
|
);
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { sortedArray, headerProps } = useSort(computedAsync(async () => {
|
const { sortedArray, headerProps } = useSort(computedAsync(async () => {
|
||||||
const itemBalances = balance.value?.itemBalances;
|
const itemBalances = balance.value?.itemBalances;
|
||||||
@@ -35,9 +34,7 @@ const { sortedArray, headerProps } = useSort(computedAsync(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="balance" class="mt-4">
|
<div v-if="balance" class="mt-4">
|
||||||
<div class="flex justify-end w-full">
|
|
||||||
<IskLabel class="mb-2" :amount="balance.iskBalance" />
|
|
||||||
</div>
|
|
||||||
<VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
|
<VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
|
||||||
<template #default="{ list }">
|
<template #default="{ list }">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
Chart as ChartJS,
|
||||||
|
type ChartData,
|
||||||
|
type ChartDataset,
|
||||||
|
type ChartOptions,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Tooltip,
|
||||||
|
} from "chart.js";
|
||||||
|
import {Chart} from "vue-chartjs";
|
||||||
|
import {useLedgerParam} from "@/ledger";
|
||||||
|
import {transactionApi} from "@/mammon";
|
||||||
|
import {useProcessedResource} from "@/activity";
|
||||||
|
import {DailyProfitResponse} from "@/generated/mammon";
|
||||||
|
import {IskLabel} from "@/market";
|
||||||
|
import {formatIsk} from "@/formaters";
|
||||||
|
import {DualRangeSlider} from "@/components";
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale, LinearScale,
|
||||||
|
BarController, BarElement,
|
||||||
|
LineController, LineElement, PointElement,
|
||||||
|
Tooltip, Legend, Filler,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {ledgerId} = useLedgerParam();
|
||||||
|
|
||||||
|
const ProfitModes = {
|
||||||
|
CashFlow: 'CASH_FLOW',
|
||||||
|
Margin: 'MARGIN',
|
||||||
|
} as const;
|
||||||
|
type ProfitMode = typeof ProfitModes[keyof typeof ProfitModes];
|
||||||
|
|
||||||
|
const mode = ref<ProfitMode>(ProfitModes.CashFlow);
|
||||||
|
|
||||||
|
const profits = useProcessedResource<DailyProfitResponse[]>(async () => {
|
||||||
|
if (!ledgerId.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = mode.value === ProfitModes.Margin
|
||||||
|
? await transactionApi.marginPerDayInLedger(ledgerId.value)
|
||||||
|
: await transactionApi.profitPerDayInLedger(ledgerId.value);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const from = ref(0);
|
||||||
|
const to = ref(0);
|
||||||
|
|
||||||
|
watch(() => profits.value.length, length => {
|
||||||
|
from.value = 0;
|
||||||
|
to.value = Math.max(length - 1, 0);
|
||||||
|
}, {immediate: true});
|
||||||
|
|
||||||
|
const maxIndex = computed(() => Math.max(profits.value.length - 1, 0));
|
||||||
|
|
||||||
|
const range = computed(() => profits.value.slice(from.value, to.value + 1));
|
||||||
|
|
||||||
|
const total = computed(() => range.value.reduce((sum, p) => sum + p.profit, 0));
|
||||||
|
|
||||||
|
const cumulativeProfit = computed(() => {
|
||||||
|
let running = 0;
|
||||||
|
|
||||||
|
return range.value.map(p => (running += p.profit));
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = computed<ChartData<'bar' | 'line'>>(() => {
|
||||||
|
const cumulative: ChartDataset<'line'> = {
|
||||||
|
type: 'line',
|
||||||
|
label: 'Cumulative profit',
|
||||||
|
data: cumulativeProfit.value,
|
||||||
|
borderColor: '#38bdf8',
|
||||||
|
backgroundColor: 'rgba(56, 189, 248, 0.15)',
|
||||||
|
pointBackgroundColor: '#38bdf8',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.2,
|
||||||
|
yAxisID: 'y',
|
||||||
|
order: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const iskIn: ChartDataset<'bar'> = {
|
||||||
|
type: 'bar',
|
||||||
|
label: 'ISK in',
|
||||||
|
data: range.value.map(p => p.iskIn),
|
||||||
|
backgroundColor: 'rgba(16, 185, 129, 0.7)',
|
||||||
|
yAxisID: 'y',
|
||||||
|
order: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const iskOut: ChartDataset<'bar'> = {
|
||||||
|
type: 'bar',
|
||||||
|
label: 'ISK out',
|
||||||
|
data: range.value.map(p => -p.iskOut),
|
||||||
|
backgroundColor: 'rgba(180, 83, 9, 0.7)',
|
||||||
|
yAxisID: 'y',
|
||||||
|
order: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: range.value.map(p => p.date),
|
||||||
|
datasets: [cumulative, iskIn, iskOut],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartOptions = computed<ChartOptions<'bar' | 'line'>>(() => ({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {mode: 'index', intersect: false},
|
||||||
|
plugins: {
|
||||||
|
legend: {display: true},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: ctx => `${ctx.dataset.label}: ${formatIsk(Math.abs(ctx.parsed.y ?? 0))}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
position: 'left',
|
||||||
|
stacked: true,
|
||||||
|
ticks: {
|
||||||
|
callback: value => formatIsk(Math.abs(Number(value))),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<div class="flex bg-slate-600 rounded-s-md p-1">
|
||||||
|
<button class="switch" :class="{active: mode === ProfitModes.CashFlow}" @click="mode = ProfitModes.CashFlow">Cash Flow</button>
|
||||||
|
</div>
|
||||||
|
<div class="switch flex bg-slate-600 rounded-e-md p-1">
|
||||||
|
<button class="switch" :class="{active: mode === ProfitModes.Margin}" @click="mode = ProfitModes.Margin">Margin</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mb-4">
|
||||||
|
<span class="mr-2">Total profit:</span>
|
||||||
|
<IskLabel :amount="total" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="profits.length === 0" class="text-center text-gray-400 mt-8">
|
||||||
|
No trade profit recorded for this ledger
|
||||||
|
</p>
|
||||||
|
<template v-else>
|
||||||
|
<div class="h-96">
|
||||||
|
<Chart type="bar" :data="chartData" :options="chartOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 mt-4">
|
||||||
|
<span class="text-sm w-24 text-gray-400">{{ profits[from]?.date }}</span>
|
||||||
|
<DualRangeSlider class="grow" :min="0" :max="maxIndex" v-model:from="from" v-model:to="to" />
|
||||||
|
<span class="text-sm w-24 text-right text-gray-400">{{ profits[to]?.date }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/style.css";
|
||||||
|
|
||||||
|
button.switch {
|
||||||
|
@apply flex items-center px-4 rounded-md bg-slate-600;
|
||||||
|
&.active {
|
||||||
|
@apply bg-emerald-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,52 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {getMarketTypes, MarketTypePrice, useApraisalStore} from "@/market";
|
import {AcquisitionsPanel, useAcquiredTypesStore} from '@/market/acquisition';
|
||||||
import {AcquiredType, AcquisitionResultTable, BuyModal, SellModal, useAcquiredTypesStore} from '@/market/acquisition';
|
|
||||||
import {ref, watch} from 'vue';
|
|
||||||
import {activityApi, processingApi} from "@/mammon";
|
|
||||||
|
|
||||||
const buyModal = ref<typeof BuyModal>();
|
|
||||||
const sellModal = ref<typeof SellModal>();
|
|
||||||
|
|
||||||
const apraisalStore = useApraisalStore();
|
|
||||||
const acquiredTypesStore = useAcquiredTypesStore();
|
const acquiredTypesStore = useAcquiredTypesStore();
|
||||||
const items = ref<AcquiredType[]>([]);
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
await activityApi.fetchAllNewActivities();
|
|
||||||
await processingApi.processNewActivities();
|
|
||||||
await acquiredTypesStore.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => acquiredTypesStore.acquiredTypes, async itms => {
|
|
||||||
if (itms.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prices = await apraisalStore.getPrices(await getMarketTypes(itms.map(i => i.type)));
|
|
||||||
|
|
||||||
items.value = itms.map(i => {
|
|
||||||
const price = prices.find(p => p.type.id === i.type) as MarketTypePrice;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...i,
|
|
||||||
type: price.type,
|
|
||||||
buy: price.buy,
|
|
||||||
sell: price.sell
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="flex">
|
<AcquisitionsPanel :items="acquiredTypesStore.acquiredTypes" ignoredColumns="date" />
|
||||||
<button class="ms-auto" @click="refresh">Refresh</button>
|
|
||||||
</div>
|
|
||||||
<template v-if="items.length > 0">
|
|
||||||
<AcquisitionResultTable :items="items" @buy="(types, price, buy, sell) => buyModal?.open(types[0].type, { 'Price': price, 'Buy': buy, 'Sell': sell })" @sell="types => sellModal?.open(types)" ignoredColums="date" />
|
|
||||||
<BuyModal ref="buyModal" />
|
|
||||||
<SellModal ref="sellModal" />
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Dropdown} from '@/components';
|
||||||
|
import {AppraisalItemValue, AppraisalResultTable, appraise} from '@/market/appraisal';
|
||||||
|
import {MarketLocationInput, useMarketLocationStore} from '@/market/location';
|
||||||
|
import {ref} from 'vue';
|
||||||
|
|
||||||
|
const items = ref('');
|
||||||
|
const showInputs = ref(true);
|
||||||
|
|
||||||
|
const marketLocationStore = useMarketLocationStore();
|
||||||
|
const result = ref<AppraisalItemValue[]>([]);
|
||||||
|
|
||||||
|
const send = async () => result.value = await appraise(items.value, marketLocationStore.location.id);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dropdown inline :auto-close="false" v-model:open="showInputs" class="mt-4">
|
||||||
|
<template #button>Items</template>
|
||||||
|
<div class="flex items-stretch mt-2">
|
||||||
|
<div class="flex-1 mx-1">
|
||||||
|
<span>Items</span>
|
||||||
|
<textarea class="mt-1" v-model="items" placeholder="Paste an EVE inventory listing" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mx-1 mt-2">
|
||||||
|
<span>Location</span>
|
||||||
|
<div class="mt-1">
|
||||||
|
<MarketLocationInput v-model="marketLocationStore.location" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid my-2">
|
||||||
|
<button class="justify-self-end" @click="send" :disabled="!items.trim()">Send</button>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
<template v-if="result.length > 0">
|
||||||
|
<hr />
|
||||||
|
<div class="grid mt-2">
|
||||||
|
<AppraisalResultTable :result="result" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {Dropdown} from '@/components';
|
||||||
|
import {compareAppraisal, ComparedItem, CompareResultTable, getPrices} from '@/market/appraisal';
|
||||||
|
import {MarketLocation, MarketLocationInput} from '@/market/location';
|
||||||
|
|
||||||
|
const items = ref('');
|
||||||
|
const from = ref<MarketLocation>();
|
||||||
|
const to = ref<MarketLocation>();
|
||||||
|
const showInputs = ref(true);
|
||||||
|
|
||||||
|
const result = ref<ComparedItem[]>([]);
|
||||||
|
const resultLocations = ref<MarketLocation[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
const selected = [from.value, to.value].filter((l): l is MarketLocation => l !== undefined);
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
result.value = await compareAppraisal(items.value, selected.map(l => l.id), getPrices);
|
||||||
|
resultLocations.value = selected;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dropdown inline :auto-close="false" v-model:open="showInputs" class="mt-4">
|
||||||
|
<template #button>Items & locations</template>
|
||||||
|
<div class="flex items-stretch mt-2">
|
||||||
|
<div class="flex-1 mx-1">
|
||||||
|
<span>Items</span>
|
||||||
|
<textarea class="mt-1" v-model="items" placeholder="Paste an EVE inventory listing" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-6 mx-1 mt-2">
|
||||||
|
<div>
|
||||||
|
<span>From</span>
|
||||||
|
<div class="mt-1">
|
||||||
|
<MarketLocationInput v-model="from" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>To</span>
|
||||||
|
<div class="mt-1">
|
||||||
|
<MarketLocationInput v-model="to" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid my-2">
|
||||||
|
<button class="justify-self-end" @click="send" :disabled="!items.trim() || !from || !to || loading">Send</button>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
<template v-if="result.length > 0">
|
||||||
|
<hr />
|
||||||
|
<div class="grid mt-2">
|
||||||
|
<CompareResultTable :result="result" :locations="resultLocations" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {getMarketTypes, TaxInput, useMarketTaxStore} from "@/market";
|
import {getMarketTypes, TaxInput, useMarketTaxStore} from "@/market";
|
||||||
import {BuyModal} from '@/market/acquisition';
|
|
||||||
import {ScanResult, ScanResultTable, toScanResult} from '@/market/scan';
|
import {ScanResult, ScanResultTable, toScanResult} from '@/market/scan';
|
||||||
import {marketApi} from "@/mammon";
|
import {marketApi} from "@/mammon";
|
||||||
import {useStorage} from "@vueuse/core";
|
import {useStorage} from "@vueuse/core";
|
||||||
import {ref, watch} from 'vue';
|
import {ref, watch} from 'vue';
|
||||||
|
|
||||||
const buyModal = ref<typeof BuyModal>();
|
|
||||||
|
|
||||||
const marketTaxStore = useMarketTaxStore();
|
const marketTaxStore = useMarketTaxStore();
|
||||||
const days = useStorage('market-scan-days', 365);
|
const days = useStorage('market-scan-days', 365);
|
||||||
const items = ref<ScanResult[]>([]);
|
const items = ref<ScanResult[]>([]);
|
||||||
@@ -51,8 +48,7 @@ watch([days, () => marketTaxStore.brokerFee, () => marketTaxStore.scc], scan, {
|
|||||||
<span>Scanning market…</span>
|
<span>Scanning market…</span>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ScanResultTable :items="items" @buy="(type, buy, sell) => buyModal?.open(type, { 'Buy': buy, 'Sell': sell })" />
|
<ScanResultTable :items="items" />
|
||||||
<BuyModal ref="buyModal" />
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ClipboardButton} from '@/components';
|
import {ClipboardButton} from '@/components';
|
||||||
import {getMarketType, MarketType, MarketTypeInput, useApraisalStore, useMarketTaxStore} from "@/market";
|
import {getMarketType, getPrice, MarketType, MarketTypeInput, useMarketTaxStore} from "@/market";
|
||||||
import {AcquisitionResultTable, BuyModal, useAcquiredTypesStore} from '@/market/acquisition';
|
import {AcquisitionResultTable, BuyModal} from '@/market/acquisition';
|
||||||
|
import {MarketLocationInput, useMarketLocationStore} from '@/market/location';
|
||||||
import {ScanResultTable, toScanResult} from '@/market/scan';
|
import {ScanResultTable, toScanResult} from '@/market/scan';
|
||||||
import {marketApi} from "@/mammon";
|
import {acquisitionApi, marketApi} from "@/mammon";
|
||||||
import {ShoppingCartIcon} from '@heroicons/vue/24/outline';
|
import {ShoppingCartIcon} from '@heroicons/vue/24/outline';
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import {computed, ref, watch} from "vue";
|
import {ref, watch} from "vue";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {routeNames} from "@/routes";
|
import {routeNames} from "@/routes";
|
||||||
import {computedAsync, useStorage} from "@vueuse/core";
|
import {computedAsync, useStorage} from "@vueuse/core";
|
||||||
@@ -18,10 +19,10 @@ const router = useRouter();
|
|||||||
const item = ref<MarketType>();
|
const item = ref<MarketType>();
|
||||||
const inputItem = ref<MarketType>();
|
const inputItem = ref<MarketType>();
|
||||||
|
|
||||||
const apraisalStore = useApraisalStore();
|
|
||||||
const marketTaxStore = useMarketTaxStore();
|
const marketTaxStore = useMarketTaxStore();
|
||||||
|
const marketLocationStore = useMarketLocationStore();
|
||||||
const days = useStorage('market-scan-days', 365);
|
const days = useStorage('market-scan-days', 365);
|
||||||
const price = computedAsync(() => item.value ? apraisalStore.getPrice(item.value) : undefined);
|
const price = computedAsync(() => item.value ? getPrice(item.value, marketLocationStore.location.id) : undefined);
|
||||||
const result = computedAsync(async () => {
|
const result = computedAsync(async () => {
|
||||||
if (!item.value) {
|
if (!item.value) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -36,20 +37,24 @@ const result = computedAsync(async () => {
|
|||||||
|
|
||||||
return toScanResult(data, item.value);
|
return toScanResult(data, item.value);
|
||||||
});
|
});
|
||||||
const acquiredTypesStore = useAcquiredTypesStore();
|
|
||||||
|
|
||||||
const acquisitions = computed(() => {
|
const acquisitions = computedAsync(async () => {
|
||||||
const p = price.value;
|
const p = price.value;
|
||||||
|
|
||||||
return !p ?[] : acquiredTypesStore.acquiredTypes
|
return !p ? [] : (await acquisitionApi.findAllAcquisitions(undefined, item.value?.id, true)).data
|
||||||
.filter(t => t.type === item.value?.id)
|
.map(a => ({
|
||||||
.map(i => ({
|
id: a.acquisitionId,
|
||||||
...i,
|
ledgerId: a.ledgerId,
|
||||||
type: p.type,
|
quantity: a.quantity,
|
||||||
buy: p.buy,
|
remaining: a.remaining,
|
||||||
sell: p.sell
|
price: a.unitCost,
|
||||||
|
date: new Date(a.datetime),
|
||||||
|
source: a.source,
|
||||||
|
type: p.type,
|
||||||
|
buy: p.buy,
|
||||||
|
sell: p.sell
|
||||||
}));
|
}));
|
||||||
});
|
}, []);
|
||||||
const view = () => {
|
const view = () => {
|
||||||
if (!inputItem.value) {
|
if (!inputItem.value) {
|
||||||
return;
|
return;
|
||||||
@@ -80,10 +85,11 @@ watch(useRoute(), async route => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid mb-2 mt-4">
|
<div class="grid mb-2 mt-4">
|
||||||
<div class="w-auto flex">
|
<div class="w-auto flex items-center">
|
||||||
<span>Item: </span>
|
<span>Item: </span>
|
||||||
<MarketTypeInput class="ms-2" v-model="inputItem" @submit="view"/>
|
<MarketTypeInput class="ms-2" v-model="inputItem" @submit="view"/>
|
||||||
<button class="justify-self-end ms-2" @click="view">View</button>
|
<button class="justify-self-end ms-2" @click="view">View</button>
|
||||||
|
<MarketLocationInput class="ms-auto" v-model="marketLocationStore.location" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="item">
|
<template v-if="item">
|
||||||
@@ -103,11 +109,11 @@ watch(useRoute(), async route => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="result" class="mb-4">
|
<div v-if="result" class="mb-4">
|
||||||
<span>Market Info:</span>
|
<span>Market Info:</span>
|
||||||
<ScanResultTable :items="[result]" infoOnly :ignoredColums="['name', 'acquisitions']" />
|
<ScanResultTable :items="[result]" infoOnly :ignoredColumns="['name', 'acquisitions']" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="acquisitions && acquisitions.length > 0">
|
<div v-if="acquisitions && acquisitions.length > 0">
|
||||||
<span>Acquisitions:</span>
|
<span>Acquisitions:</span>
|
||||||
<AcquisitionResultTable :items="acquisitions" infoOnly showAll :ignoredColums="['name', 'buy', 'sell']" defaultSortKey="date"/>
|
<AcquisitionResultTable :items="acquisitions" infoOnly showAll :ignoredColumns="['name', 'buy', 'sell']" defaultSortKey="date"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<BuyModal ref="buyModal" />
|
<BuyModal ref="buyModal" />
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {Character, CharacterLabel, useCharactersStore} from "@/characters";
|
|
||||||
import {useRoute} from "vue-router";
|
|
||||||
import {computed, ref, watch, watchEffect} from "vue";
|
|
||||||
import log from "loglevel";
|
|
||||||
import {
|
|
||||||
RuleBook,
|
|
||||||
useCharacterRuleBooksStore,
|
|
||||||
useRuleBooksStore
|
|
||||||
} from "@/rules";
|
|
||||||
import {isMain, Ledger, LedgerSelect, systemLedger, useLedgersStore} from "@/ledger";
|
|
||||||
|
|
||||||
type Bindings = { [key: string]: Ledger; };
|
|
||||||
|
|
||||||
const ruleBookStore = useRuleBooksStore();
|
|
||||||
const characterRuleBooksStore = useCharacterRuleBooksStore();
|
|
||||||
const charactersStore = useCharactersStore();
|
|
||||||
const ledgersStore = useLedgersStore();
|
|
||||||
|
|
||||||
const ledgersToUse = computed(() => [systemLedger, ...ledgersStore.ledgers.filter(isMain)]);
|
|
||||||
|
|
||||||
const character = ref<Character>();
|
|
||||||
const ruleBook = ref<RuleBook>();
|
|
||||||
const bindings = ref<Bindings>({});
|
|
||||||
const ledgerRefs = computed<string[]>(() => ruleBook.value?.ledgerRefs ?? [])
|
|
||||||
|
|
||||||
watchEffect(async () => {
|
|
||||||
const characterId = character.value?.characterId;
|
|
||||||
|
|
||||||
if (characterId) {
|
|
||||||
const characterRuleBook = characterRuleBooksStore.findByCharacterId(characterId);
|
|
||||||
|
|
||||||
ruleBook.value = ruleBookStore.findById(characterRuleBook?.ruleBook.ruleBookId ?? '');
|
|
||||||
bindings.value = Object.fromEntries(
|
|
||||||
Object.entries(characterRuleBook?.bindings ?? {})
|
|
||||||
.map(([key, id]) => [key, ledgersToUse.value.find(l => l.ledgerId === id) ?? systemLedger])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const save = () => {
|
|
||||||
const characterId = character.value?.characterId;
|
|
||||||
const ruleBookId = ruleBook.value?.ruleBookId;
|
|
||||||
|
|
||||||
if (characterId && ruleBookId) {
|
|
||||||
characterRuleBooksStore.setForCharacter(characterId, {
|
|
||||||
ruleBookId,
|
|
||||||
bindings: Object.fromEntries(
|
|
||||||
Object.entries(bindings.value)
|
|
||||||
.map(([key, ledger]) => [key, ledger.ledgerId])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(useRoute(), async route => {
|
|
||||||
if (route.params.characterId) {
|
|
||||||
const id = parseInt(typeof route.params.characterId === 'string' ? route.params.characterId : route.params.characterId[0]);
|
|
||||||
|
|
||||||
character.value = await charactersStore.findById(id);
|
|
||||||
log.info('Loaded character:', character.value);
|
|
||||||
} else {
|
|
||||||
character.value = undefined;
|
|
||||||
log.info('No character to load');
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="character" class="grid mb-2 mt-4">
|
|
||||||
<div class="mb-2 border-b-1 flex">
|
|
||||||
<CharacterLabel class="flex grow mb-2" :character="character" :size="64" />
|
|
||||||
<div>
|
|
||||||
<button @click="save">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-col border-b-1">
|
|
||||||
Rule Book:
|
|
||||||
<select class="me-2 mb-2 w-50" v-model="ruleBook">
|
|
||||||
<option v-for="rb in ruleBookStore.ruleBooks" :key="rb.ruleBookId" :value="rb">{{ rb.name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex-col border-b-1">
|
|
||||||
Ledger Bindings:
|
|
||||||
<div class="flex flex-wrap items-center mb-2 mt-2">
|
|
||||||
<div class="me-2" v-for="ref in ledgerRefs" :ref="ref">
|
|
||||||
<span class="me-1">{{ref}}:</span>
|
|
||||||
<LedgerSelect :ledgers="ledgersToUse" :modelValue="bindings[ref] ?? systemLedger" @update:modelValue="value => { if (value) bindings[ref] = value }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,119 +1,244 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {computed, ref, watchEffect} from "vue";
|
||||||
import {ref, watch} from "vue";
|
import {useEventListener} from "@vueuse/core";
|
||||||
import {useDebounceFn, useEventListener} from "@vueuse/core";
|
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import {ScriptEditor, useRuleBooksStore} from "@/rules";
|
import {ScriptEditor, useRuleBookStore} from "@/rules";
|
||||||
import {PlusIcon, TrashIcon} from "@heroicons/vue/24/outline";
|
import {PlusIcon, TrashIcon} from "@heroicons/vue/24/outline";
|
||||||
import {routeNames} from "@/routes";
|
import {Dropdown} from "@/components";
|
||||||
import {SliderCheckbox} from "@/components";
|
import {isMain, Ledger, LedgerSelect, systemLedger, useLedgersStore} from "@/ledger";
|
||||||
|
import {useCharactersStore} from "@/characters";
|
||||||
|
import {CorporationLabel, useCorporationsStore} from "@/corporations";
|
||||||
|
import {RuleScriptRequest} from "@/generated/mammon";
|
||||||
|
import {toast} from "@/toast";
|
||||||
|
import {confirm} from "@/confirm";
|
||||||
|
|
||||||
const ruleBookId = ref<string>();
|
type LedgerScopeEntry = { ref: string; ledger: Ledger };
|
||||||
const name = ref<string>('');
|
type CharacterScopeEntry = { ref: string; characterId: number };
|
||||||
const usedForAcquisitions = ref<boolean>(false);
|
type CorporationScopeEntry = { ref: string; corporationId: number };
|
||||||
const ledgerRefs = ref<string[]>([]);
|
type WalletDivisionScopeEntry = { ref: string; corporationId: number; division: number };
|
||||||
const script = ref<string>('');
|
|
||||||
|
|
||||||
const ruleBooksStore = useRuleBooksStore();
|
interface ScriptDraft {
|
||||||
const router = useRouter();
|
name: string;
|
||||||
|
script: string;
|
||||||
const save = async () => {
|
ledgers: LedgerScopeEntry[];
|
||||||
if (!ruleBookId.value) {
|
characters: CharacterScopeEntry[];
|
||||||
const created = await ruleBooksStore.create({
|
corporations: CorporationScopeEntry[];
|
||||||
name: name.value,
|
walletDivisions: WalletDivisionScopeEntry[];
|
||||||
usedForAcquisitions: usedForAcquisitions.value,
|
|
||||||
ledgerRefs: ledgerRefs.value,
|
|
||||||
script: script.value
|
|
||||||
})
|
|
||||||
await router.push({ name: routeNames.editRuleBook, params: {ruleBookId: created.ruleBookId}})
|
|
||||||
|
|
||||||
} else {
|
|
||||||
await ruleBooksStore.update(ruleBookId.value, {
|
|
||||||
name: name.value,
|
|
||||||
usedForAcquisitions: usedForAcquisitions.value,
|
|
||||||
ledgerRefs: ledgerRefs.value,
|
|
||||||
script: script.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ruleBookStore = useRuleBookStore();
|
||||||
|
const ledgersStore = useLedgersStore();
|
||||||
|
const charactersStore = useCharactersStore();
|
||||||
|
const corporationsStore = useCorporationsStore();
|
||||||
|
|
||||||
|
const ledgersToUse = computed(() => [systemLedger, ...ledgersStore.ledgers.filter(isMain)]);
|
||||||
|
|
||||||
|
const scripts = ref<ScriptDraft[]>([]);
|
||||||
|
const selectedIndex = ref(0);
|
||||||
|
const scopesExpanded = ref(true);
|
||||||
|
const ledgersExpanded = ref(true);
|
||||||
|
const charactersExpanded = ref(true);
|
||||||
|
const corporationsExpanded = ref(true);
|
||||||
|
const walletDivisionsExpanded = ref(true);
|
||||||
|
const selected = computed<ScriptDraft | undefined>(() => scripts.value[selectedIndex.value]);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const ruleBook = ruleBookStore.ruleBook;
|
||||||
|
|
||||||
|
scripts.value = (ruleBook?.scripts ?? []).map(script => ({
|
||||||
|
name: script.name,
|
||||||
|
script: script.script,
|
||||||
|
ledgers: Object.entries(script.scope.ledgers).map(([ref, id]) => ({ref, ledger: ledgersToUse.value.find(l => l.ledgerId === id) ?? systemLedger})),
|
||||||
|
characters: Object.entries(script.scope.characters).map(([ref, characterId]) => ({ref, characterId})),
|
||||||
|
corporations: Object.entries(script.scope.corporations).map(([ref, corporationId]) => ({ref, corporationId})),
|
||||||
|
walletDivisions: Object.entries(script.scope.walletDivisions).map(([ref, target]) => ({ref, corporationId: target.corporationId, division: target.division})),
|
||||||
|
}));
|
||||||
|
selectedIndex.value = 0;
|
||||||
|
log.info('Loaded rule book:', ruleBook);
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorScope = computed(() => selected.value ? {
|
||||||
|
ledgers: selected.value.ledgers.map(entry => entry.ref),
|
||||||
|
characters: selected.value.characters.map(entry => entry.ref),
|
||||||
|
corporations: selected.value.corporations.map(entry => entry.ref),
|
||||||
|
walletDivisions: selected.value.walletDivisions.map(entry => entry.ref),
|
||||||
|
} : undefined);
|
||||||
|
|
||||||
|
const scriptText = computed<string>({
|
||||||
|
get: () => selected.value?.script ?? '',
|
||||||
|
set: value => {
|
||||||
|
if (selected.value) {
|
||||||
|
selected.value.script = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const addScript = () => {
|
||||||
|
scripts.value = [...scripts.value, {name: '', script: '', ledgers: [], characters: [], corporations: [], walletDivisions: []}];
|
||||||
|
selectedIndex.value = scripts.value.length - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeScript = async (index: number) => {
|
||||||
|
const script = scripts.value[index];
|
||||||
|
|
||||||
|
if (script?.script.trim() && !await confirm({message: `Delete script "${script.name || '(unnamed)'}"? It still has content.`, danger: true})) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scripts.value = scripts.value.toSpliced(index, 1);
|
||||||
|
if (selectedIndex.value >= scripts.value.length) {
|
||||||
|
selectedIndex.value = Math.max(0, scripts.value.length - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addLedgerScope = () => selected.value?.ledgers.push({ref: '', ledger: systemLedger});
|
||||||
|
const removeLedgerScope = (index: number) => { if (selected.value) selected.value.ledgers = selected.value.ledgers.toSpliced(index, 1); };
|
||||||
|
|
||||||
|
const addCharacterScope = () => selected.value?.characters.push({ref: '', characterId: charactersStore.characters[0]?.characterId ?? 0});
|
||||||
|
const removeCharacterScope = (index: number) => { if (selected.value) selected.value.characters = selected.value.characters.toSpliced(index, 1); };
|
||||||
|
|
||||||
|
const addCorporationScope = () => selected.value?.corporations.push({ref: '', corporationId: 0});
|
||||||
|
const removeCorporationScope = (index: number) => { if (selected.value) selected.value.corporations = selected.value.corporations.toSpliced(index, 1); };
|
||||||
|
|
||||||
|
const addWalletDivisionScope = () => selected.value?.walletDivisions.push({ref: '', corporationId: 0, division: 1});
|
||||||
|
const removeWalletDivisionScope = (index: number) => { if (selected.value) selected.value.walletDivisions = selected.value.walletDivisions.toSpliced(index, 1); };
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const corporationIds = new Set([
|
||||||
|
...(selected.value?.corporations.map(entry => entry.corporationId) ?? []),
|
||||||
|
...(selected.value?.walletDivisions.map(entry => entry.corporationId) ?? []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const corporationId of corporationIds) {
|
||||||
|
if (corporationId) {
|
||||||
|
corporationsStore.findById(corporationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const save = () => ruleBookStore.update(
|
||||||
|
scripts.value
|
||||||
|
.filter(s => s.name)
|
||||||
|
.map((s): RuleScriptRequest => ({
|
||||||
|
name: s.name,
|
||||||
|
script: s.script,
|
||||||
|
scope: {
|
||||||
|
ledgers: Object.fromEntries(s.ledgers.filter(entry => entry.ref).map(entry => [entry.ref, entry.ledger.ledgerId])),
|
||||||
|
characters: Object.fromEntries(s.characters.filter(entry => entry.ref).map(entry => [entry.ref, entry.characterId])),
|
||||||
|
corporations: Object.fromEntries(s.corporations.filter(entry => entry.ref).map(entry => [entry.ref, entry.corporationId])),
|
||||||
|
walletDivisions: Object.fromEntries(s.walletDivisions.filter(entry => entry.ref).map(entry => [entry.ref, {corporationId: entry.corporationId, division: entry.division}])),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.then(() => toast.success('Rule book saved'));
|
||||||
|
|
||||||
useEventListener(window, 'keydown', (event: KeyboardEvent) => {
|
useEventListener(window, 'keydown', (event: KeyboardEvent) => {
|
||||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
}, {capture: true});
|
}, {capture: true});
|
||||||
|
|
||||||
const addLedgerRef = () => {
|
|
||||||
ledgerRefs.value = [...ledgerRefs.value, '']
|
|
||||||
}
|
|
||||||
const updateLedgerRef = useDebounceFn((index: number, value: string) => {
|
|
||||||
ledgerRefs.value[index] = value;
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
const removeLedgerRef = (index: number) => {
|
|
||||||
ledgerRefs.value = ledgerRefs.value.toSpliced(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(useRoute(), async route => {
|
|
||||||
if (route.params.ruleBookId) {
|
|
||||||
const promise = ruleBooksStore.refresh(); // FIXME don't call refresh
|
|
||||||
|
|
||||||
const id = typeof route.params.ruleBookId === 'string' ? route.params.ruleBookId : route.params.ruleBookId[0];
|
|
||||||
|
|
||||||
await promise;
|
|
||||||
|
|
||||||
const ruleBook = ruleBooksStore.findById(id);
|
|
||||||
|
|
||||||
ruleBookId.value = id;
|
|
||||||
name.value = ruleBook?.name ?? '';
|
|
||||||
usedForAcquisitions.value = ruleBook?.usedForAcquisitions ?? false;
|
|
||||||
ledgerRefs.value = [...(ruleBook?.ledgerRefs ?? [])];
|
|
||||||
script.value = ruleBook?.script ?? '';
|
|
||||||
log.info('Loaded rule book:', ruleBook);
|
|
||||||
} else {
|
|
||||||
ruleBookId.value = undefined;
|
|
||||||
name.value = '';
|
|
||||||
usedForAcquisitions.value = false;
|
|
||||||
ledgerRefs.value = [];
|
|
||||||
script.value = '';
|
|
||||||
log.info('No rule book to load');
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col mb-2 mt-4 h-[calc(100vh-4.5rem)]">
|
<div class="flex mb-2 mt-4 h-[calc(100vh-1.5rem)]">
|
||||||
<div class="flex flex-col grow min-h-0">
|
<div class="flex flex-col me-4 w-48 shrink-0">
|
||||||
<div class="flex grow border-b-1">
|
<div class="grow overflow-y-auto">
|
||||||
Name:
|
<div v-for="(script, index) in scripts" :key="index"
|
||||||
<input class="mb-2 ms-2" type="text" v-model="name" />
|
class="flex items-center mb-1 px-1 py-0.5 border-l-4 cursor-pointer"
|
||||||
<label class="flex items-center ms-2 mb-2">
|
:class="index === selectedIndex ? 'border-emerald-500' : 'border-transparent'"
|
||||||
<SliderCheckbox class="me-2" v-model="usedForAcquisitions" />
|
@click="selectedIndex = index">
|
||||||
Used for acquisitions
|
<span class="grow truncate">{{ script.name || '(unnamed)' }}</span>
|
||||||
</label>
|
<button class="btn-icon text-amber-700 hover:text-amber-600" @click.stop="removeScript(index)"><TrashIcon /></button>
|
||||||
</div>
|
|
||||||
<div class="border-b-1">
|
|
||||||
Ledgers References:
|
|
||||||
<div class="flex flex-wrap items-center mt-2">
|
|
||||||
<div class="flex items-center mb-2" v-for="(ledgerRef, index) in ledgerRefs" :key="index">
|
|
||||||
<input class="me-1" type="text" :value="ledgerRef" @input="updateLedgerRef(index, ($event.target as HTMLInputElement).value)" />
|
|
||||||
<button class="btn-icon me-2" @click="removeLedgerRef(index)"><TrashIcon /></button>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<button class="btn-icon" @click="addLedgerRef"><PlusIcon /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button class="btn-icon flex items-center" @click="addScript"><PlusIcon /> Add script</button>
|
||||||
<div class="flex flex-col grow min-h-0 border-b-1">
|
|
||||||
Script:
|
|
||||||
<ScriptEditor class="mt-2 mb-2" v-model="script" :ledgerRefs="ledgerRefs" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 justify-end flex">
|
|
||||||
<div>
|
<div v-if="selected" class="flex flex-col grow min-h-0">
|
||||||
|
<div class="border-b-1 mb-2 pb-2">
|
||||||
|
Name: <input class="me-1" type="text" v-model="selected.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dropdown inline :auto-close="false" v-model:open="scopesExpanded" class="mb-2">
|
||||||
|
<template #button>Scopes</template>
|
||||||
|
<Dropdown inline :auto-close="false" v-model:open="ledgersExpanded" class="ms-4 mt-2">
|
||||||
|
<template #button>Ledgers</template>
|
||||||
|
<div class="flex flex-wrap items-center mt-2">
|
||||||
|
<div class="flex items-center mb-2 me-2" v-for="(entry, index) in selected.ledgers" :key="index">
|
||||||
|
<input class="me-1" type="text" v-model="entry.ref" />
|
||||||
|
<span class="me-1">:</span>
|
||||||
|
<LedgerSelect class="me-1" :ledgers="ledgersToUse" v-model="entry.ledger" />
|
||||||
|
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="removeLedgerScope(index)"><TrashIcon /></button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<button class="btn-icon" @click="addLedgerScope"><PlusIcon /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<Dropdown inline :auto-close="false" v-model:open="charactersExpanded" class="ms-4 mt-2">
|
||||||
|
<template #button>Characters</template>
|
||||||
|
<div class="flex flex-wrap items-center mt-2">
|
||||||
|
<div class="flex items-center mb-2 me-2" v-for="(entry, index) in selected.characters" :key="index">
|
||||||
|
<input class="me-1" type="text" v-model="entry.ref" />
|
||||||
|
<span class="me-1">:</span>
|
||||||
|
<select class="me-1" v-model="entry.characterId">
|
||||||
|
<option v-for="character in charactersStore.characters" :key="character.characterId" :value="character.characterId">{{ character.name }}</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="removeCharacterScope(index)"><TrashIcon /></button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<button class="btn-icon" @click="addCharacterScope"><PlusIcon /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<Dropdown inline :auto-close="false" v-model:open="corporationsExpanded" class="ms-4 mt-2">
|
||||||
|
<template #button>Corporations</template>
|
||||||
|
<div class="flex flex-wrap items-center mt-2">
|
||||||
|
<div class="flex items-center mb-2 me-2" v-for="(entry, index) in selected.corporations" :key="index">
|
||||||
|
<input class="me-1" type="text" v-model="entry.ref" />
|
||||||
|
<span class="me-1">:</span>
|
||||||
|
<input class="me-1 w-32" type="number" v-model.number="entry.corporationId" placeholder="Corporation ID" />
|
||||||
|
<CorporationLabel class="me-1" :corporation-id="entry.corporationId" :corporation="corporationsStore.corporations[entry.corporationId]" :size="16" v-if="entry.corporationId" />
|
||||||
|
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="removeCorporationScope(index)"><TrashIcon /></button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<button class="btn-icon" @click="addCorporationScope"><PlusIcon /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<Dropdown inline :auto-close="false" v-model:open="walletDivisionsExpanded" class="ms-4 mt-2 mb-2">
|
||||||
|
<template #button>Wallet Divisions</template>
|
||||||
|
<div class="flex flex-wrap items-center mt-2">
|
||||||
|
<div class="flex items-center mb-2 me-2" v-for="(entry, index) in selected.walletDivisions" :key="index">
|
||||||
|
<input class="me-1" type="text" v-model="entry.ref" />
|
||||||
|
<span class="me-1">:</span>
|
||||||
|
<input class="me-1 w-32" type="number" v-model.number="entry.corporationId" placeholder="Corporation ID" />
|
||||||
|
<input class="me-1 w-16" type="number" min="1" max="7" v-model.number="entry.division" placeholder="Division" />
|
||||||
|
<CorporationLabel class="me-1" :corporation-id="entry.corporationId" :corporation="corporationsStore.corporations[entry.corporationId]" :division="entry.division" :size="16" v-if="entry.corporationId" />
|
||||||
|
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="removeWalletDivisionScope(index)"><TrashIcon /></button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<button class="btn-icon" @click="addWalletDivisionScope"><PlusIcon /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<div class="flex flex-col grow min-h-0 border-b-1">
|
||||||
|
Script:
|
||||||
|
<ScriptEditor class="mt-2 mb-2" v-model="scriptText" :scope="editorScope" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 justify-end flex">
|
||||||
<button @click="save">Save</button>
|
<button @click="save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="flex grow items-center justify-center text-neutral-500">
|
||||||
|
No scripts yet
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {Character, CharacterLabel} from "@/characters";
|
|
||||||
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
|
|
||||||
import {CharacterRuleBook, useCharacterRuleBooksStore} from "@/rules";
|
|
||||||
import {routeNames} from "@/routes.ts";
|
|
||||||
import {SortableHeader, useSort} from "@/components/table";
|
|
||||||
|
|
||||||
type CharacterRuleBookView = {
|
|
||||||
character: Character;
|
|
||||||
characterName: string;
|
|
||||||
ruleBookName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const characterRuleBooksStore = useCharacterRuleBooksStore();
|
|
||||||
|
|
||||||
const { sortedArray, headerProps } = useSort<CharacterRuleBookView>(() => characterRuleBooksStore.characterRuleBooks.map((characterRuleBook: CharacterRuleBook): CharacterRuleBookView => ({
|
|
||||||
character: characterRuleBook.character,
|
|
||||||
characterName: characterRuleBook.character.name,
|
|
||||||
ruleBookName: characterRuleBook.ruleBook.name
|
|
||||||
})))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="grid mb-2 mt-4">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<SortableHeader v-bind="headerProps" sortKey="characterName">Character</SortableHeader>
|
|
||||||
<SortableHeader v-bind="headerProps" sortKey="ruleBookName">Rule Book</SortableHeader>
|
|
||||||
<SortableHeader v-bind="headerProps" sortKey="buttons" unsortable />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="characterRuleBookView in sortedArray" :key="characterRuleBookView.character.characterId" >
|
|
||||||
<td>
|
|
||||||
<CharacterLabel :character="characterRuleBookView.character" />
|
|
||||||
</td>
|
|
||||||
<td>{{characterRuleBookView.ruleBookName}}</td>
|
|
||||||
<td class="text-right">
|
|
||||||
<RouterLink class="btn-icon" :to="{ name: routeNames.editCharacterRulebook, params: { characterId: characterRuleBookView.character.characterId } }"><PencilSquareIcon /></RouterLink>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {DocumentDuplicateIcon, PencilSquareIcon, TrashIcon} from "@heroicons/vue/24/outline";
|
|
||||||
import {confirm} from "@/confirm";
|
|
||||||
import {RuleBook, useRuleBooksStore} from "@/rules";
|
|
||||||
import {routeNames} from "@/routes";
|
|
||||||
|
|
||||||
const ruleBooksStore = useRuleBooksStore();
|
|
||||||
|
|
||||||
const duplicate = async (ruleBook: RuleBook) => {
|
|
||||||
if (await confirm({title: "Duplicate Rule Book", message: `Duplicate ${ruleBook.name}?`, confirmLabel: "Duplicate"})) {
|
|
||||||
await ruleBooksStore.duplicate(ruleBook);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const remove = async (ruleBook: RuleBook) => {
|
|
||||||
if (await confirm({title: "Delete Rule Book", message: `Delete ${ruleBook.name}?`, confirmLabel: "Delete", danger: true})) {
|
|
||||||
await ruleBooksStore.remove(ruleBook.ruleBookId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="grid mb-2 mt-4">
|
|
||||||
<div class="flex justify-end border-b-1">
|
|
||||||
<RouterLink class="button mb-2 ms-2" :to="{ name: routeNames.newRuleBook}">New Rule Book</RouterLink>
|
|
||||||
</div>
|
|
||||||
<div v-for="ruleBook in ruleBooksStore.ruleBooks" :key="ruleBook.ruleBookId" class="flex items-center mt-2">
|
|
||||||
<span class="flex grow me-2">{{ruleBook.name}}</span>
|
|
||||||
<RouterLink class="btn-icon me-1" :to="{ name: routeNames.editRuleBook, params: { ruleBookId: ruleBook.ruleBookId } }"><PencilSquareIcon /></RouterLink>
|
|
||||||
<button class="btn-icon me-1" @click="duplicate(ruleBook)"><DocumentDuplicateIcon /></button>
|
|
||||||
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="remove(ruleBook)"><TrashIcon /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { evepraisalAxiosInstance } from '@/market/appraisal/evepraisal';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelValue = defineModel({ default: '' });
|
|
||||||
defineProps<Props>();
|
|
||||||
|
|
||||||
const loadFromId = async (e: Event) => {
|
|
||||||
const input = e.target as HTMLInputElement;
|
|
||||||
const id = input.value;
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const response = await evepraisalAxiosInstance.get(`/a/${id}.json`);
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
modelValue.value = JSON.stringify(response.data);
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex-1 mx-1">
|
|
||||||
<span>{{ name }}</span><input type="text" class="ms-2" @change="loadFromId" placeholder="id evepraisal" />
|
|
||||||
<textarea class="mt-1" v-model="modelValue" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SortableHeader, useSort, VirtualScrollTable } from '@/components/table';
|
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||||
import { formatIsk, percentFormater } from '@/formaters';
|
import {formatIsk, percentFormater} from '@/formaters';
|
||||||
import { MarketTypeLabel } from '@/market/type';
|
import {MarketTypeLabel} from '@/market/type';
|
||||||
import { useStorage } from '@vueuse/core';
|
import {useStorage} from '@vueuse/core';
|
||||||
import { computed } from 'vue';
|
import {computed} from 'vue';
|
||||||
import BuySellSlider from './BuySellSlider.vue';
|
import BuySellSlider from './BuySellSlider.vue';
|
||||||
import { ReprocessItemValues } from './reprocess';
|
import {ReprocessItemValues} from './reprocess';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
result?: ReprocessItemValues[];
|
result?: ReprocessItemValues[];
|
||||||
@@ -26,8 +26,8 @@ const { sortedArray, headerProps } = useSort(computed(() => props.result.map(r =
|
|||||||
|
|
||||||
return { ...r, market, materials, ratio };
|
return { ...r, market, materials, ratio };
|
||||||
})), {
|
})), {
|
||||||
defaultSortKey: 'name',
|
defaultSortKey: 'ratio',
|
||||||
defaultSortDirection: 'asc'
|
defaultSortDirection: 'desc'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export * from './reprocess';
|
export * from './reprocess';
|
||||||
|
|
||||||
export { default as ReprocessInput } from './ReprocessInput.vue';
|
|
||||||
export { default as ReprocessResultTable } from './ReprocessResultTable.vue';
|
export { default as ReprocessResultTable } from './ReprocessResultTable.vue';
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -1,3 +1,6 @@
|
|||||||
|
import { reprocessingApi } from '@/mammon';
|
||||||
|
import { getMarketTypes } from '@/market';
|
||||||
|
|
||||||
export type ReprocessItemValues = {
|
export type ReprocessItemValues = {
|
||||||
typeID: number;
|
typeID: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -7,18 +10,21 @@ export type ReprocessItemValues = {
|
|||||||
sell_reprocess: number;
|
sell_reprocess: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reprocess = async (items: string, minerals: string, efficiency?: number): Promise<ReprocessItemValues[]> => {
|
export const reprocess = async (characterId?: number, items?: string, locationId?: number): Promise<ReprocessItemValues[]> => {
|
||||||
if (!items || !minerals || (efficiency && (efficiency < 0 || efficiency > 1))) {
|
if (!characterId || !items?.trim()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsJson = JSON.parse(items);
|
const results = await reprocessingApi.reprocessRawItems(characterId, items, locationId).then(r => r.data);
|
||||||
const mineralsJson = JSON.parse(minerals);
|
const names = await getMarketTypes(results.map(r => r.input.marketTypeId))
|
||||||
const sourceJson = {
|
.then(types => new Map(types.map(t => [t.id, t.name])));
|
||||||
"ep_items": itemsJson,
|
|
||||||
"ep_mat": mineralsJson
|
|
||||||
};
|
|
||||||
const source = JSON.stringify(sourceJson);
|
|
||||||
|
|
||||||
return []
|
return results.map(({ input, output }) => ({
|
||||||
};
|
typeID: input.marketTypeId,
|
||||||
|
name: names.get(input.marketTypeId) ?? '',
|
||||||
|
buy: input.buy,
|
||||||
|
sell: input.sell,
|
||||||
|
buy_reprocess: output.reduce((sum, o) => sum + o.buy, 0),
|
||||||
|
sell_reprocess: output.reduce((sum, o) => sum + o.sell, 0),
|
||||||
|
}));
|
||||||
|
};
|
||||||
+18
-18
@@ -1,22 +1,29 @@
|
|||||||
import {RouteRecordRaw} from 'vue-router';
|
import {RouteRecordRaw} from 'vue-router';
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta {
|
||||||
|
public?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const routeNames = {
|
export const routeNames = {
|
||||||
home: 'home',
|
home: 'home',
|
||||||
callback: 'callback',
|
callback: 'callback',
|
||||||
viewLedger: 'view-ledger',
|
viewLedger: 'view-ledger',
|
||||||
viewLedgerBalance: 'view-ledger-balance',
|
viewLedgerBalance: 'view-ledger-balance',
|
||||||
listLedgerTransactions: 'list-ledger-transactions',
|
listLedgerTransactions: 'list-ledger-transactions',
|
||||||
listRuleBooks: 'list-rule-books',
|
listLedgerAcquisitions: 'list-ledger-acquisitions',
|
||||||
newRuleBook: 'new-rule-book',
|
viewLedgerStatistics: 'view-ledger-statistics',
|
||||||
editRuleBook: 'edit-rule-book',
|
editRuleBook: 'edit-rule-book',
|
||||||
editCharacterRulebook: 'edit-character-rule-book',
|
|
||||||
marketTypes: 'market-types',
|
marketTypes: 'market-types',
|
||||||
|
appraise: 'appraise',
|
||||||
|
compareAppraisal: 'compare-appraisal',
|
||||||
about: 'about',
|
about: 'about',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const routes: RouteRecordRaw[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
{path: '/', name: routeNames.home, component: () => import('@/pages/Index.vue')},
|
{path: '/', name: routeNames.home, component: () => import('@/pages/Index.vue'), meta: {public: true}},
|
||||||
{path: '/callback', name: routeNames.callback, component: () => import('@/pages/Index.vue')},
|
{path: '/callback', name: routeNames.callback, component: () => import('@/pages/Index.vue'), meta: {public: true}},
|
||||||
|
|
||||||
{path: '/ledgers', component: () => import('@/pages/Ledgers.vue'), children: [
|
{path: '/ledgers', component: () => import('@/pages/Ledgers.vue'), children: [
|
||||||
{path: '', component: () => import('@/pages/ledger/ListLedgers.vue')},
|
{path: '', component: () => import('@/pages/ledger/ListLedgers.vue')},
|
||||||
@@ -24,27 +31,20 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
{path: '', name: routeNames.viewLedger, redirect: {name: routeNames.viewLedgerBalance}},
|
{path: '', name: routeNames.viewLedger, redirect: {name: routeNames.viewLedgerBalance}},
|
||||||
{path: 'balance', name: routeNames.viewLedgerBalance, component: () => import('@/pages/ledger/ViewLedgerBalance.vue')},
|
{path: 'balance', name: routeNames.viewLedgerBalance, component: () => import('@/pages/ledger/ViewLedgerBalance.vue')},
|
||||||
{path: 'transactions', name: routeNames.listLedgerTransactions, component: () => import('@/pages/ledger/ListLedgerTransactions.vue')},
|
{path: 'transactions', name: routeNames.listLedgerTransactions, component: () => import('@/pages/ledger/ListLedgerTransactions.vue')},
|
||||||
|
{path: 'acquisitions', name: routeNames.listLedgerAcquisitions, component: () => import('@/pages/ledger/ViewLedgerAcquisitions.vue')},
|
||||||
|
{path: 'statistics', name: routeNames.viewLedgerStatistics, component: () => import('@/pages/ledger/ViewLedgerStatistics.vue')},
|
||||||
]},
|
]},
|
||||||
]},
|
]},
|
||||||
|
|
||||||
{path: '/rules', component: () => import('@/pages/Rules.vue'), children: [
|
{path: '/rules', name: routeNames.editRuleBook, component: () => import('@/pages/rules/EditRuleBook.vue')},
|
||||||
{path: '', redirect: {name: routeNames.listRuleBooks}},
|
|
||||||
{path: '/rule-books', children: [
|
|
||||||
{path: '', name: routeNames.listRuleBooks, component: () => import('@/pages/rules/ListRuleBooks.vue')},
|
|
||||||
{path: 'new', name: routeNames.newRuleBook, component: () => import('@/pages/rules/EditRuleBook.vue')},
|
|
||||||
{path: ':ruleBookId', name: routeNames.editRuleBook, component: () => import('@/pages/rules/EditRuleBook.vue')},
|
|
||||||
]},
|
|
||||||
{path: '/characters/rules', children: [
|
|
||||||
{path: '', component: () => import('@/pages/rules/ListCharacterRuleBooks.vue')},
|
|
||||||
{path: '/characters/:characterId/rules', name: routeNames.editCharacterRulebook, component: () => import('@/pages/rules/EditCharacterRuleBook.vue')},
|
|
||||||
]}
|
|
||||||
]},
|
|
||||||
|
|
||||||
{path: '/market', component: () => import('@/pages/Market.vue'), children: [
|
{path: '/market', component: () => import('@/pages/Market.vue'), children: [
|
||||||
{path: '', redirect: {name: routeNames.marketTypes}},
|
{path: '', redirect: {name: routeNames.marketTypes}},
|
||||||
{path: 'types/:type?', name: routeNames.marketTypes, component: () => import('@/pages/market/TypeInfo.vue')},
|
{path: 'types/:type?', name: routeNames.marketTypes, component: () => import('@/pages/market/TypeInfo.vue')},
|
||||||
{path: 'scan', component: () => import('@/pages/market/Scan.vue')},
|
{path: 'scan', component: () => import('@/pages/market/Scan.vue')},
|
||||||
{path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')},
|
{path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')},
|
||||||
|
{path: 'appraisal', name: routeNames.appraise, component: () => import('@/pages/market/Appraise.vue')},
|
||||||
|
{path: 'appraisal/compare', name: routeNames.compareAppraisal, component: () => import('@/pages/market/CompareAppraisal.vue')},
|
||||||
]},
|
]},
|
||||||
|
|
||||||
{path: '/reprocess', component: () => import('@/pages/Reprocess.vue')},
|
{path: '/reprocess', component: () => import('@/pages/Reprocess.vue')},
|
||||||
@@ -52,5 +52,5 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
{path: '/tools', component: () => import('@/pages/Tools.vue')},
|
{path: '/tools', component: () => import('@/pages/Tools.vue')},
|
||||||
|
|
||||||
{path: '/characters', component: () => import('@/pages/Characters.vue')},
|
{path: '/characters', component: () => import('@/pages/Characters.vue')},
|
||||||
{path: '/about', name: routeNames.about, component: () => import('@/pages/About.vue')},
|
{path: '/about', name: routeNames.about, component: () => import('@/pages/About.vue'), meta: {public: true}},
|
||||||
] as const;
|
] as const;
|
||||||
+27
-13
@@ -22,27 +22,41 @@ const loadScriptDefinitions = async () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const definitions = await fetchScriptDefinitions();
|
const definitions = await fetchScriptDefinitions();
|
||||||
monaco.typescript.javascriptDefaults.addExtraLib(definitions, 'ts:rule-runner.d.ts');
|
monaco.typescript.javascriptDefaults.addExtraLib(definitions, 'file:///node_modules/@eveal/runtime/index.d.ts');
|
||||||
extraLibLoaded = true;
|
extraLibLoaded = true;
|
||||||
} catch {
|
} catch {
|
||||||
// type definitions are optional — the editor still works without autocomplete
|
// type definitions are optional — the editor still works without autocomplete
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<{ ledgerRefs?: string[] }>();
|
export interface ScriptEditorScope {
|
||||||
|
ledgers?: string[];
|
||||||
|
characters?: string[];
|
||||||
|
corporations?: string[];
|
||||||
|
walletDivisions?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
let ledgersLib: monaco.IDisposable | undefined;
|
const props = defineProps<{ scope?: ScriptEditorScope }>();
|
||||||
|
|
||||||
const updateLedgerRefs = (refs: readonly string[]) => {
|
let scopeLib: monaco.IDisposable | undefined;
|
||||||
ledgersLib?.dispose();
|
|
||||||
|
const declareInterface = (name: string, type: string, refs: readonly string[]) => {
|
||||||
const members = refs
|
const members = refs
|
||||||
.filter(ref => ref && ref !== 'system')
|
.filter(ref => ref && ref !== 'system')
|
||||||
.map(ref => ` readonly ${JSON.stringify(ref)}: Ledger;`)
|
.map(ref => ` readonly ${JSON.stringify(ref)}: ${type};`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
ledgersLib = monaco.typescript.javascriptDefaults.addExtraLib(
|
return ` interface ${name} {\n${members}\n }\n`;
|
||||||
`declare interface Ledgers {\n${members}\n}\n`,
|
};
|
||||||
'ts:rule-runner.ledgers.d.ts'
|
|
||||||
);
|
const updateScope = (scope: ScriptEditorScope) => {
|
||||||
|
scopeLib?.dispose();
|
||||||
|
const source = `declare module '@eveal/runtime' {\n${[
|
||||||
|
declareInterface('Ledgers', 'Ledger', scope.ledgers ?? []),
|
||||||
|
declareInterface('Characters', 'Character', scope.characters ?? []),
|
||||||
|
declareInterface('Corporations', 'Corporation', scope.corporations ?? []),
|
||||||
|
declareInterface('WalletDivisions', 'WalletDivision', scope.walletDivisions ?? []),
|
||||||
|
].join('\n')}}\n`;
|
||||||
|
scopeLib = monaco.typescript.javascriptDefaults.addExtraLib(source, 'ts:rule-runner.scope.d.ts');
|
||||||
};
|
};
|
||||||
|
|
||||||
const model = defineModel<string>({default: ''});
|
const model = defineModel<string>({default: ''});
|
||||||
@@ -51,7 +65,7 @@ let editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadScriptDefinitions();
|
await loadScriptDefinitions();
|
||||||
updateLedgerRefs(props.ledgerRefs ?? []);
|
updateScope(props.scope ?? {});
|
||||||
|
|
||||||
if (!container.value) {
|
if (!container.value) {
|
||||||
return;
|
return;
|
||||||
@@ -83,11 +97,11 @@ watch(model, value => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.ledgerRefs, refs => updateLedgerRefs(refs ?? []), {deep: true});
|
watch(() => props.scope, scope => updateScope(scope ?? {}), {deep: true});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
editor?.dispose();
|
editor?.dispose();
|
||||||
ledgersLib?.dispose();
|
scopeLib?.dispose();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+31
-70
@@ -1,83 +1,44 @@
|
|||||||
import {characterRuleBookApi, ruleBookApi} from "@/mammon";
|
import {ruleBookApi, ruleScriptApi} from "@/mammon";
|
||||||
import {
|
import {RuleBookResponse, RuleScopeRequest, RuleScriptRequest, RuleScriptResponse, UpdateRuleBookRequest} from "@/generated/mammon";
|
||||||
CharacterRuleBookResponse,
|
import {isAxiosError} from "axios";
|
||||||
CreateRuleBookRequest,
|
|
||||||
RuleBookResponse,
|
|
||||||
SetCharacterRuleBookRequest
|
|
||||||
} from "@/generated/mammon";
|
|
||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {ref, triggerRef} from "vue";
|
import {ref} from "vue";
|
||||||
|
|
||||||
export type RuleBook = RuleBookResponse;
|
export type RuleBook = RuleBookResponse;
|
||||||
|
export type RuleScript = RuleScriptResponse;
|
||||||
|
export type RuleScope = RuleScriptResponse['scope'];
|
||||||
|
|
||||||
export const useRuleBooksStore = defineStore('rule-books', () => {
|
export const emptyScope = (): RuleScopeRequest => ({
|
||||||
const ruleBooks = ref<RuleBook[]>([]);
|
ledgers: {},
|
||||||
|
characters: {},
|
||||||
|
corporations: {},
|
||||||
|
walletDivisions: {},
|
||||||
|
});
|
||||||
|
|
||||||
const addRuleBook = (ruleBook: RuleBook) => {
|
export const useRuleBookStore = defineStore('rule-book', () => {
|
||||||
ruleBooks.value.push(ruleBook);
|
const ruleBook = ref<RuleBook | null>(null);
|
||||||
triggerRef(ruleBooks);
|
|
||||||
return ruleBook;
|
|
||||||
};
|
|
||||||
|
|
||||||
const replaceRuleBook = (ruleBook: RuleBook) => {
|
const refresh = () => ruleBookApi.findRuleBook()
|
||||||
const index = ruleBooks.value.findIndex(rb => rb.ruleBookId === ruleBook.ruleBookId);
|
.then(response => ruleBook.value = response.data)
|
||||||
|
.catch(error => {
|
||||||
|
if (isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
ruleBook.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
if (index !== -1) {
|
const update = (scripts: RuleScriptRequest[]) => ruleBookApi.updateRuleBook({scripts} satisfies UpdateRuleBookRequest)
|
||||||
ruleBooks.value[index] = ruleBook;
|
.then(response => ruleBook.value = response.data);
|
||||||
}
|
|
||||||
triggerRef(ruleBooks);
|
|
||||||
return ruleBook;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findById = (ruleBookId: string): RuleBook | undefined => ruleBooks.value.find(rb => rb.ruleBookId === ruleBookId);
|
|
||||||
const create = (ruleBook: CreateRuleBookRequest) => ruleBookApi.createRuleBook(ruleBook).then(response => addRuleBook(response.data));
|
|
||||||
const update = (ruleBookId: string, ruleBook: CreateRuleBookRequest) => ruleBookApi.updateRuleBook(ruleBookId, ruleBook).then(response => replaceRuleBook(response.data));
|
|
||||||
const duplicate = (ruleBook: RuleBook) => create({
|
|
||||||
name: `${ruleBook.name} (copy)`,
|
|
||||||
usedForAcquisitions: ruleBook.usedForAcquisitions,
|
|
||||||
ledgerRefs: [...ruleBook.ledgerRefs],
|
|
||||||
script: ruleBook.script,
|
|
||||||
});
|
|
||||||
|
|
||||||
const remove = (ruleBookId: string) => ruleBookApi.deleteRuleBook(ruleBookId).then(() => {
|
|
||||||
ruleBooks.value = ruleBooks.value.filter(rb => rb.ruleBookId !== ruleBookId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const refresh = () => ruleBookApi.findAllRuleBooks().then(response => ruleBooks.value = response.data);
|
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
return {ruleBooks, findById, create, update, duplicate, remove, refresh};
|
return {ruleBook, update};
|
||||||
})
|
|
||||||
|
|
||||||
export type CharacterRuleBook = CharacterRuleBookResponse;
|
|
||||||
|
|
||||||
export const useCharacterRuleBooksStore = defineStore('character-rule-books', () => {
|
|
||||||
const characterRuleBooks = ref<CharacterRuleBook[]>([]);
|
|
||||||
|
|
||||||
const replaceCharacterRuleBook = (characterRuleBook: CharacterRuleBook) => {
|
|
||||||
const index = characterRuleBooks.value.findIndex(crb => crb.character.characterId === characterRuleBook.character.characterId);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
characterRuleBooks.value[index] = characterRuleBook;
|
|
||||||
} else {
|
|
||||||
characterRuleBooks.value.push(characterRuleBook);
|
|
||||||
}
|
|
||||||
triggerRef(characterRuleBooks);
|
|
||||||
return characterRuleBook;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findByCharacterId = (characterId: number): CharacterRuleBook | undefined => characterRuleBooks.value.find(crb => crb.character.characterId === characterId);
|
|
||||||
|
|
||||||
const setForCharacter = (characterId: number, ruleBook: SetCharacterRuleBookRequest) => characterRuleBookApi.setCharacterRuleBookForCharacter(characterId, ruleBook)
|
|
||||||
.then(response => replaceCharacterRuleBook(response.data));
|
|
||||||
|
|
||||||
const refresh = () => characterRuleBookApi.findAllCharacterRuleBooks().then(response => characterRuleBooks.value = response.data);
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
|
|
||||||
return {characterRuleBooks, findByCharacterId, setForCharacter, refresh};
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const fetchScriptDefinitions = (): Promise<string> =>
|
export const fetchScriptDefinitions = (): Promise<string> =>
|
||||||
ruleBookApi.getScriptDefinitions({responseType: 'text'}).then(response => response.data);
|
ruleScriptApi.listScriptDefinitions()
|
||||||
|
.then(response => Promise.all(
|
||||||
|
response.data.map(name => ruleScriptApi.getScriptDefinition(name, {responseType: 'text'}).then(r => r.data))
|
||||||
|
))
|
||||||
|
.then(definitions => definitions.join('\n'));
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user