diff --git a/package-lock.json b/package-lock.json
index 34db8a7..7792924 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
"react-leaflet": "^5.0.0",
"react-markdown": "^9.1.0",
"react-router-dom": "^7.7.1",
+ "recharts": "^3.7.0",
"remark-gfm": "^4.0.1",
"xstate": "^5.20.1"
},
@@ -1091,6 +1092,40 @@
"react-dom": "^19.0.0"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
+ "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -1392,6 +1427,16 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1437,6 +1482,60 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -1549,6 +1648,11 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
@@ -2274,6 +2378,14 @@
"node": ">= 10.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2451,6 +2563,116 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -2468,6 +2690,11 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
+ },
"node_modules/decode-named-character-reference": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
@@ -2746,6 +2973,11 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
+ "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="
+ },
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -3018,6 +3250,11 @@
"node": ">= 0.6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="
+ },
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -3682,6 +3919,15 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -3721,6 +3967,14 @@
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
"license": "MIT"
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -5813,6 +6067,28 @@
"react": ">=18"
}
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -5870,6 +6146,45 @@
"node": ">=18"
}
},
+ "node_modules/recharts": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz",
+ "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==",
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
@@ -5946,6 +6261,11 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -6386,6 +6706,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6734,6 +7059,27 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/vite": {
"version": "5.4.19",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
diff --git a/package.json b/package.json
index e2f4809..ca11c58 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"react-leaflet": "^5.0.0",
"react-markdown": "^9.1.0",
"react-router-dom": "^7.7.1",
+ "recharts": "^3.7.0",
"remark-gfm": "^4.0.1",
"xstate": "^5.20.1"
},
diff --git a/src/App.tsx b/src/App.tsx
index 95e21b5..3b82754 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -43,14 +43,11 @@ import { GDPRPage } from './pages/GDPR';
import { FeatureViewPage } from './pages/FeatureView';
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage } from './pages/admin';
-// Workflow Pages (global)
-import { PlaygroundPage, WorkflowsPage, AutomationsPage, AutomationTemplatesPage } from './pages/workflows';
-
// Basedata Pages (global)
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
-// Migrate Pages (temporary - to be migrated to feature instances)
-import { PekPage, SpeechPage } from './pages/migrate';
+// Billing Pages
+import { BillingDashboard, BillingDataView, BillingAdmin } from './pages/billing';
function App() {
// Load saved theme preference and set app name on app mount
@@ -107,16 +104,6 @@ function App() {
} />
} />
- {/* ============================================== */}
- {/* WORKFLOWS ROUTES (global) */}
- {/* ============================================== */}
-
- } />
- } />
- } />
- } />
-
-
{/* ============================================== */}
{/* BASISDATEN ROUTES (global) */}
{/* ============================================== */}
@@ -127,11 +114,12 @@ function App() {
{/* ============================================== */}
- {/* MIGRATE TO FEATURES (temporary) */}
+ {/* BILLING ROUTES */}
{/* ============================================== */}
- } />
- } />
- } />
+
+ } />
+ } />
+
{/* ============================================== */}
{/* FEATURE-INSTANZ ROUTES */}
@@ -159,6 +147,15 @@ function App() {
} />
} />
+ {/* Chat Playground Feature Views */}
+ } />
+ } />
+
+ {/* Automation Feature Views */}
+ } />
+ } />
+ } />
+
{/* Catch-all für unbekannte Sub-Pfade */}
} />
@@ -179,6 +176,7 @@ function App() {
} />
} />
} />
+ } />
diff --git a/src/api/automationApi.ts b/src/api/automationApi.ts
index b6f0628..38d560c 100644
--- a/src/api/automationApi.ts
+++ b/src/api/automationApi.ts
@@ -17,10 +17,12 @@ export interface Automation {
lastExecution?: number;
nextExecution?: number;
executionLogs?: AutomationLog[];
+ allowedProviders?: string[];
_createdAt?: number;
_updatedAt?: number;
_createdByUserName?: string;
mandateName?: string;
+ featureInstanceName?: string;
[key: string]: any;
}
diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts
new file mode 100644
index 0000000..7c78496
--- /dev/null
+++ b/src/api/billingApi.ts
@@ -0,0 +1,380 @@
+import { ApiRequestOptions } from '../hooks/useApi';
+
+// ============================================================================
+// TYPES & INTERFACES
+// ============================================================================
+
+export type BillingModel = 'PREPAY_MANDATE' | 'PREPAY_USER' | 'CREDIT_POSTPAY' | 'UNLIMITED';
+export type TransactionType = 'CREDIT' | 'DEBIT' | 'ADJUSTMENT';
+export type ReferenceType = 'WORKFLOW' | 'PAYMENT' | 'ADMIN' | 'SYSTEM';
+
+export interface BillingAddress {
+ company: string;
+ street: string;
+ zip: string;
+ city: string;
+ country: string;
+ vatNumber?: string;
+}
+
+export interface BillingBalance {
+ mandateId: string;
+ mandateName: string;
+ billingModel: BillingModel;
+ balance: number;
+ currency: string;
+ warningThreshold: number;
+ isWarning: boolean;
+ creditLimit?: number;
+}
+
+export interface BillingTransaction {
+ id: string;
+ accountId: string;
+ transactionType: TransactionType;
+ amount: number;
+ description: string;
+ referenceType?: ReferenceType;
+ workflowId?: string;
+ featureInstanceId?: string;
+ featureCode?: string;
+ aicoreProvider?: string;
+ aicoreModel?: string;
+ createdByUserId?: string;
+ createdAt?: string;
+ mandateId?: string;
+ mandateName?: string;
+ userId?: string;
+ userName?: string;
+}
+
+export interface BillingSettings {
+ id: string;
+ mandateId: string;
+ billingModel: BillingModel;
+ defaultUserCredit: number;
+ warningThresholdPercent: number;
+ blockOnZeroBalance: boolean;
+ notifyOnWarning: boolean;
+ notifyEmails: string[];
+ billingAddress?: BillingAddress;
+}
+
+export interface BillingSettingsUpdate {
+ billingModel?: BillingModel;
+ defaultUserCredit?: number;
+ warningThresholdPercent?: number;
+ blockOnZeroBalance?: boolean;
+ notifyOnWarning?: boolean;
+ notifyEmails?: string[];
+ billingAddress?: BillingAddress;
+}
+
+export interface UsageReport {
+ period: string;
+ totalCost: number;
+ transactionCount: number;
+ costByProvider: Record;
+ costByModel: Record;
+ costByFeature: Record;
+}
+
+export interface AccountSummary {
+ id: string;
+ mandateId: string;
+ userId?: string;
+ accountType: string;
+ balance: number;
+ creditLimit?: number;
+ warningThreshold: number;
+ enabled: boolean;
+}
+
+export interface CreditAddRequest {
+ userId?: string;
+ amount: number;
+ description?: string;
+}
+
+// Type for the request function passed to API functions
+export type ApiRequestFunction = (options: ApiRequestOptions) => Promise;
+
+// ============================================================================
+// USER API FUNCTIONS
+// ============================================================================
+
+/**
+ * Fetch billing balances for all mandates the user belongs to
+ * Endpoint: GET /api/billing/balance
+ */
+export async function fetchBalances(
+ request: ApiRequestFunction
+): Promise {
+ return await request({
+ url: '/api/billing/balance',
+ method: 'get'
+ });
+}
+
+/**
+ * Fetch billing balance for a specific mandate
+ * Endpoint: GET /api/billing/balance/{mandateId}
+ */
+export async function fetchBalanceForMandate(
+ request: ApiRequestFunction,
+ mandateId: string
+): Promise {
+ return await request({
+ url: `/api/billing/balance/${mandateId}`,
+ method: 'get'
+ });
+}
+
+/**
+ * Fetch transaction history
+ * Endpoint: GET /api/billing/transactions
+ */
+export async function fetchTransactions(
+ request: ApiRequestFunction,
+ limit: number = 50,
+ offset: number = 0
+): Promise {
+ return await request({
+ url: '/api/billing/transactions',
+ method: 'get',
+ params: { limit, offset }
+ });
+}
+
+/**
+ * Fetch usage statistics
+ * Endpoint: GET /api/billing/statistics/{period}
+ */
+export async function fetchStatistics(
+ request: ApiRequestFunction,
+ period: 'day' | 'month' | 'year',
+ year: number,
+ month?: number
+): Promise {
+ const params: Record = { year };
+ if (month !== undefined) {
+ params.month = month;
+ }
+
+ return await request({
+ url: `/api/billing/statistics/${period}`,
+ method: 'get',
+ params
+ });
+}
+
+/**
+ * Fetch allowed AICore providers
+ * Endpoint: GET /api/billing/providers
+ */
+export async function fetchAllowedProviders(
+ request: ApiRequestFunction
+): Promise {
+ return await request({
+ url: '/api/billing/providers',
+ method: 'get'
+ });
+}
+
+// ============================================================================
+// ADMIN API FUNCTIONS
+// ============================================================================
+
+/**
+ * Fetch billing settings for a mandate (Admin)
+ * Endpoint: GET /api/billing/admin/settings/{mandateId}
+ */
+export async function fetchSettingsAdmin(
+ request: ApiRequestFunction,
+ mandateId: string
+): Promise {
+ return await request({
+ url: `/api/billing/admin/settings/${mandateId}`,
+ method: 'get'
+ });
+}
+
+/**
+ * Create or update billing settings (Admin)
+ * Endpoint: POST /api/billing/admin/settings/{mandateId}
+ */
+export async function updateSettingsAdmin(
+ request: ApiRequestFunction,
+ mandateId: string,
+ settings: BillingSettingsUpdate
+): Promise {
+ return await request({
+ url: `/api/billing/admin/settings/${mandateId}`,
+ method: 'post',
+ data: settings
+ });
+}
+
+/**
+ * Add credit to an account (Admin)
+ * Endpoint: POST /api/billing/admin/credit/{mandateId}
+ */
+export async function addCreditAdmin(
+ request: ApiRequestFunction,
+ mandateId: string,
+ creditRequest: CreditAddRequest
+): Promise {
+ return await request({
+ url: `/api/billing/admin/credit/${mandateId}`,
+ method: 'post',
+ data: creditRequest
+ });
+}
+
+/**
+ * Fetch all accounts for a mandate (Admin)
+ * Endpoint: GET /api/billing/admin/accounts/{mandateId}
+ */
+export async function fetchAccountsAdmin(
+ request: ApiRequestFunction,
+ mandateId: string
+): Promise {
+ return await request({
+ url: `/api/billing/admin/accounts/${mandateId}`,
+ method: 'get'
+ });
+}
+
+/**
+ * Fetch all transactions for a mandate (Admin)
+ * Endpoint: GET /api/billing/admin/transactions/{mandateId}
+ */
+export async function fetchTransactionsAdmin(
+ request: ApiRequestFunction,
+ mandateId: string,
+ limit: number = 100
+): Promise {
+ return await request({
+ url: `/api/billing/admin/transactions/${mandateId}`,
+ method: 'get',
+ params: { limit }
+ });
+}
+
+/**
+ * User summary for billing admin
+ */
+export interface MandateUserSummary {
+ id: string;
+ username?: string;
+ email?: string;
+ firstName?: string;
+ lastName?: string;
+ displayName?: string;
+}
+
+/**
+ * Fetch all users for a mandate (Admin)
+ * Endpoint: GET /api/billing/admin/users/{mandateId}
+ */
+export async function fetchUsersForMandateAdmin(
+ request: ApiRequestFunction,
+ mandateId: string
+): Promise {
+ return await request({
+ url: `/api/billing/admin/users/${mandateId}`,
+ method: 'get'
+ });
+}
+
+// ============================================================================
+// MANDATE VIEW TYPES & API FUNCTIONS
+// ============================================================================
+
+export interface MandateBalance {
+ mandateId: string;
+ mandateName: string;
+ billingModel: BillingModel;
+ totalBalance: number;
+ userCount: number;
+ defaultUserCredit: number;
+ warningThresholdPercent: number;
+ blockOnZeroBalance: boolean;
+}
+
+/**
+ * Fetch mandate-level balances (SysAdmin only)
+ * Endpoint: GET /api/billing/view/mandates/balances
+ */
+export async function fetchMandateViewBalances(
+ request: ApiRequestFunction
+): Promise {
+ return await request({
+ url: '/api/billing/view/mandates/balances',
+ method: 'get'
+ });
+}
+
+/**
+ * Fetch mandate-level transactions (SysAdmin only)
+ * Endpoint: GET /api/billing/view/mandates/transactions
+ */
+export async function fetchMandateViewTransactions(
+ request: ApiRequestFunction,
+ limit: number = 100
+): Promise {
+ return await request({
+ url: '/api/billing/view/mandates/transactions',
+ method: 'get',
+ params: { limit }
+ });
+}
+
+// ============================================================================
+// USER VIEW TYPES & API FUNCTIONS
+// ============================================================================
+
+export interface UserBalance {
+ accountId: string;
+ mandateId: string;
+ mandateName: string;
+ userId: string;
+ userName: string;
+ balance: number;
+ warningThreshold: number;
+ isWarning: boolean;
+ enabled: boolean;
+}
+
+export interface UserTransaction extends BillingTransaction {
+ userId?: string;
+ userName?: string;
+}
+
+/**
+ * Fetch user-level balances (RBAC-based)
+ * Endpoint: GET /api/billing/view/users/balances
+ */
+export async function fetchUserViewBalances(
+ request: ApiRequestFunction
+): Promise {
+ return await request({
+ url: '/api/billing/view/users/balances',
+ method: 'get'
+ });
+}
+
+/**
+ * Fetch user-level transactions (RBAC-based)
+ * Endpoint: GET /api/billing/view/users/transactions
+ */
+export async function fetchUserViewTransactions(
+ request: ApiRequestFunction,
+ limit: number = 100
+): Promise {
+ return await request({
+ url: '/api/billing/view/users/transactions',
+ method: 'get',
+ params: { limit }
+ });
+}
diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts
index 2e813f5..a6191b4 100644
--- a/src/api/workflowApi.ts
+++ b/src/api/workflowApi.ts
@@ -47,6 +47,7 @@ export interface StartWorkflowRequest {
listFileId?: string[]; // Array of file ID strings (files must be uploaded first via /api/files/upload)
userLanguage?: string; // Optional, defaults to "en"
metadata?: Record;
+ allowedProviders?: string[]; // Optional: Restrict AI calls to these providers (empty = all RBAC-permitted)
}
export interface StartWorkflowResponse extends Workflow {
@@ -231,17 +232,18 @@ export async function fetchWorkflowLogs(
/**
* Fetch unified chat data (messages, logs, stats, documents)
- * Endpoint: GET /api/chat/playground/{workflowId}/chatData
+ * Endpoint: GET /api/chatplayground/{instanceId}/{workflowId}/chatData
* Query params: afterTimestamp (optional) - fetch only data created after this time
*/
export async function fetchChatData(
request: ApiRequestFunction,
+ instanceId: string,
workflowId: string,
afterTimestamp?: number
): Promise {
const params = afterTimestamp ? { afterTimestamp: afterTimestamp.toString() } : undefined;
const requestConfig = {
- url: `/api/chat/playground/${workflowId}/chatData`,
+ url: `/api/chatplayground/${instanceId}/${workflowId}/chatData`,
method: 'get' as const,
params
};
@@ -314,11 +316,12 @@ export async function fetchChatData(
/**
* Start a new workflow or continue an existing one
- * Endpoint: POST /api/chat/playground/start
- * Query params: workflowId (optional), workflowMode (default: "Actionplan")
+ * Endpoint: POST /api/chatplayground/{instanceId}/start
+ * Query params: workflowId (optional), workflowMode (default: "Dynamic")
*/
export async function startWorkflowApi(
request: ApiRequestFunction,
+ instanceId: string,
workflowData: StartWorkflowRequest,
options?: { workflowId?: string; workflowMode?: 'Dynamic' | 'Automation' }
): Promise {
@@ -341,11 +344,12 @@ export async function startWorkflowApi(
prompt: workflowData.prompt,
...(workflowData.listFileId && workflowData.listFileId.length > 0 && { listFileId: workflowData.listFileId }),
...(workflowData.userLanguage && { userLanguage: workflowData.userLanguage }),
- ...(workflowData.metadata && { metadata: workflowData.metadata })
+ ...(workflowData.metadata && { metadata: workflowData.metadata }),
+ ...(workflowData.allowedProviders && workflowData.allowedProviders.length > 0 && { allowedProviders: workflowData.allowedProviders })
};
const requestConfig = {
- url: '/api/chat/playground/start',
+ url: `/api/chatplayground/${instanceId}/start`,
method: 'post' as const,
data: requestBody,
params: params // Always include workflowMode
@@ -368,14 +372,15 @@ export async function startWorkflowApi(
/**
* Stop a running workflow
- * Endpoint: POST /api/chat/playground/{workflowId}/stop
+ * Endpoint: POST /api/chatplayground/{instanceId}/{workflowId}/stop
*/
export async function stopWorkflowApi(
request: ApiRequestFunction,
+ instanceId: string,
workflowId: string
): Promise {
await request({
- url: `/api/chat/playground/${workflowId}/stop`,
+ url: `/api/chatplayground/${instanceId}/${workflowId}/stop`,
method: 'post'
});
}
diff --git a/src/components/AutomationEditor/AutomationEditor.tsx b/src/components/AutomationEditor/AutomationEditor.tsx
index 677daea..255e404 100644
--- a/src/components/AutomationEditor/AutomationEditor.tsx
+++ b/src/components/AutomationEditor/AutomationEditor.tsx
@@ -13,6 +13,7 @@
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { FaTimes, FaSave, FaChevronLeft, FaChevronRight, FaRocket, FaFileAlt, FaCode, FaExclamationTriangle, FaMagic, FaFolder, FaFolderOpen, FaArrowUp, FaSpinner } from 'react-icons/fa';
import { ActionsPanel } from '../ActionsPanel';
+import { ProviderMultiSelect } from '../ProviderSelector';
import { useToast } from '../../contexts/ToastContext';
import { useLanguage } from '../../providers/language/LanguageContext';
import { useWorkflowActions } from '../../hooks/useAutomations';
@@ -368,6 +369,7 @@ export const AutomationEditor: React.FC = ({
const [label, setLabel] = useState('');
const [schedule, setSchedule] = useState('0 22 * * *');
const [active, setActive] = useState(false);
+ const [allowedProviders, setAllowedProviders] = useState([]);
// Template multilingual fields
const [labelMulti, setLabelMulti] = useState({ en: '', de: '' });
@@ -530,6 +532,7 @@ export const AutomationEditor: React.FC = ({
setLabel(def.label || '');
setSchedule(def.schedule || '0 22 * * *');
setActive(def.active ?? false);
+ setAllowedProviders(def.allowedProviders || []);
}
// Extract template JSON
@@ -684,7 +687,8 @@ export const AutomationEditor: React.FC = ({
schedule,
active,
template: templateJson,
- placeholders
+ placeholders,
+ allowedProviders
};
}
@@ -700,7 +704,7 @@ export const AutomationEditor: React.FC = ({
} finally {
setIsSaving(false);
}
- }, [label, schedule, active, labelMulti, overviewMulti, templateJson, placeholders, mode, initialData, onSave, showError]);
+ }, [label, schedule, active, allowedProviders, labelMulti, overviewMulti, templateJson, placeholders, mode, initialData, onSave, showError]);
// Computed values
const editorTitle = title || (mode === 'template'
@@ -831,6 +835,18 @@ export const AutomationEditor: React.FC = ({
Automatisierung ist aktiv und wird planmässig ausgeführt
+
+ {/* Allowed AI Providers */}
+
+
+
+ Beschränkt die Automation auf bestimmte AI-Provider. Leer = alle erlaubt.
+
+
)}
diff --git a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.module.css b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.module.css
index d9ed884..2aa4da0 100644
--- a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.module.css
+++ b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.module.css
@@ -37,6 +37,43 @@
white-space: nowrap;
}
+/* CSV Export Button */
+.csvExportButton {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ height: 40px;
+ padding: 0 14px;
+ border: 1px solid var(--color-primary);
+ border-radius: 25px;
+ background: var(--color-bg);
+ color: var(--color-text);
+ font-size: 12px;
+ font-family: var(--font-family);
+ font-weight: 400;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.csvExportButton:hover:not(:disabled) {
+ background: var(--color-secondary);
+ color: var(--color-bg);
+ border-color: var(--color-secondary);
+}
+
+.csvExportButton:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.csvExportIcon {
+ font-size: 13px;
+ display: flex;
+ align-items: center;
+}
+
.refreshButton {
display: flex;
align-items: center;
diff --git a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
index 04b233f..719be78 100644
--- a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
+++ b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
@@ -3,7 +3,7 @@ import { useLanguage } from '../../../providers/language/LanguageContext';
import styles from './FormGeneratorControls.module.css';
import { Button } from '../../UiComponents/Button';
import { IoIosRefresh } from "react-icons/io";
-import { FaTrash } from "react-icons/fa";
+import { FaTrash, FaDownload } from "react-icons/fa";
import type { AttributeType } from '../../../utils/attributeTypeMapper';
// Generic field/column config interface
@@ -62,6 +62,9 @@ export interface FormGeneratorControlsProps {
onPageSizeChange?: (pageSize: number) => void;
supportsBackendPagination?: boolean;
hookData?: any;
+ // CSV Export
+ onCsvExport?: () => void;
+ csvExporting?: boolean;
}
export function FormGeneratorControls({
@@ -87,7 +90,9 @@ export function FormGeneratorControls({
onPageChange,
onPageSizeChange,
supportsBackendPagination = false,
- hookData: _hookData // Reserved for future use
+ hookData: _hookData, // Reserved for future use
+ onCsvExport,
+ csvExporting = false
}: FormGeneratorControlsProps) {
void _hookData; // Suppress unused variable warning
const { t } = useLanguage();
@@ -147,6 +152,17 @@ export function FormGeneratorControls({
{activeFiltersCount} {t('formgen.filter.active', 'filter(s)')}
)}
+ {onCsvExport && (
+
+ )}
{onRefresh && (