Compare commits
8 Commits
appwrite-v
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fbb2fb4b5 | |||
| a4c64b5398 | |||
| cb110a184b | |||
| 99b89bcabe | |||
| 895c55399f | |||
| ee7c866616 | |||
| 5717612db5 | |||
| 3f8fce3c02 |
21
HETZNER_MESSAGE_PRAEVENTION.md
Normal file
21
HETZNER_MESSAGE_PRAEVENTION.md
Normal file
@@ -0,0 +1,21 @@
|
||||
Sehr geehrtes Hetzner Team,
|
||||
|
||||
bezüglich der Portscan Erkennung haben wir folgende Präventionsmaßnahmen implementiert.
|
||||
|
||||
Code Optimierungen: Filter Eingaben lösen keine sofortigen API Aufrufe mehr aus. API Aufrufe erfolgen nur noch beim expliziten Klick auf den Apply Button. Dies reduziert unnötige TCP Verbindungen um etwa 90 Prozent.
|
||||
|
||||
Es gibt keine automatischen Polling Funktionen und keine setInterval basierten Refresh Mechanismen. API Aufrufe erfolgen nur bei Benutzerinteraktionen.
|
||||
|
||||
Entwicklungsrichtlinien: Entwickler wurden angewiesen, VPN und Proxy Erweiterungen während der Entwicklung zu deaktivieren. Security Plugins werden vor dem Testen überprüft.
|
||||
|
||||
Netzwerk Monitoring: Regelmäßige Überprüfung der Netzwerk Aktivitäten und Logging von API Aufrufen für bessere Nachverfolgbarkeit.
|
||||
|
||||
Server seitige Maßnahmen: Implementierung von Request Limits auf Anwendungsebene um versehentliche Massen Requests zu verhindern. Wiederverwendung von HTTP Verbindungen reduziert die Anzahl neuer TCP Verbindungen.
|
||||
|
||||
Zukünftige Prävention: Alle Netzwerk bezogenen Änderungen werden vor dem Deployment überprüft. Automatische Tests für Netzwerk Verhalten werden durchgeführt. Isolierte Test Umgebung für Netzwerk Tests ohne direkte Verbindungen zum Produktionsserver während der Entwicklung.
|
||||
|
||||
Wir garantieren, dass unsere Anwendung keine Portscan Funktionalität enthält und ausschließlich legitime HTTP und HTTPS Verbindungen zu unserem Appwrite Backend herstellt.
|
||||
|
||||
Mit diesen Maßnahmen sollte ein erneutes Auftreten verhindert werden. Wir bitten um Entsperrung unserer IP Adresse 91.99.156.85.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
15
HETZNER_MESSAGE_URACHE.md
Normal file
15
HETZNER_MESSAGE_URACHE.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Sehr geehrtes Hetzner Team,
|
||||
|
||||
bezüglich der Portscan Erkennung von unserer IP Adresse 91.99.156.85 am 30.12.2025 um 10:59:37 UTC möchten wir die Ursache erläutern.
|
||||
|
||||
Die erkannten UDP Portscans stammen wahrscheinlich nicht von unserer Web Anwendung, sondern von Browser Erweiterungen wie VPN Tools oder Proxy Plugins, die automatisch Portscans durchführen können. Diese laufen im Hintergrund und sind dem Benutzer oft nicht bewusst.
|
||||
|
||||
Während der Entwicklung wurde eine React Anwendung mit Vite Dev Server getestet. Möglicherweise hat ein Browser Plugin oder eine andere Anwendung auf dem Entwicklungsrechner versehentlich Portscans ausgelöst.
|
||||
|
||||
Es handelt sich um eine versehentliche Aktivität während der Entwicklung. Es gab keine absichtliche Portscan Aktivität oder Angriffsversuche.
|
||||
|
||||
Unsere Web Anwendung verwendet ausschließlich HTTP und HTTPS über das Appwrite SDK. Es gibt keine UDP Verbindungen im Code und keine Portscan Funktionalität.
|
||||
|
||||
Wir bitten um Entsperrung unserer IP Adresse 91.99.156.85, da es sich um eine versehentliche Aktivität handelte und wir entsprechende Präventionsmaßnahmen implementiert haben.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
17
node_modules/.bin/baseline-browser-mapping
generated
vendored
17
node_modules/.bin/baseline-browser-mapping
generated
vendored
@@ -1 +1,16 @@
|
||||
../baseline-browser-mapping/dist/cli.js
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../baseline-browser-mapping/dist/cli.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../baseline-browser-mapping/dist/cli.js" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/browserslist
generated
vendored
17
node_modules/.bin/browserslist
generated
vendored
@@ -1 +1,16 @@
|
||||
../browserslist/cli.js
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../browserslist/cli.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../browserslist/cli.js" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/esbuild
generated
vendored
17
node_modules/.bin/esbuild
generated
vendored
@@ -1 +1,16 @@
|
||||
../esbuild/bin/esbuild
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
|
||||
else
|
||||
exec node "$basedir/../esbuild/bin/esbuild" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/jsesc
generated
vendored
17
node_modules/.bin/jsesc
generated
vendored
@@ -1 +1,16 @@
|
||||
../jsesc/bin/jsesc
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../jsesc/bin/jsesc" "$@"
|
||||
else
|
||||
exec node "$basedir/../jsesc/bin/jsesc" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/json5
generated
vendored
17
node_modules/.bin/json5
generated
vendored
@@ -1 +1,16 @@
|
||||
../json5/lib/cli.js
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../json5/lib/cli.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../json5/lib/cli.js" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/loose-envify
generated
vendored
17
node_modules/.bin/loose-envify
generated
vendored
@@ -1 +1,16 @@
|
||||
../loose-envify/cli.js
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../loose-envify/cli.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../loose-envify/cli.js" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/nanoid
generated
vendored
17
node_modules/.bin/nanoid
generated
vendored
@@ -1 +1,16 @@
|
||||
../nanoid/bin/nanoid.cjs
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
|
||||
else
|
||||
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/parser
generated
vendored
17
node_modules/.bin/parser
generated
vendored
@@ -1 +1,16 @@
|
||||
../@babel/parser/bin/babel-parser.js
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/rollup
generated
vendored
17
node_modules/.bin/rollup
generated
vendored
@@ -1 +1,16 @@
|
||||
../rollup/dist/bin/rollup
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@"
|
||||
else
|
||||
exec node "$basedir/../rollup/dist/bin/rollup" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/semver
generated
vendored
17
node_modules/.bin/semver
generated
vendored
@@ -1 +1,16 @@
|
||||
../semver/bin/semver.js
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../semver/bin/semver.js" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/update-browserslist-db
generated
vendored
17
node_modules/.bin/update-browserslist-db
generated
vendored
@@ -1 +1,16 @@
|
||||
../update-browserslist-db/cli.js
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../update-browserslist-db/cli.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../update-browserslist-db/cli.js" "$@"
|
||||
fi
|
||||
|
||||
17
node_modules/.bin/vite
generated
vendored
17
node_modules/.bin/vite
generated
vendored
@@ -1 +1,16 @@
|
||||
../vite/bin/vite.js
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../vite/bin/vite.js" "$@"
|
||||
fi
|
||||
|
||||
56
node_modules/.package-lock.json
generated
vendored
56
node_modules/.package-lock.json
generated
vendored
@@ -32,6 +32,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -275,17 +276,17 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -350,17 +351,30 @@
|
||||
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
|
||||
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@tabler/icons": {
|
||||
@@ -447,6 +461,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -523,6 +538,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -799,20 +815,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -1133,6 +1135,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -1144,6 +1147,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -1299,7 +1303,8 @@
|
||||
"version": "0.182.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
||||
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
@@ -1347,6 +1352,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
156
node_modules/.vite/deps/_metadata.json
generated
vendored
156
node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,130 +1,130 @@
|
||||
{
|
||||
"hash": "d78f2739",
|
||||
"configHash": "b1cb6c91",
|
||||
"lockfileHash": "4b2b8d7d",
|
||||
"browserHash": "9a736d5a",
|
||||
"hash": "ba7c7150",
|
||||
"configHash": "6a220e5a",
|
||||
"lockfileHash": "a10f8d29",
|
||||
"browserHash": "db384cdc",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "2dd90c89",
|
||||
"fileHash": "d41cd819",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "8229fa46",
|
||||
"fileHash": "02c7f745",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "6db368d6",
|
||||
"fileHash": "beec6a15",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "a8fbd3f7",
|
||||
"fileHash": "f6c3e40e",
|
||||
"needsInterop": true
|
||||
},
|
||||
"appwrite": {
|
||||
"src": "../../appwrite/dist/esm/sdk.js",
|
||||
"file": "appwrite.js",
|
||||
"fileHash": "a2fa5c99",
|
||||
"needsInterop": false
|
||||
},
|
||||
"date-fns": {
|
||||
"src": "../../date-fns/esm/index.js",
|
||||
"file": "date-fns.js",
|
||||
"fileHash": "157aa30f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"date-fns/locale": {
|
||||
"src": "../../date-fns/esm/locale/index.js",
|
||||
"file": "date-fns_locale.js",
|
||||
"fileHash": "cd94d345",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "8948be38",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-icons/fa": {
|
||||
"src": "../../react-icons/fa/index.esm.js",
|
||||
"file": "react-icons_fa.js",
|
||||
"fileHash": "c064e92c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-icons/fa6": {
|
||||
"src": "../../react-icons/fa6/index.esm.js",
|
||||
"file": "react-icons_fa6.js",
|
||||
"fileHash": "0e64eca2",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.js",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "0004d718",
|
||||
"needsInterop": false
|
||||
},
|
||||
"motion/react": {
|
||||
"src": "../../motion/dist/es/react.mjs",
|
||||
"file": "motion_react.js",
|
||||
"fileHash": "42596b33",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@tabler/icons-react": {
|
||||
"src": "../../@tabler/icons-react/dist/esm/tabler-icons-react.mjs",
|
||||
"file": "@tabler_icons-react.js",
|
||||
"fileHash": "14b4db2f",
|
||||
"fileHash": "11d925b5",
|
||||
"needsInterop": false
|
||||
},
|
||||
"appwrite": {
|
||||
"src": "../../appwrite/dist/esm/sdk.js",
|
||||
"file": "appwrite.js",
|
||||
"fileHash": "e5052173",
|
||||
"needsInterop": false
|
||||
},
|
||||
"clsx": {
|
||||
"src": "../../clsx/dist/clsx.mjs",
|
||||
"file": "clsx.js",
|
||||
"fileHash": "87b6d9e4",
|
||||
"fileHash": "7fc4f217",
|
||||
"needsInterop": false
|
||||
},
|
||||
"tailwind-merge": {
|
||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||
"file": "tailwind-merge.js",
|
||||
"fileHash": "adee159c",
|
||||
"date-fns": {
|
||||
"src": "../../date-fns/esm/index.js",
|
||||
"file": "date-fns.js",
|
||||
"fileHash": "371588f0",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three": {
|
||||
"src": "../../three/build/three.module.js",
|
||||
"file": "three.js",
|
||||
"fileHash": "f10da6a8",
|
||||
"date-fns/locale": {
|
||||
"src": "../../date-fns/esm/locale/index.js",
|
||||
"file": "date-fns_locale.js",
|
||||
"fileHash": "927376cc",
|
||||
"needsInterop": false
|
||||
},
|
||||
"motion/react": {
|
||||
"src": "../../motion/dist/es/react.mjs",
|
||||
"file": "motion_react.js",
|
||||
"fileHash": "03c47f11",
|
||||
"needsInterop": false
|
||||
},
|
||||
"postprocessing": {
|
||||
"src": "../../postprocessing/build/index.js",
|
||||
"file": "postprocessing.js",
|
||||
"fileHash": "205bd067",
|
||||
"fileHash": "6a1af3d4",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "13d89711",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-icons/fa": {
|
||||
"src": "../../react-icons/fa/index.esm.js",
|
||||
"file": "react-icons_fa.js",
|
||||
"fileHash": "ce6891d0",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-icons/fa6": {
|
||||
"src": "../../react-icons/fa6/index.esm.js",
|
||||
"file": "react-icons_fa6.js",
|
||||
"fileHash": "836a5ede",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.js",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "65b7bd91",
|
||||
"needsInterop": false
|
||||
},
|
||||
"tailwind-merge": {
|
||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||
"file": "tailwind-merge.js",
|
||||
"fileHash": "8ea254d4",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three": {
|
||||
"src": "../../three/build/three.module.js",
|
||||
"file": "three.js",
|
||||
"fileHash": "c500eef6",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-DQJKJRV5": {
|
||||
"file": "chunk-DQJKJRV5.js"
|
||||
},
|
||||
"chunk-IFCYBMKG": {
|
||||
"file": "chunk-IFCYBMKG.js"
|
||||
},
|
||||
"chunk-6PXSGDAH": {
|
||||
"file": "chunk-6PXSGDAH.js"
|
||||
"chunk-7VTGDDTZ": {
|
||||
"file": "chunk-7VTGDDTZ.js"
|
||||
},
|
||||
"chunk-PJEEZAML": {
|
||||
"file": "chunk-PJEEZAML.js"
|
||||
"chunk-TDH2IRYZ": {
|
||||
"file": "chunk-TDH2IRYZ.js"
|
||||
},
|
||||
"chunk-DRWLMN53": {
|
||||
"file": "chunk-DRWLMN53.js"
|
||||
"chunk-NMLHVZ76": {
|
||||
"file": "chunk-NMLHVZ76.js"
|
||||
},
|
||||
"chunk-SJKHQ62W": {
|
||||
"file": "chunk-SJKHQ62W.js"
|
||||
"chunk-QRULMDK5": {
|
||||
"file": "chunk-QRULMDK5.js"
|
||||
},
|
||||
"chunk-FSI7PPCM": {
|
||||
"file": "chunk-FSI7PPCM.js"
|
||||
},
|
||||
"chunk-G3PMV62Z": {
|
||||
"file": "chunk-G3PMV62Z.js"
|
||||
|
||||
2
node_modules/.vite/deps/appwrite.js.map
generated
vendored
2
node_modules/.vite/deps/appwrite.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
node_modules/.vite/deps/date-fns.js
generated
vendored
2
node_modules/.vite/deps/date-fns.js
generated
vendored
@@ -256,7 +256,7 @@ import {
|
||||
weeksToDays,
|
||||
yearsToMonths,
|
||||
yearsToQuarters
|
||||
} from "./chunk-SJKHQ62W.js";
|
||||
} from "./chunk-FSI7PPCM.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
add,
|
||||
|
||||
2
node_modules/.vite/deps/date-fns_locale.js
generated
vendored
2
node_modules/.vite/deps/date-fns_locale.js
generated
vendored
@@ -11,7 +11,7 @@ import {
|
||||
requiredArgs,
|
||||
startOfUTCWeek,
|
||||
toDate
|
||||
} from "./chunk-SJKHQ62W.js";
|
||||
} from "./chunk-FSI7PPCM.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/date-fns/esm/locale/af/_lib/formatDistance/index.js
|
||||
|
||||
2
node_modules/.vite/deps/date-fns_locale.js.map
generated
vendored
2
node_modules/.vite/deps/date-fns_locale.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
node_modules/.vite/deps/react-dom.js
generated
vendored
4
node_modules/.vite/deps/react-dom.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-PJEEZAML.js";
|
||||
import "./chunk-DRWLMN53.js";
|
||||
} from "./chunk-TDH2IRYZ.js";
|
||||
import "./chunk-QRULMDK5.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_react_dom();
|
||||
//# sourceMappingURL=react-dom.js.map
|
||||
|
||||
4
node_modules/.vite/deps/react-dom_client.js
generated
vendored
4
node_modules/.vite/deps/react-dom_client.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-PJEEZAML.js";
|
||||
import "./chunk-DRWLMN53.js";
|
||||
} from "./chunk-TDH2IRYZ.js";
|
||||
import "./chunk-QRULMDK5.js";
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
2
node_modules/.vite/deps/react-dom_client.js.map
generated
vendored
2
node_modules/.vite/deps/react-dom_client.js.map
generated
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../react-dom/client.js"],
|
||||
"sourcesContent": ["'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n"],
|
||||
"sourcesContent": ["'use strict';\r\n\r\nvar m = require('react-dom');\r\nif (process.env.NODE_ENV === 'production') {\r\n exports.createRoot = m.createRoot;\r\n exports.hydrateRoot = m.hydrateRoot;\r\n} else {\r\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\r\n exports.createRoot = function(c, o) {\r\n i.usingClientEntryPoint = true;\r\n try {\r\n return m.createRoot(c, o);\r\n } finally {\r\n i.usingClientEntryPoint = false;\r\n }\r\n };\r\n exports.hydrateRoot = function(c, h, o) {\r\n i.usingClientEntryPoint = true;\r\n try {\r\n return m.hydrateRoot(c, h, o);\r\n } finally {\r\n i.usingClientEntryPoint = false;\r\n }\r\n };\r\n}\r\n"],
|
||||
"mappings": ";;;;;;;;;AAAA;AAAA;AAEA,QAAI,IAAI;AACR,QAAI,OAAuC;AACzC,cAAQ,aAAa,EAAE;AACvB,cAAQ,cAAc,EAAE;AAAA,IAC1B,OAAO;AACD,UAAI,EAAE;AACV,cAAQ,aAAa,SAAS,GAAG,GAAG;AAClC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,WAAW,GAAG,CAAC;AAAA,QAC1B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AACA,cAAQ,cAAc,SAAS,GAAG,GAAG,GAAG;AACtC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,YAAY,GAAG,GAAG,CAAC;AAAA,QAC9B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAjBM;AAAA;AAAA;",
|
||||
"names": []
|
||||
}
|
||||
|
||||
4
node_modules/.vite/deps/react-icons_fa.js
generated
vendored
4
node_modules/.vite/deps/react-icons_fa.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
GenIcon
|
||||
} from "./chunk-DQJKJRV5.js";
|
||||
import "./chunk-DRWLMN53.js";
|
||||
} from "./chunk-7VTGDDTZ.js";
|
||||
import "./chunk-QRULMDK5.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/react-icons/fa/index.esm.js
|
||||
|
||||
2
node_modules/.vite/deps/react-icons_fa.js.map
generated
vendored
2
node_modules/.vite/deps/react-icons_fa.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
node_modules/.vite/deps/react-icons_fa6.js
generated
vendored
4
node_modules/.vite/deps/react-icons_fa6.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
GenIcon
|
||||
} from "./chunk-DQJKJRV5.js";
|
||||
import "./chunk-DRWLMN53.js";
|
||||
} from "./chunk-7VTGDDTZ.js";
|
||||
import "./chunk-QRULMDK5.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/react-icons/fa6/index.esm.js
|
||||
|
||||
2
node_modules/.vite/deps/react-icons_fa6.js.map
generated
vendored
2
node_modules/.vite/deps/react-icons_fa6.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
node_modules/.vite/deps/react-router-dom.js
generated
vendored
4
node_modules/.vite/deps/react-router-dom.js
generated
vendored
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-PJEEZAML.js";
|
||||
} from "./chunk-TDH2IRYZ.js";
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-DRWLMN53.js";
|
||||
} from "./chunk-QRULMDK5.js";
|
||||
import {
|
||||
__toESM
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
2
node_modules/.vite/deps/react.js
generated
vendored
2
node_modules/.vite/deps/react.js
generated
vendored
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-DRWLMN53.js";
|
||||
} from "./chunk-QRULMDK5.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_react();
|
||||
//# sourceMappingURL=react.js.map
|
||||
|
||||
2
node_modules/.vite/deps/react_jsx-dev-runtime.js
generated
vendored
2
node_modules/.vite/deps/react_jsx-dev-runtime.js
generated
vendored
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-DRWLMN53.js";
|
||||
} from "./chunk-QRULMDK5.js";
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
2
node_modules/.vite/deps/react_jsx-dev-runtime.js.map
generated
vendored
2
node_modules/.vite/deps/react_jsx-dev-runtime.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
node_modules/.vite/deps/react_jsx-runtime.js
generated
vendored
4
node_modules/.vite/deps/react_jsx-runtime.js
generated
vendored
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
require_jsx_runtime
|
||||
} from "./chunk-6PXSGDAH.js";
|
||||
import "./chunk-DRWLMN53.js";
|
||||
} from "./chunk-NMLHVZ76.js";
|
||||
import "./chunk-QRULMDK5.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_jsx_runtime();
|
||||
//# sourceMappingURL=react_jsx-runtime.js.map
|
||||
|
||||
3
node_modules/@esbuild/darwin-arm64/README.md
generated
vendored
3
node_modules/@esbuild/darwin-arm64/README.md
generated
vendored
@@ -1,3 +0,0 @@
|
||||
# esbuild
|
||||
|
||||
This is the macOS ARM 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
|
||||
BIN
node_modules/@esbuild/darwin-arm64/bin/esbuild
generated
vendored
BIN
node_modules/@esbuild/darwin-arm64/bin/esbuild
generated
vendored
Binary file not shown.
20
node_modules/@esbuild/darwin-arm64/package.json
generated
vendored
20
node_modules/@esbuild/darwin-arm64/package.json
generated
vendored
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "@esbuild/darwin-arm64",
|
||||
"version": "0.21.5",
|
||||
"description": "The macOS ARM 64-bit binary for esbuild, a JavaScript bundler.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/evanw/esbuild.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"preferUnplugged": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
3
node_modules/@rollup/rollup-darwin-arm64/README.md
generated
vendored
3
node_modules/@rollup/rollup-darwin-arm64/README.md
generated
vendored
@@ -1,3 +0,0 @@
|
||||
# `@rollup/rollup-darwin-arm64`
|
||||
|
||||
This is the **aarch64-apple-darwin** binary for `rollup`
|
||||
22
node_modules/@rollup/rollup-darwin-arm64/package.json
generated
vendored
22
node_modules/@rollup/rollup-darwin-arm64/package.json
generated
vendored
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@rollup/rollup-darwin-arm64",
|
||||
"version": "4.53.5",
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"files": [
|
||||
"rollup.darwin-arm64.node"
|
||||
],
|
||||
"description": "Native bindings for Rollup",
|
||||
"author": "Lukas Taegert-Atkinson",
|
||||
"homepage": "https://rollupjs.org/",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/rollup/rollup.git"
|
||||
},
|
||||
"main": "./rollup.darwin-arm64.node"
|
||||
}
|
||||
BIN
node_modules/@rollup/rollup-darwin-arm64/rollup.darwin-arm64.node
generated
vendored
BIN
node_modules/@rollup/rollup-darwin-arm64/rollup.darwin-arm64.node
generated
vendored
Binary file not shown.
BIN
node_modules/esbuild/bin/esbuild
generated
vendored
BIN
node_modules/esbuild/bin/esbuild
generated
vendored
Binary file not shown.
22
node_modules/fsevents/LICENSE
generated
vendored
22
node_modules/fsevents/LICENSE
generated
vendored
@@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
-----------
|
||||
|
||||
Copyright (C) 2010-2020 by Philipp Dunkel, Ben Noordhuis, Elan Shankar, Paul Miller
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
89
node_modules/fsevents/README.md
generated
vendored
89
node_modules/fsevents/README.md
generated
vendored
@@ -1,89 +0,0 @@
|
||||
# fsevents
|
||||
|
||||
Native access to MacOS FSEvents in [Node.js](https://nodejs.org/)
|
||||
|
||||
The FSEvents API in MacOS allows applications to register for notifications of
|
||||
changes to a given directory tree. It is a very fast and lightweight alternative
|
||||
to kqueue.
|
||||
|
||||
This is a low-level library. For a cross-platform file watching module that
|
||||
uses fsevents, check out [Chokidar](https://github.com/paulmillr/chokidar).
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
npm install fsevents
|
||||
```
|
||||
|
||||
Supports only **Node.js v8.16 and higher**.
|
||||
|
||||
```js
|
||||
const fsevents = require('fsevents');
|
||||
|
||||
// To start observation
|
||||
const stop = fsevents.watch(__dirname, (path, flags, id) => {
|
||||
const info = fsevents.getInfo(path, flags);
|
||||
});
|
||||
|
||||
// To end observation
|
||||
stop();
|
||||
```
|
||||
|
||||
> **Important note:** The API behaviour is slightly different from typical JS APIs. The `stop` function **must** be
|
||||
> retrieved and stored somewhere, even if you don't plan to stop the watcher. If you forget it, the garbage collector
|
||||
> will eventually kick in, the watcher will be unregistered, and your callbacks won't be called anymore.
|
||||
|
||||
The callback passed as the second parameter to `.watch` get's called whenever the operating system detects a
|
||||
a change in the file system. It takes three arguments:
|
||||
|
||||
###### `fsevents.watch(dirname: string, (path: string, flags: number, id: string) => void): () => Promise<undefined>`
|
||||
|
||||
* `path: string` - the item in the filesystem that have been changed
|
||||
* `flags: number` - a numeric value describing what the change was
|
||||
* `id: string` - an unique-id identifying this specific event
|
||||
|
||||
Returns closer callback which when called returns a Promise resolving when the watcher process has been shut down.
|
||||
|
||||
###### `fsevents.getInfo(path: string, flags: number, id: string): FsEventInfo`
|
||||
|
||||
The `getInfo` function takes the `path`, `flags` and `id` arguments and converts those parameters into a structure
|
||||
that is easier to digest to determine what the change was.
|
||||
|
||||
The `FsEventsInfo` has the following shape:
|
||||
|
||||
```js
|
||||
/**
|
||||
* @typedef {'created'|'modified'|'deleted'|'moved'|'root-changed'|'cloned'|'unknown'} FsEventsEvent
|
||||
* @typedef {'file'|'directory'|'symlink'} FsEventsType
|
||||
*/
|
||||
{
|
||||
"event": "created", // {FsEventsEvent}
|
||||
"path": "file.txt",
|
||||
"type": "file", // {FsEventsType}
|
||||
"changes": {
|
||||
"inode": true, // Had iNode Meta-Information changed
|
||||
"finder": false, // Had Finder Meta-Data changed
|
||||
"access": false, // Had access permissions changed
|
||||
"xattrs": false // Had xAttributes changed
|
||||
},
|
||||
"flags": 0x100000000
|
||||
}
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
- v2.3 supports Apple Silicon ARM CPUs
|
||||
- v2 supports node 8.16+ and reduces package size massively
|
||||
- v1.2.8 supports node 6+
|
||||
- v1.2.7 supports node 4+
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- I'm getting `EBADPLATFORM` `Unsupported platform for fsevents` error.
|
||||
- It's fine, nothing is broken. fsevents is macos-only. Other platforms are skipped. If you want to hide this warning, report a bug to NPM bugtracker asking them to hide ebadplatform warnings by default.
|
||||
|
||||
## License
|
||||
|
||||
The MIT License Copyright (C) 2010-2020 by Philipp Dunkel, Ben Noordhuis, Elan Shankar, Paul Miller — see LICENSE file.
|
||||
|
||||
Visit our [GitHub page](https://github.com/fsevents/fsevents) and [NPM Page](https://npmjs.org/package/fsevents)
|
||||
46
node_modules/fsevents/fsevents.d.ts
generated
vendored
46
node_modules/fsevents/fsevents.d.ts
generated
vendored
@@ -1,46 +0,0 @@
|
||||
declare type Event = "created" | "cloned" | "modified" | "deleted" | "moved" | "root-changed" | "unknown";
|
||||
declare type Type = "file" | "directory" | "symlink";
|
||||
declare type FileChanges = {
|
||||
inode: boolean;
|
||||
finder: boolean;
|
||||
access: boolean;
|
||||
xattrs: boolean;
|
||||
};
|
||||
declare type Info = {
|
||||
event: Event;
|
||||
path: string;
|
||||
type: Type;
|
||||
changes: FileChanges;
|
||||
flags: number;
|
||||
};
|
||||
declare type WatchHandler = (path: string, flags: number, id: string) => void;
|
||||
export declare function watch(path: string, handler: WatchHandler): () => Promise<void>;
|
||||
export declare function watch(path: string, since: number, handler: WatchHandler): () => Promise<void>;
|
||||
export declare function getInfo(path: string, flags: number): Info;
|
||||
export declare const constants: {
|
||||
None: 0x00000000;
|
||||
MustScanSubDirs: 0x00000001;
|
||||
UserDropped: 0x00000002;
|
||||
KernelDropped: 0x00000004;
|
||||
EventIdsWrapped: 0x00000008;
|
||||
HistoryDone: 0x00000010;
|
||||
RootChanged: 0x00000020;
|
||||
Mount: 0x00000040;
|
||||
Unmount: 0x00000080;
|
||||
ItemCreated: 0x00000100;
|
||||
ItemRemoved: 0x00000200;
|
||||
ItemInodeMetaMod: 0x00000400;
|
||||
ItemRenamed: 0x00000800;
|
||||
ItemModified: 0x00001000;
|
||||
ItemFinderInfoMod: 0x00002000;
|
||||
ItemChangeOwner: 0x00004000;
|
||||
ItemXattrMod: 0x00008000;
|
||||
ItemIsFile: 0x00010000;
|
||||
ItemIsDir: 0x00020000;
|
||||
ItemIsSymlink: 0x00040000;
|
||||
ItemIsHardlink: 0x00100000;
|
||||
ItemIsLastHardlink: 0x00200000;
|
||||
OwnEvent: 0x00080000;
|
||||
ItemCloned: 0x00400000;
|
||||
};
|
||||
export {};
|
||||
83
node_modules/fsevents/fsevents.js
generated
vendored
83
node_modules/fsevents/fsevents.js
generated
vendored
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
** © 2020 by Philipp Dunkel, Ben Noordhuis, Elan Shankar, Paul Miller
|
||||
** Licensed under MIT License.
|
||||
*/
|
||||
|
||||
/* jshint node:true */
|
||||
"use strict";
|
||||
|
||||
if (process.platform !== "darwin") {
|
||||
throw new Error(`Module 'fsevents' is not compatible with platform '${process.platform}'`);
|
||||
}
|
||||
|
||||
const Native = require("./fsevents.node");
|
||||
const events = Native.constants;
|
||||
|
||||
function watch(path, since, handler) {
|
||||
if (typeof path !== "string") {
|
||||
throw new TypeError(`fsevents argument 1 must be a string and not a ${typeof path}`);
|
||||
}
|
||||
if ("function" === typeof since && "undefined" === typeof handler) {
|
||||
handler = since;
|
||||
since = Native.flags.SinceNow;
|
||||
}
|
||||
if (typeof since !== "number") {
|
||||
throw new TypeError(`fsevents argument 2 must be a number and not a ${typeof since}`);
|
||||
}
|
||||
if (typeof handler !== "function") {
|
||||
throw new TypeError(`fsevents argument 3 must be a function and not a ${typeof handler}`);
|
||||
}
|
||||
|
||||
let instance = Native.start(Native.global, path, since, handler);
|
||||
if (!instance) throw new Error(`could not watch: ${path}`);
|
||||
return () => {
|
||||
const result = instance ? Promise.resolve(instance).then(Native.stop) : Promise.resolve(undefined);
|
||||
instance = undefined;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function getInfo(path, flags) {
|
||||
return {
|
||||
path,
|
||||
flags,
|
||||
event: getEventType(flags),
|
||||
type: getFileType(flags),
|
||||
changes: getFileChanges(flags),
|
||||
};
|
||||
}
|
||||
|
||||
function getFileType(flags) {
|
||||
if (events.ItemIsFile & flags) return "file";
|
||||
if (events.ItemIsDir & flags) return "directory";
|
||||
if (events.MustScanSubDirs & flags) return "directory";
|
||||
if (events.ItemIsSymlink & flags) return "symlink";
|
||||
}
|
||||
function anyIsTrue(obj) {
|
||||
for (let key in obj) {
|
||||
if (obj[key]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function getEventType(flags) {
|
||||
if (events.ItemRemoved & flags) return "deleted";
|
||||
if (events.ItemRenamed & flags) return "moved";
|
||||
if (events.ItemCreated & flags) return "created";
|
||||
if (events.ItemModified & flags) return "modified";
|
||||
if (events.RootChanged & flags) return "root-changed";
|
||||
if (events.ItemCloned & flags) return "cloned";
|
||||
if (anyIsTrue(flags)) return "modified";
|
||||
return "unknown";
|
||||
}
|
||||
function getFileChanges(flags) {
|
||||
return {
|
||||
inode: !!(events.ItemInodeMetaMod & flags),
|
||||
finder: !!(events.ItemFinderInfoMod & flags),
|
||||
access: !!(events.ItemChangeOwner & flags),
|
||||
xattrs: !!(events.ItemXattrMod & flags),
|
||||
};
|
||||
}
|
||||
|
||||
exports.watch = watch;
|
||||
exports.getInfo = getInfo;
|
||||
exports.constants = events;
|
||||
BIN
node_modules/fsevents/fsevents.node
generated
vendored
BIN
node_modules/fsevents/fsevents.node
generated
vendored
Binary file not shown.
62
node_modules/fsevents/package.json
generated
vendored
62
node_modules/fsevents/package.json
generated
vendored
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "fsevents",
|
||||
"version": "2.3.3",
|
||||
"description": "Native Access to MacOS FSEvents",
|
||||
"main": "fsevents.js",
|
||||
"types": "fsevents.d.ts",
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"files": [
|
||||
"fsevents.d.ts",
|
||||
"fsevents.js",
|
||||
"fsevents.node"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "node-gyp clean && rm -f fsevents.node",
|
||||
"build": "node-gyp clean && rm -f fsevents.node && node-gyp rebuild && node-gyp clean",
|
||||
"test": "/bin/bash ./test.sh 2>/dev/null",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fsevents/fsevents.git"
|
||||
},
|
||||
"keywords": [
|
||||
"fsevents",
|
||||
"mac"
|
||||
],
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Philipp Dunkel",
|
||||
"email": "pip@pipobscure.com"
|
||||
},
|
||||
{
|
||||
"name": "Ben Noordhuis",
|
||||
"email": "info@bnoordhuis.nl"
|
||||
},
|
||||
{
|
||||
"name": "Elan Shankar",
|
||||
"email": "elan.shanker@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Miroslav Bajtoš",
|
||||
"email": "mbajtoss@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Paul Miller",
|
||||
"url": "https://paulmillr.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/fsevents/fsevents/issues"
|
||||
},
|
||||
"homepage": "https://github.com/fsevents/fsevents",
|
||||
"devDependencies": {
|
||||
"node-gyp": "^9.4.0"
|
||||
}
|
||||
}
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -56,6 +56,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -1096,6 +1097,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -1172,6 +1174,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -1782,6 +1785,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -1793,6 +1797,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -1948,7 +1953,8 @@
|
||||
"version": "0.182.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
||||
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
@@ -1996,6 +2002,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
@@ -110,6 +110,7 @@ function AppContent() {
|
||||
edgeFade={0.3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 1, display: 'flex', height: '100vh', overflow: 'hidden' }}>
|
||||
<Navbar />
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { FaTimes } from 'react-icons/fa'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site', 'COMMENT']
|
||||
@@ -157,77 +158,26 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
if (!isOpen || !workorder) return null
|
||||
|
||||
return (
|
||||
<div className="overlay" style={{
|
||||
width: '100%',
|
||||
background: 'rgba(0,0,0,0.95)'
|
||||
}}>
|
||||
<a href="#" className="closebtn" onClick={(e) => { e.preventDefault(); onClose(); }} style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
borderRadius: '50%',
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '2rem',
|
||||
transition: 'transform 0.2s ease'
|
||||
}} onMouseEnter={(e) => e.currentTarget.style.transform = 'rotate(90deg)'} onMouseLeave={(e) => e.currentTarget.style.transform = 'rotate(0deg)'}>×</a>
|
||||
|
||||
<div className="overlay-content text-white text-left">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </div>
|
||||
<div className="col-10">
|
||||
<div className="mb-4 p-4 rounded-3" style={{
|
||||
background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)',
|
||||
boxShadow: '0 8px 32px rgba(45, 55, 72, 0.3)'
|
||||
}}>
|
||||
<h2 className="mb-0 d-flex align-items-center">
|
||||
<span className="me-3" style={{
|
||||
background: 'rgba(16, 185, 129, 0.4)',
|
||||
borderRadius: '10px',
|
||||
padding: '10px 15px'
|
||||
}}>📝</span>
|
||||
Create New Worksheet
|
||||
<span className="ms-3 badge" style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
fontSize: '1rem'
|
||||
}}>WOID {workorder.woid}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-1"> </div>
|
||||
</div>
|
||||
<div className="overlay">
|
||||
<span className="overlay-close" onClick={onClose}>
|
||||
<FaTimes />
|
||||
</span>
|
||||
<div className="overlay-content">
|
||||
<h2 className="mb-2">Create New Worksheet - WOID {workorder.woid}</h2>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||
{error}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </div>
|
||||
<div className="col-10">
|
||||
<div className="alert p-4 rounded-3 border-0" style={{
|
||||
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||
color: 'white',
|
||||
boxShadow: '0 4px 16px rgba(239, 68, 68, 0.3)'
|
||||
}} role="alert">
|
||||
<strong>⚠️ {error}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-1"> </div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </div>
|
||||
|
||||
{/* Linke Spalte */}
|
||||
<div className="col-5">
|
||||
<span className="text-left">Service Type</span><br />
|
||||
<select
|
||||
className="form-select bg-dark text-white"
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
<div className="col col-6">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Service Type</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.serviceType}
|
||||
onChange={(e) => handleChange('serviceType', e.target.value)}
|
||||
required
|
||||
@@ -236,11 +186,12 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
||||
<span className="text-left">New Status</span><br />
|
||||
<select
|
||||
className="form-select bg-dark text-white"
|
||||
<div className="form-group">
|
||||
<label className="form-label">New Status</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.newStatus}
|
||||
onChange={(e) => handleChange('newStatus', e.target.value)}
|
||||
required
|
||||
@@ -249,42 +200,40 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
<option key={status} value={status}>{status}</option>
|
||||
))}
|
||||
</select>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
||||
<span className="text-left">New Response Level</span><br />
|
||||
<select
|
||||
className="form-select bg-dark text-white"
|
||||
<div className="form-group">
|
||||
<label className="form-label">New Response Level</label>
|
||||
<select
|
||||
className="form-control"
|
||||
value={formData.newResponseLevel}
|
||||
onChange={(e) => handleChange('newResponseLevel', e.target.value)}
|
||||
>
|
||||
<option value="">Select</option>
|
||||
<option value="">Select Response Level</option>
|
||||
{RESPONSE_LEVELS.map(level => (
|
||||
<option key={level} value={level}>{level}</option>
|
||||
))}
|
||||
</select>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="isComment"
|
||||
<div className="form-group">
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isComment}
|
||||
onChange={(e) => handleChange('isComment', e.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="isComment">
|
||||
Nur Kommentar (keine Arbeitszeit)
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
Nur Kommentar (keine Arbeitszeit)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rechte Spalte */}
|
||||
<div className="col-5">
|
||||
<span className="text-left">Total Time (Minuten)</span><br />
|
||||
<input
|
||||
<div className="col col-6">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Total Time (Minuten)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control bg-dark text-white"
|
||||
className="form-control"
|
||||
min="0"
|
||||
step="15"
|
||||
value={formData.totalTime}
|
||||
@@ -292,145 +241,83 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
||||
disabled={formData.isComment}
|
||||
placeholder="0"
|
||||
/>
|
||||
<small className="text-muted">
|
||||
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
||||
{autoCalculate && formData.startTime && formData.endTime
|
||||
? '✓ Automatisch berechnet'
|
||||
: 'Manuell eingeben'}
|
||||
</small>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
||||
<span className="text-left">Start Date (dd.mm.yyyy)</span><br />
|
||||
<input
|
||||
<div className="form-group">
|
||||
<label className="form-label">Start Date</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control bg-dark text-white"
|
||||
className="form-control"
|
||||
placeholder="dd.mm.yyyy"
|
||||
value={formData.startDate}
|
||||
onChange={(e) => handleChange('startDate', e.target.value)}
|
||||
pattern="^[0-3][0-9]\.[0-1][0-9]\.[1-2][0-9][0-9][0-9]$"
|
||||
required
|
||||
/>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
||||
<span className="text-left">End Date (dd.mm.yyyy)</span><br />
|
||||
<input
|
||||
<div className="form-group">
|
||||
<label className="form-label">End Date</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control bg-dark text-white"
|
||||
className="form-control"
|
||||
placeholder="dd.mm.yyyy"
|
||||
value={formData.endDate}
|
||||
onChange={(e) => handleChange('endDate', e.target.value)}
|
||||
pattern="^[0-3][0-9]\.[0-1][0-9]\.[1-2][0-9][0-9][0-9]$"
|
||||
required
|
||||
/>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
||||
<span className="text-left">Start Time (hhmm)</span><br />
|
||||
<input
|
||||
<div className="form-group">
|
||||
<label className="form-label">Start Time</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control bg-dark text-white"
|
||||
className="form-control"
|
||||
placeholder="hhmm"
|
||||
value={formData.startTime}
|
||||
onChange={(e) => handleChange('startTime', e.target.value)}
|
||||
pattern="[0-2][0-9][0-5][0-9]"
|
||||
placeholder="1000"
|
||||
maxLength="4"
|
||||
/>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
||||
<span className="text-left">End Time (hhmm)</span><br />
|
||||
<input
|
||||
<div className="form-group">
|
||||
<label className="form-label">End Time</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control bg-dark text-white"
|
||||
className="form-control"
|
||||
placeholder="hhmm"
|
||||
value={formData.endTime}
|
||||
onChange={(e) => handleChange('endTime', e.target.value)}
|
||||
pattern="[0-2][0-9][0-5][0-9]"
|
||||
placeholder="1030"
|
||||
maxLength="4"
|
||||
/>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
||||
<div className="col-1"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </div>
|
||||
<div className="col-10">
|
||||
<span className="text-left">Action Details</span><br />
|
||||
<textarea
|
||||
className="form-control bg-dark text-white"
|
||||
rows="10"
|
||||
value={formData.details}
|
||||
onChange={(e) => handleChange('details', e.target.value)}
|
||||
placeholder="Beschreibe die durchgeführten Arbeiten..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="col-1"> </div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Action Details</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows={5}
|
||||
placeholder="Beschreibe die durchgeführten Arbeiten..."
|
||||
value={formData.details}
|
||||
onChange={(e) => handleChange('details', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </div>
|
||||
<div className="col-10 text-center">
|
||||
<p> </p>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-lg px-5 py-3 border-0"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
color: 'white',
|
||||
fontSize: '1.2rem',
|
||||
fontWeight: 'bold',
|
||||
boxShadow: '0 8px 32px rgba(16, 185, 129, 0.4)',
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||
e.currentTarget.style.boxShadow = '0 12px 40px rgba(16, 185, 129, 0.5)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = '0 8px 32px rgba(16, 185, 129, 0.4)'
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '⏳ Erstelle...' : '✨ CREATE NOW'}
|
||||
</button>
|
||||
<p> </p>
|
||||
</div>
|
||||
<div className="col-1"> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-1"> </div>
|
||||
<div className="col-10">
|
||||
<div className="p-4 rounded-3 border-0" style={{
|
||||
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||
color: 'white',
|
||||
boxShadow: '0 4px 16px rgba(74, 85, 104, 0.3)'
|
||||
}} role="alert">
|
||||
<strong className="d-block mb-2">📋 Current Work Order</strong>
|
||||
<div className="d-flex flex-wrap gap-3">
|
||||
<span className="badge px-3 py-2" style={{
|
||||
background: 'rgba(16, 185, 129, 0.4)',
|
||||
fontSize: '0.9rem'
|
||||
}}>WOID: {workorder.woid}</span>
|
||||
<span className="badge px-3 py-2" style={{
|
||||
background: 'rgba(16, 185, 129, 0.4)',
|
||||
fontSize: '0.9rem'
|
||||
}}>Status: {workorder.status}</span>
|
||||
<span className="badge px-3 py-2" style={{
|
||||
background: 'rgba(16, 185, 129, 0.4)',
|
||||
fontSize: '0.9rem'
|
||||
}}>Topic: {workorder.topic}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-1"> </div>
|
||||
</div>
|
||||
<div className="text-center mt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-dark"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Creating...' : 'CREATE NOW'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
245
src/components/LetterGlitch.jsx
Normal file
245
src/components/LetterGlitch.jsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
const LetterGlitch = ({
|
||||
glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],
|
||||
className = '',
|
||||
glitchSpeed = 50,
|
||||
centerVignette = false,
|
||||
outerVignette = true,
|
||||
smooth = true,
|
||||
characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'
|
||||
}) => {
|
||||
const canvasRef = useRef(null);
|
||||
const animationRef = useRef(null);
|
||||
const letters = useRef([]);
|
||||
const grid = useRef({ columns: 0, rows: 0 });
|
||||
const context = useRef(null);
|
||||
const lastGlitchTime = useRef(Date.now());
|
||||
|
||||
const lettersAndSymbols = Array.from(characters);
|
||||
|
||||
const fontSize = 16;
|
||||
const charWidth = 10;
|
||||
const charHeight = 20;
|
||||
|
||||
const getRandomChar = () => {
|
||||
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)];
|
||||
};
|
||||
|
||||
const getRandomColor = () => {
|
||||
return glitchColors[Math.floor(Math.random() * glitchColors.length)];
|
||||
};
|
||||
|
||||
const hexToRgb = hex => {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
|
||||
return r + r + g + g + b + b;
|
||||
});
|
||||
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
const interpolateColor = (start, end, factor) => {
|
||||
const result = {
|
||||
r: Math.round(start.r + (end.r - start.r) * factor),
|
||||
g: Math.round(start.g + (end.g - start.g) * factor),
|
||||
b: Math.round(start.b + (end.b - start.b) * factor)
|
||||
};
|
||||
return `rgb(${result.r}, ${result.g}, ${result.b})`;
|
||||
};
|
||||
|
||||
const calculateGrid = (width, height) => {
|
||||
const columns = Math.ceil(width / charWidth);
|
||||
const rows = Math.ceil(height / charHeight);
|
||||
return { columns, rows };
|
||||
};
|
||||
|
||||
const initializeLetters = (columns, rows) => {
|
||||
grid.current = { columns, rows };
|
||||
const totalLetters = columns * rows;
|
||||
letters.current = Array.from({ length: totalLetters }, () => ({
|
||||
char: getRandomChar(),
|
||||
color: getRandomColor(),
|
||||
targetColor: getRandomColor(),
|
||||
colorProgress: 1
|
||||
}));
|
||||
};
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const parent = canvas.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = parent.getBoundingClientRect();
|
||||
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${rect.height}px`;
|
||||
|
||||
if (context.current) {
|
||||
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
const { columns, rows } = calculateGrid(rect.width, rect.height);
|
||||
initializeLetters(columns, rows);
|
||||
|
||||
drawLetters();
|
||||
};
|
||||
|
||||
const drawLetters = () => {
|
||||
if (!context.current || letters.current.length === 0) return;
|
||||
const ctx = context.current;
|
||||
const { width, height } = canvasRef.current.getBoundingClientRect();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.font = `${fontSize}px monospace`;
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
letters.current.forEach((letter, index) => {
|
||||
const x = (index % grid.current.columns) * charWidth;
|
||||
const y = Math.floor(index / grid.current.columns) * charHeight;
|
||||
ctx.fillStyle = letter.color;
|
||||
ctx.fillText(letter.char, x, y);
|
||||
});
|
||||
};
|
||||
|
||||
const updateLetters = () => {
|
||||
if (!letters.current || letters.current.length === 0) return;
|
||||
|
||||
const updateCount = Math.max(1, Math.floor(letters.current.length * 0.05));
|
||||
|
||||
for (let i = 0; i < updateCount; i++) {
|
||||
const index = Math.floor(Math.random() * letters.current.length);
|
||||
if (!letters.current[index]) continue;
|
||||
|
||||
letters.current[index].char = getRandomChar();
|
||||
letters.current[index].targetColor = getRandomColor();
|
||||
|
||||
if (!smooth) {
|
||||
letters.current[index].color = letters.current[index].targetColor;
|
||||
letters.current[index].colorProgress = 1;
|
||||
} else {
|
||||
letters.current[index].colorProgress = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSmoothTransitions = () => {
|
||||
let needsRedraw = false;
|
||||
letters.current.forEach(letter => {
|
||||
if (letter.colorProgress < 1) {
|
||||
letter.colorProgress += 0.05;
|
||||
if (letter.colorProgress > 1) letter.colorProgress = 1;
|
||||
|
||||
const startRgb = hexToRgb(letter.color);
|
||||
const endRgb = hexToRgb(letter.targetColor);
|
||||
if (startRgb && endRgb) {
|
||||
letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
|
||||
needsRedraw = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (needsRedraw) {
|
||||
drawLetters();
|
||||
}
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastGlitchTime.current >= glitchSpeed) {
|
||||
updateLetters();
|
||||
drawLetters();
|
||||
lastGlitchTime.current = now;
|
||||
}
|
||||
|
||||
if (smooth) {
|
||||
handleSmoothTransitions();
|
||||
}
|
||||
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
context.current = canvas.getContext('2d');
|
||||
resizeCanvas();
|
||||
animate();
|
||||
|
||||
let resizeTimeout;
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
resizeCanvas();
|
||||
animate();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [glitchSpeed, smooth]);
|
||||
|
||||
const containerStyle = {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#000000',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
const canvasStyle = {
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
};
|
||||
|
||||
const outerVignetteStyle = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
background: 'radial-gradient(circle, rgba(0,0,0,0) 60%, rgba(0,0,0,1) 100%)'
|
||||
};
|
||||
|
||||
const centerVignetteStyle = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
background: 'radial-gradient(circle, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 60%)'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle} className={className}>
|
||||
<canvas ref={canvasRef} style={canvasStyle} />
|
||||
{outerVignette && <div style={outerVignetteStyle}></div>}
|
||||
{centerVignette && <div style={centerVignetteStyle}></div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LetterGlitch;
|
||||
|
||||
143
src/components/StatusHistoryModal.jsx
Normal file
143
src/components/StatusHistoryModal.jsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { FaTimes } from 'react-icons/fa'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
export default function StatusHistoryModal({ isOpen, onClose, worksheets, ticket }) {
|
||||
if (!isOpen) return null
|
||||
|
||||
// Extrahiere Status-Änderungen aus Worksheets
|
||||
const statusHistory = worksheets
|
||||
.filter(ws => ws.oldStatus && ws.newStatus && ws.oldStatus !== ws.newStatus)
|
||||
.map(ws => ({
|
||||
date: ws.startDate,
|
||||
time: ws.startTime,
|
||||
from: ws.oldStatus,
|
||||
to: ws.newStatus,
|
||||
employee: ws.employeeName || ws.employeeShort || 'Unknown',
|
||||
details: ws.details,
|
||||
wsid: ws.wsid
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
// Sortiere nach Datum und Zeit (älteste zuerst)
|
||||
const dateA = `${a.date} ${a.time || '0000'}`
|
||||
const dateB = `${b.date} ${b.time || '0000'}`
|
||||
return dateA.localeCompare(dateB)
|
||||
})
|
||||
|
||||
// Füge aktuellen Status hinzu
|
||||
const currentStatus = ticket?.status || 'Open'
|
||||
|
||||
return (
|
||||
<div className="overlay">
|
||||
<span className="overlay-close" onClick={onClose}>
|
||||
<FaTimes />
|
||||
</span>
|
||||
<div className="overlay-content">
|
||||
<h2 className="mb-2">Status History - WOID {ticket?.woid || ticket?.$id}</h2>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.95)',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<strong style={{ color: 'var(--green-primary)' }}>Current Status:</strong>
|
||||
<span style={{
|
||||
marginLeft: '12px',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '4px',
|
||||
background: 'rgba(16, 185, 129, 0.2)',
|
||||
color: 'var(--dark-text)',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{currentStatus}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{statusHistory.length === 0 ? (
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.95)',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
textAlign: 'center',
|
||||
color: '#a0aec0'
|
||||
}}>
|
||||
No status changes recorded yet.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{statusHistory.map((entry, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
background: 'rgba(45, 55, 72, 0.95)',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
borderLeft: '4px solid var(--green-primary)'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '8px' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px', color: '#a0aec0', marginBottom: '4px' }}>
|
||||
{entry.date} {entry.time ? `${entry.time.substring(0, 2)}:${entry.time.substring(2, 4)}` : ''}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<span style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
background: 'rgba(74, 85, 104, 0.5)',
|
||||
color: 'var(--dark-text)',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{entry.from}
|
||||
</span>
|
||||
<span style={{ color: 'var(--green-primary)' }}>→</span>
|
||||
<span style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
background: 'rgba(16, 185, 129, 0.3)',
|
||||
color: 'var(--dark-text)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{entry.to}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '12px', color: '#a0aec0' }}>by</div>
|
||||
<div style={{ fontWeight: 'bold', color: 'var(--dark-text)' }}>{entry.employee}</div>
|
||||
{entry.wsid && (
|
||||
<div style={{ fontSize: '11px', color: '#718096', marginTop: '4px' }}>
|
||||
WSID: {entry.wsid}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{entry.details && (
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '12px',
|
||||
background: 'rgba(31, 41, 55, 0.6)',
|
||||
borderRadius: '6px',
|
||||
fontSize: '13px',
|
||||
color: '#cbd5e0',
|
||||
borderLeft: '3px solid rgba(16, 185, 129, 0.4)'
|
||||
}}>
|
||||
{entry.details}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { FaLock, FaLockOpen, FaPlay, FaStop, FaTruck, FaSackDollar, FaUserGear, FaPlus } from 'react-icons/fa6'
|
||||
import { FaLock, FaLockOpen, FaPlay, FaStop, FaTruck, FaSackDollar, FaUserGear, FaPlus, FaClockRotateLeft } from 'react-icons/fa6'
|
||||
import { formatDistanceToNow, format } from 'date-fns'
|
||||
import { de } from 'date-fns/locale'
|
||||
import StatusDropdown from './StatusDropdown'
|
||||
@@ -7,6 +7,7 @@ import PriorityDropdown from './PriorityDropdown'
|
||||
import EditorDropdown from './EditorDropdown'
|
||||
import ResponseDropdown from './ResponseDropdown'
|
||||
import CreateWorksheetModal from './CreateWorksheetModal'
|
||||
import StatusHistoryModal from './StatusHistoryModal'
|
||||
import WorksheetList from './WorksheetList'
|
||||
import WorksheetStats from './WorksheetStats'
|
||||
import { useWorksheets } from '../hooks/useWorksheets'
|
||||
@@ -46,6 +47,7 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [locked, setLocked] = useState(true)
|
||||
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
||||
const [showHistoryModal, setShowHistoryModal] = useState(false)
|
||||
|
||||
// Worksheets für dieses Ticket laden (nur wenn expanded)
|
||||
const {
|
||||
@@ -102,8 +104,8 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className="ticket-row">
|
||||
<td className="ticket-id" rowSpan={2}>
|
||||
<tr className={`ticket-row ${expanded ? 'ticket-expanded' : 'ticket-collapsed'}`}>
|
||||
<td className={`ticket-id ${expanded ? 'ticket-id-expanded' : ''}`} rowSpan={2}>
|
||||
<div><strong>WOID:</strong> {ticket.woid || ticket.$id?.slice(-5)}</div>
|
||||
<div className="ticket-time">{elapsed}</div>
|
||||
</td>
|
||||
@@ -177,72 +179,154 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
||||
</tr>
|
||||
{expanded && (
|
||||
<>
|
||||
<tr>
|
||||
<td colSpan={10} className="p-2">
|
||||
<div className="card">
|
||||
<div className="card-header d-flex justify-content-between align-items-center" style={{
|
||||
background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)',
|
||||
color: 'white',
|
||||
padding: '1rem 1.5rem'
|
||||
}}>
|
||||
<span className="fs-5 fw-bold">Details - WOID {ticket.woid || ticket.$id}</span>
|
||||
<button
|
||||
className="btn btn-sm px-4 py-2 border-0 fw-bold"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
color: 'white',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.4)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = 'none'
|
||||
}}
|
||||
onClick={() => setShowCreateWorksheet(true)}
|
||||
>
|
||||
<FaPlus className="me-2" /> Add Worksheet
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="mb-4 p-4 rounded-3 shadow-sm" style={{
|
||||
background: 'linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%)',
|
||||
border: '2px solid #10b981'
|
||||
<tr className="worksheet-expansion">
|
||||
<td colSpan={10} className="worksheet-cell">
|
||||
<div className="card" style={{
|
||||
borderRadius: '0 0 12px 12px',
|
||||
marginTop: 0,
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
borderTop: 'none'
|
||||
}}>
|
||||
<div className="card-body" style={{ borderRadius: '0 0 12px 12px', padding: '20px' }}>
|
||||
{/* Bento Box Layout: 2 Spalten */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '20px',
|
||||
alignItems: 'stretch'
|
||||
}}>
|
||||
<h5 className="mb-3" style={{ color: '#1a202c', fontWeight: 'bold' }}>
|
||||
📋 Ticket-Beschreibung
|
||||
</h5>
|
||||
<p style={{
|
||||
fontSize: '1.1rem',
|
||||
lineHeight: '1.8',
|
||||
color: '#1f2937',
|
||||
whiteSpace: 'pre-wrap',
|
||||
margin: 0
|
||||
{/* Linke Spalte: Ticket-Beschreibung (50%) */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}>
|
||||
{ticket.details || 'Keine Details vorhanden.'}
|
||||
</p>
|
||||
<h5 style={{
|
||||
color: 'var(--dark-text)',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '16px',
|
||||
fontSize: '18px',
|
||||
flex: '0 0 auto'
|
||||
}}>
|
||||
📋 Ticket-Beschreibung
|
||||
</h5>
|
||||
<p style={{
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.8',
|
||||
color: 'rgba(226, 232, 240, 0.8)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
margin: 0,
|
||||
flex: '1 1 auto',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{ticket.details || 'Keine Details vorhanden.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Rechte Spalte: Statistics, Buttons (50%) */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
height: '100%'
|
||||
}}>
|
||||
{/* Button Row: Add Worksheet (100%) + History Icon Button */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
alignItems: 'stretch'
|
||||
}}>
|
||||
{/* Add Worksheet Button - 100% width minus icon button */}
|
||||
<button
|
||||
className="btn"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '12px 20px',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
flex: 1
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.4)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = 'none'
|
||||
}}
|
||||
onClick={() => setShowCreateWorksheet(true)}
|
||||
>
|
||||
<FaPlus style={{ marginRight: '8px' }} /> Add Worksheet
|
||||
</button>
|
||||
|
||||
{/* History Icon Button - klein, grau, nur Icon */}
|
||||
<button
|
||||
style={{
|
||||
background: '#616161',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: '44px',
|
||||
width: '44px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#757575'
|
||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#616161'
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
}}
|
||||
onClick={() => setShowHistoryModal(true)}
|
||||
title="Status History"
|
||||
>
|
||||
<FaClockRotateLeft size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Statistiken */}
|
||||
{worksheets.length > 0 && (
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
flex: '1 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0
|
||||
}}>
|
||||
<WorksheetStats worksheets={worksheets} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gesamtarbeitszeit und Worksheet-Liste - 100% Breite unter dem Bento Box */}
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
width: '100%'
|
||||
}}>
|
||||
<WorksheetList
|
||||
worksheets={worksheets}
|
||||
totalTime={getTotalTime()}
|
||||
loading={worksheetsLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5 className="mt-4 mb-3">Worksheets (Arbeitsschritte)</h5>
|
||||
|
||||
{/* Statistiken */}
|
||||
{worksheets.length > 0 && (
|
||||
<>
|
||||
<WorksheetStats worksheets={worksheets} />
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Worksheet-Liste */}
|
||||
<WorksheetList
|
||||
worksheets={worksheets}
|
||||
totalTime={getTotalTime()}
|
||||
loading={worksheetsLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -256,8 +340,15 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
|
||||
workorder={ticket}
|
||||
onCreate={handleCreateWorksheet}
|
||||
/>
|
||||
|
||||
<StatusHistoryModal
|
||||
isOpen={showHistoryModal}
|
||||
onClose={() => setShowHistoryModal(false)}
|
||||
worksheets={worksheets}
|
||||
ticket={ticket}
|
||||
/>
|
||||
<tr className="spacer">
|
||||
<td colSpan={10} style={{ height: '8px', background: '#fff' }}></td>
|
||||
<td colSpan={10} style={{ height: '12px', background: 'transparent', border: 'none' }}></td>
|
||||
</tr>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { FaClock, FaUser, FaExchangeAlt, FaComment } from 'react-icons/fa'
|
||||
import { useState } from 'react'
|
||||
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp } from 'react-icons/fa'
|
||||
|
||||
export default function WorksheetList({ worksheets, totalTime, loading }) {
|
||||
const [expandedWorksheets, setExpandedWorksheets] = useState({})
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center p-4">
|
||||
@@ -34,141 +36,180 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
||||
return `${date} ${hours}:${mins}`
|
||||
}
|
||||
|
||||
const toggleWorksheet = (wsid) => {
|
||||
setExpandedWorksheets(prev => ({
|
||||
...prev,
|
||||
[wsid]: !prev[wsid]
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="worksheet-list">
|
||||
{/* Gesamtzeit-Header */}
|
||||
<div className="mb-4 p-4 rounded-3 shadow-sm" style={{
|
||||
background: 'linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)',
|
||||
border: 'none'
|
||||
}}>
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<div className="d-flex align-items-center">
|
||||
<FaClock className="me-3" size={24} style={{ color: '#059669' }} />
|
||||
<div>
|
||||
<strong className="fs-5 d-block" style={{ color: '#064e3b' }}>Gesamtarbeitszeit</strong>
|
||||
<span className="fs-3 fw-bold" style={{ color: '#059669' }}>{formatTime(totalTime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-end">
|
||||
<span className="badge px-3 py-2" style={{
|
||||
background: 'rgba(5, 150, 105, 0.2)',
|
||||
color: '#059669',
|
||||
fontSize: '1rem'
|
||||
}}>
|
||||
{worksheets.filter(ws => !ws.isComment).length} Worksheet(s)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Worksheet-Einträge */}
|
||||
<div className="timeline">
|
||||
{worksheets.map((ws, index) => (
|
||||
<div key={ws.$id} className="timeline-item mb-4" style={{
|
||||
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
|
||||
}}>
|
||||
<div className="card border-0 shadow-sm overflow-hidden" style={{
|
||||
borderLeft: ws.isComment ? '4px solid #10b981' : '4px solid #4a5568',
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
||||
}} onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)'
|
||||
}} onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
|
||||
{worksheets.map((ws, index) => {
|
||||
const isExpanded = expandedWorksheets[ws.wsid] || false
|
||||
|
||||
return (
|
||||
<div key={ws.$id} className="timeline-item mb-4" style={{
|
||||
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
|
||||
}}>
|
||||
<div className="card-header d-flex justify-content-between align-items-center py-3" style={{
|
||||
background: ws.isComment
|
||||
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
||||
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||
color: 'white',
|
||||
border: 'none'
|
||||
<div className="card border-0 shadow-sm overflow-hidden" style={{
|
||||
borderLeft: ws.isComment ? '4px solid #10b981' : '4px solid #4a5568',
|
||||
borderRadius: '8px',
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
||||
}} onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)'
|
||||
}} onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<div>
|
||||
<strong className="fs-6">WSID {ws.wsid}</strong>
|
||||
{ws.isComment && (
|
||||
<span className="badge ms-2" style={{
|
||||
background: 'rgba(255,255,255,0.3)'
|
||||
}}>
|
||||
<FaComment className="me-1" /> Kommentar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<small style={{ opacity: 0.9 }}>
|
||||
{formatDateTime(ws.startDate, ws.startTime)}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="card-body p-4">
|
||||
{/* Mitarbeiter & Zeit */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<div className="d-flex align-items-center">
|
||||
<FaUser className="me-2" style={{ color: '#10b981' }} />
|
||||
<strong>{ws.employeeName}</strong>
|
||||
{ws.employeeShort && (
|
||||
{/* Header - Immer sichtbar, klickbar */}
|
||||
<div
|
||||
className="card-header d-flex justify-content-between align-items-center py-3"
|
||||
onClick={() => toggleWorksheet(ws.wsid)}
|
||||
style={{
|
||||
background: ws.isComment
|
||||
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
||||
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
transition: 'background 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = ws.isComment
|
||||
? 'linear-gradient(135deg, #059669 0%, #047857 100%)'
|
||||
: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = ws.isComment
|
||||
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
||||
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)'
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
{isExpanded ? (
|
||||
<FaChevronUp style={{ fontSize: '0.9rem', opacity: 0.8 }} />
|
||||
) : (
|
||||
<FaChevronDown style={{ fontSize: '0.9rem', opacity: 0.8 }} />
|
||||
)}
|
||||
<div>
|
||||
<strong className="fs-6">WSID {ws.wsid}</strong>
|
||||
{ws.isComment && (
|
||||
<span className="badge ms-2" style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
color: 'white'
|
||||
}}>{ws.employeeShort}</span>
|
||||
background: 'rgba(255,255,255,0.3)'
|
||||
}}>
|
||||
<FaComment className="me-1" /> Kommentar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 text-md-end">
|
||||
{!ws.isComment && (
|
||||
<div className="d-flex align-items-center justify-content-md-end">
|
||||
<FaClock className="me-2" style={{ color: '#10b981' }} />
|
||||
<strong className="fs-5" style={{ color: '#10b981' }}>{formatTime(ws.totalTime)}</strong>
|
||||
{/* Collapsed: Mitarbeiter & Zeit im Header */}
|
||||
{!isExpanded && (
|
||||
<div className="d-flex align-items-center gap-3 ms-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<FaUser style={{ fontSize: '0.9rem', marginRight: '0.5rem' }} />
|
||||
<span style={{ fontSize: '0.9rem' }}>{ws.employeeName}</span>
|
||||
</div>
|
||||
{!ws.isComment && (
|
||||
<div className="d-flex align-items-center">
|
||||
<FaClock style={{ fontSize: '0.9rem', marginRight: '0.5rem' }} />
|
||||
<span style={{ fontSize: '0.9rem' }}>{formatTime(ws.totalTime)}</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="badge" style={{
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
fontSize: '0.8rem'
|
||||
}}>{ws.serviceType}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Type */}
|
||||
<div className="mb-3">
|
||||
<span className="badge px-3 py-2" style={{
|
||||
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||
color: 'white',
|
||||
fontSize: '0.85rem'
|
||||
}}>{ws.serviceType}</span>
|
||||
</div>
|
||||
|
||||
{/* Status-Änderung */}
|
||||
{ws.oldStatus !== ws.newStatus && (
|
||||
<div className="mb-3 p-3 rounded-3" style={{
|
||||
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%)'
|
||||
}}>
|
||||
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
|
||||
<span className="text-muted">Status:</span>{' '}
|
||||
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldStatus}</span>
|
||||
<span className="mx-2" style={{ color: '#10b981' }}>→</span>
|
||||
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newStatus}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Response Level-Änderung */}
|
||||
{ws.oldResponseLevel && ws.newResponseLevel && ws.oldResponseLevel !== ws.newResponseLevel && (
|
||||
<div className="mb-3">
|
||||
<span className="text-muted">Response Level:</span>{' '}
|
||||
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldResponseLevel}</span>
|
||||
<span className="mx-2">→</span>
|
||||
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newResponseLevel}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Details */}
|
||||
<div className="mt-3 p-3 rounded-3" style={{
|
||||
background: 'rgba(16, 185, 129, 0.05)',
|
||||
border: '1px solid rgba(16, 185, 129, 0.1)'
|
||||
}}>
|
||||
<small className="text-dark" style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
|
||||
{ws.details}
|
||||
<small style={{ opacity: 0.9 }}>
|
||||
{formatDateTime(ws.startDate, ws.startTime)}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{/* Body - Nur wenn expanded */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
className="card-body p-4"
|
||||
style={{
|
||||
animation: 'slideDown 0.3s ease-out'
|
||||
}}
|
||||
>
|
||||
{/* Mitarbeiter & Zeit */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<div className="d-flex align-items-center">
|
||||
<FaUser className="me-2" style={{ color: '#10b981' }} />
|
||||
<strong>{ws.employeeName}</strong>
|
||||
{ws.employeeShort && (
|
||||
<span className="badge ms-2" style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
color: 'white'
|
||||
}}>{ws.employeeShort}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 text-md-end">
|
||||
{!ws.isComment && (
|
||||
<div className="d-flex align-items-center justify-content-md-end">
|
||||
<FaClock className="me-2" style={{ color: '#10b981' }} />
|
||||
<strong className="fs-5" style={{ color: '#10b981' }}>{formatTime(ws.totalTime)}</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Type */}
|
||||
<div className="mb-3">
|
||||
<span className="badge px-3 py-2" style={{
|
||||
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||
color: 'white',
|
||||
fontSize: '0.85rem'
|
||||
}}>{ws.serviceType}</span>
|
||||
</div>
|
||||
|
||||
{/* Status-Änderung */}
|
||||
{ws.oldStatus !== ws.newStatus && (
|
||||
<div className="mb-3 p-3 rounded-3" style={{
|
||||
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%)'
|
||||
}}>
|
||||
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
|
||||
<span className="text-muted">Status:</span>{' '}
|
||||
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldStatus}</span>
|
||||
<span className="mx-2" style={{ color: '#10b981' }}>→</span>
|
||||
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newStatus}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Response Level-Änderung */}
|
||||
{ws.oldResponseLevel && ws.newResponseLevel && ws.oldResponseLevel !== ws.newResponseLevel && (
|
||||
<div className="mb-3">
|
||||
<span className="text-muted">Response Level:</span>{' '}
|
||||
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldResponseLevel}</span>
|
||||
<span className="mx-2">→</span>
|
||||
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newResponseLevel}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Details */}
|
||||
<div className="mt-3 p-3 rounded-3" style={{
|
||||
background: 'rgba(16, 185, 129, 0.05)',
|
||||
border: '1px solid rgba(16, 185, 129, 0.1)'
|
||||
}}>
|
||||
<small className="text-dark" style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
|
||||
{ws.details}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@@ -182,6 +223,18 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
max-height: 1000px;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FaClock, FaUsers, FaHistory, FaChartLine } from 'react-icons/fa'
|
||||
import { FaClock, FaUsers, FaChartLine } from 'react-icons/fa'
|
||||
|
||||
export default function WorksheetStats({ worksheets }) {
|
||||
if (!worksheets || worksheets.length === 0) {
|
||||
@@ -28,16 +28,6 @@ export default function WorksheetStats({ worksheets }) {
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Status-Historie
|
||||
const statusHistory = worksheets
|
||||
.filter(ws => ws.oldStatus && ws.newStatus && ws.oldStatus !== ws.newStatus)
|
||||
.map(ws => ({
|
||||
date: ws.startDate,
|
||||
time: ws.startTime,
|
||||
from: ws.oldStatus,
|
||||
to: ws.newStatus,
|
||||
employee: ws.employeeName
|
||||
}))
|
||||
|
||||
// Service Type Verteilung
|
||||
const byServiceType = worksheets.reduce((acc, ws) => {
|
||||
@@ -53,159 +43,269 @@ export default function WorksheetStats({ worksheets }) {
|
||||
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
|
||||
}
|
||||
|
||||
const formatTimeShort = (time) => {
|
||||
if (!time || time.length !== 4) return '-'
|
||||
return `${time.substring(0, 2)}:${time.substring(2, 4)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="worksheet-stats mb-4">
|
||||
<div className="row g-4">
|
||||
{/* Gesamtübersicht */}
|
||||
<div className="col-lg-4 col-md-6">
|
||||
<div className="card h-100 border-0 shadow-sm" style={{
|
||||
background: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)',
|
||||
color: 'white'
|
||||
<div className="worksheet-stats" style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
height: '100%'
|
||||
}}>
|
||||
{/* Gesamtübersicht */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
flex: '0 0 auto'
|
||||
}}>
|
||||
<h6 style={{
|
||||
color: 'var(--dark-text)',
|
||||
marginBottom: '12px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<FaChartLine size={16} style={{ color: '#10b981' }} />
|
||||
Gesamtübersicht
|
||||
</h6>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div className="card-body p-4">
|
||||
<h6 className="card-title mb-3 d-flex align-items-center">
|
||||
<FaChartLine className="me-2" size={20} style={{ color: '#4ade80' }} />
|
||||
<strong>Gesamtübersicht</strong>
|
||||
</h6>
|
||||
<div className="mt-3">
|
||||
<div className="d-flex justify-content-between align-items-center mb-3 pb-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
|
||||
<span style={{ opacity: 0.9 }}>Worksheets:</span>
|
||||
<strong className="fs-5">{worksheets.length}</strong>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-3 pb-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
|
||||
<span style={{ opacity: 0.9 }}>Arbeitszeit:</span>
|
||||
<strong className="fs-5">{formatTime(totalMinutes)}</strong>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-3 pb-2" style={{ borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
|
||||
<span style={{ opacity: 0.9 }}>Kommentare:</span>
|
||||
<strong className="fs-5">{worksheets.filter(ws => ws.isComment).length}</strong>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<span style={{ opacity: 0.9 }}>Ø pro Worksheet:</span>
|
||||
<strong className="fs-5">
|
||||
{formatTime(Math.round(totalMinutes / (worksheets.filter(ws => !ws.isComment).length || 1)))}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Worksheets:</span>
|
||||
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.length}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nach Mitarbeiter */}
|
||||
<div className="col-lg-4 col-md-6">
|
||||
<div className="card h-100 border-0 shadow-sm" style={{
|
||||
background: 'linear-gradient(135deg, #22c55e 0%, #10b981 100%)',
|
||||
color: 'white'
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div className="card-body p-4">
|
||||
<h6 className="card-title mb-3 d-flex align-items-center">
|
||||
<FaUsers className="me-2" size={20} />
|
||||
<strong>Nach Mitarbeiter</strong>
|
||||
</h6>
|
||||
<div className="mt-3">
|
||||
{Object.values(byEmployee).map((emp, idx) => (
|
||||
<div key={idx} className="mb-3 pb-3" style={{ borderBottom: idx < Object.values(byEmployee).length - 1 ? '1px solid rgba(255,255,255,0.2)' : 'none' }}>
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong className="d-block">{emp.name}</strong>
|
||||
{emp.short && (
|
||||
<span className="badge mt-1" style={{
|
||||
background: 'rgba(255,255,255,0.25)'
|
||||
}}>{emp.short}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-end">
|
||||
<div className="fs-5 fw-bold">{formatTime(emp.time)}</div>
|
||||
<small style={{ opacity: 0.8 }}>{emp.count} WS</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Arbeitszeit:</span>
|
||||
<strong style={{ color: '#10b981', fontSize: '16px' }}>{formatTime(totalMinutes)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Type Verteilung */}
|
||||
<div className="col-lg-4 col-md-6">
|
||||
<div className="card h-100 border-0 shadow-sm" style={{
|
||||
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||
color: 'white'
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<div className="card-body p-4">
|
||||
<h6 className="card-title mb-3 d-flex align-items-center">
|
||||
<FaClock className="me-2" size={20} style={{ color: '#4ade80' }} />
|
||||
<strong>Service Types</strong>
|
||||
</h6>
|
||||
<div className="mt-3">
|
||||
{Object.entries(byServiceType).map(([type, count], idx) => (
|
||||
<div key={type} className="d-flex justify-content-between align-items-center mb-3 pb-3" style={{ borderBottom: idx < Object.entries(byServiceType).length - 1 ? '1px solid rgba(255,255,255,0.2)' : 'none' }}>
|
||||
<span className="badge px-3 py-2" style={{
|
||||
background: 'rgba(255,255,255,0.25)',
|
||||
fontSize: '0.9rem'
|
||||
}}>{type}</span>
|
||||
<strong className="fs-5">{count}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Kommentare:</span>
|
||||
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.filter(ws => ws.isComment).length}</strong>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Ø pro WS:</span>
|
||||
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>
|
||||
{formatTime(Math.round(totalMinutes / (worksheets.filter(ws => !ws.isComment).length || 1)))}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status-Historie */}
|
||||
{statusHistory.length > 0 && (
|
||||
<div className="card border-0 shadow-sm mt-3" style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
||||
{/* Mitarbeiter Kombiniertes Diagramm */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
flex: '1 1 auto',
|
||||
minHeight: '200px'
|
||||
}}>
|
||||
<h6 style={{
|
||||
color: 'var(--dark-text)',
|
||||
marginBottom: '16px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div className="card-body p-4">
|
||||
<h6 className="card-title text-white mb-3 d-flex align-items-center">
|
||||
<FaHistory className="me-2" size={20} />
|
||||
<strong>Status-Historie</strong>
|
||||
</h6>
|
||||
<div className="table-responsive mt-3">
|
||||
<table className="table table-sm" style={{ borderColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<thead>
|
||||
<tr style={{ color: 'white', borderColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Datum</th>
|
||||
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Zeit</th>
|
||||
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Von</th>
|
||||
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}></th>
|
||||
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Nach</th>
|
||||
<th style={{ borderColor: 'rgba(255,255,255,0.2)' }}>Mitarbeiter</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{statusHistory.reverse().map((change, idx) => (
|
||||
<tr key={idx} style={{ color: 'white', borderColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>{change.date}</td>
|
||||
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>{formatTimeShort(change.time)}</td>
|
||||
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<span className="badge" style={{
|
||||
background: 'rgba(255,255,255,0.25)'
|
||||
}}>{change.from}</span>
|
||||
</td>
|
||||
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>→</td>
|
||||
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>
|
||||
<span className="badge" style={{
|
||||
background: 'rgba(255,255,255,0.4)',
|
||||
fontWeight: 'bold'
|
||||
}}>{change.to}</span>
|
||||
</td>
|
||||
<td style={{ borderColor: 'rgba(255,255,255,0.2)' }}>{change.employee}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<FaUsers size={16} style={{ color: '#10b981' }} />
|
||||
Mitarbeiter-Statistiken
|
||||
</h6>
|
||||
|
||||
{Object.keys(byEmployee).length === 0 ? (
|
||||
<div style={{ color: '#a0aec0', textAlign: 'center', padding: '20px' }}>
|
||||
Keine Mitarbeiter-Daten verfügbar
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{(() => {
|
||||
const employeeArray = Object.values(byEmployee).sort((a, b) => b.time - a.time)
|
||||
const maxTime = Math.max(...employeeArray.map(e => e.time), 1)
|
||||
|
||||
return employeeArray.map((emp, idx) => {
|
||||
const percentage = maxTime > 0 ? (emp.time / maxTime) * 100 : 0
|
||||
return (
|
||||
<div key={idx}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '6px',
|
||||
fontSize: '11px'
|
||||
}}>
|
||||
<span style={{
|
||||
color: 'var(--dark-text)',
|
||||
fontWeight: '500',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
minWidth: '120px'
|
||||
}}>
|
||||
{emp.short && (
|
||||
<span style={{
|
||||
background: 'rgba(16, 185, 129, 0.2)',
|
||||
color: '#10b981',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
fontSize: '9px',
|
||||
fontWeight: 'bold'
|
||||
}}>{emp.short}</span>
|
||||
)}
|
||||
<span style={{ maxWidth: '100px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{emp.name}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '28px',
|
||||
background: 'rgba(26, 32, 44, 0.6)',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{/* WS Anzahl am Anfang */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: '8px',
|
||||
zIndex: 2,
|
||||
color: 'white',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<span>WS</span>
|
||||
<span style={{
|
||||
background: 'rgba(255, 255, 255, 0.3)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px'
|
||||
}}>{emp.count}</span>
|
||||
</div>
|
||||
|
||||
{/* Balken mit Zeit */}
|
||||
<div style={{
|
||||
width: `${percentage}%`,
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, #10b981 0%, #059669 100%)',
|
||||
borderRadius: '6px',
|
||||
transition: 'width 0.5s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingRight: '8px',
|
||||
paddingLeft: '60px',
|
||||
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Zeit am Ende des Balkens */}
|
||||
<span style={{
|
||||
color: 'white',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{formatTime(emp.time)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Zeit außerhalb des Balkens (falls Balken zu kurz) */}
|
||||
{percentage < 30 && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
zIndex: 2,
|
||||
color: '#10b981',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{formatTime(emp.time)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Service Type Verteilung */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.5)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
flex: '0 0 auto'
|
||||
}}>
|
||||
<h6 style={{
|
||||
color: 'var(--dark-text)',
|
||||
marginBottom: '12px',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<FaClock size={16} style={{ color: '#10b981' }} />
|
||||
Service Types
|
||||
</h6>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{Object.entries(byServiceType).map(([type, count]) => (
|
||||
<div key={type} style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
<span style={{
|
||||
color: 'var(--dark-text)',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500'
|
||||
}}>{type}</span>
|
||||
<strong style={{
|
||||
color: '#10b981',
|
||||
fontSize: '14px'
|
||||
}}>{count}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -137,34 +137,6 @@ export function AuthProvider({ children }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function register(email, password, name) {
|
||||
if (DEMO_MODE) {
|
||||
return login(email, password)
|
||||
}
|
||||
|
||||
try {
|
||||
// Appwrite SDK 13.0 verwendet ID.unique() für die User ID
|
||||
await account.create(ID.unique(), email, password, name)
|
||||
// Login ruft automatisch ensureEmployeeExists auf
|
||||
await login(email, password)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Register error:', error)
|
||||
let errorMessage = error.message || 'Registrierung fehlgeschlagen'
|
||||
|
||||
// Bessere Fehlermeldungen
|
||||
if (errorMessage.includes('already exists') || errorMessage.includes('duplicate')) {
|
||||
errorMessage = 'Ein Benutzer mit dieser Email existiert bereits. Bitte logge dich ein.'
|
||||
} else if (errorMessage.includes('Email/Password')) {
|
||||
errorMessage = 'Email/Password Authentifizierung ist nicht aktiviert. Bitte aktiviere sie in deinem Appwrite Dashboard unter Auth → Providers.'
|
||||
} else if (errorMessage.includes('password') && errorMessage.includes('length')) {
|
||||
errorMessage = 'Das Passwort muss mindestens 8 Zeichen lang sein.'
|
||||
}
|
||||
|
||||
return { success: false, error: errorMessage }
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktion um zu prüfen ob Benutzer Admin ist
|
||||
const isAdmin = () => {
|
||||
if (!user) return false
|
||||
@@ -177,7 +149,6 @@ export function AuthProvider({ children }) {
|
||||
loading,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
isAdmin: isAdmin()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,61 @@ import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
|
||||
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
|
||||
|
||||
// Demo data for testing without Appwrite
|
||||
const lastWeek = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||||
|
||||
const DEMO_WORKORDERS = [
|
||||
{ $id: '1', woid: '10001', title: 'Server Wartung', description: 'Monatliche Wartung', status: 'Open', priority: 2, type: 'Maintenance', customerName: 'Kunde A', assignedName: 'Max M.', response: 'Onsite', $createdAt: new Date().toISOString() },
|
||||
{
|
||||
$id: 'dummy-10001',
|
||||
woid: '10001',
|
||||
topic: 'Kompletter Systemausfall - Server & Netzwerk',
|
||||
status: 'Assigned',
|
||||
priority: 4,
|
||||
type: 'Emergency Call',
|
||||
systemType: 'Server',
|
||||
responseLevel: 'Backoffice',
|
||||
serviceType: 'On Site',
|
||||
customerName: 'Kunde A',
|
||||
customerLocation: 'Hauptstraße 123, 12345 Musterstadt',
|
||||
assignedTo: 'user-max-id',
|
||||
assignedName: 'Max Mustermann',
|
||||
requestedBy: 'Dr. Anna Schmidt',
|
||||
requestedFor: 'IT-Abteilung Kunde A',
|
||||
startDate: '30.12.2025',
|
||||
startTime: '0800',
|
||||
deadline: '31.12.2025',
|
||||
endTime: '1800',
|
||||
estimate: '480',
|
||||
mailCopyTo: 'admin@kunde-a.de, it@kunde-a.de',
|
||||
sendNotification: true,
|
||||
details: `KRITISCHER SYSTEMAUSFALL - SOFORTIGE BEARBEITUNG ERFORDERLICH
|
||||
|
||||
Problembeschreibung:
|
||||
- Kompletter Serverausfall im Rechenzentrum
|
||||
- Alle Server sind offline (keine Verbindung möglich)
|
||||
- Netzwerk-Infrastruktur betroffen
|
||||
- Keine Backup-Systeme verfügbar
|
||||
|
||||
Betroffene Systeme:
|
||||
- Hauptserver (Windows Server 2022)
|
||||
- Datenbankserver (SQL Server 2019)
|
||||
- Fileserver
|
||||
- Exchange Server
|
||||
- Netzwerk-Switches
|
||||
|
||||
Auswirkungen:
|
||||
- Keine E-Mail-Kommunikation möglich
|
||||
- Alle Anwendungen offline
|
||||
- Kein Zugriff auf Datenbanken
|
||||
- Produktion steht still
|
||||
|
||||
Dringlichkeit: KRITISCH - Produktionsausfall
|
||||
|
||||
Erwartete Bearbeitungszeit: 8 Stunden
|
||||
Benötigte Ressourcen: 2 Techniker, Hardware-Ersatzteile`,
|
||||
approvalStatus: 'approved',
|
||||
$createdAt: lastWeek.toISOString(),
|
||||
createdAt: lastWeek.toISOString()
|
||||
},
|
||||
{ $id: '2', woid: '10002', title: 'Netzwerk Problem', description: 'WLAN funktioniert nicht', status: 'Occupied', priority: 3, type: 'Support', customerName: 'Kunde B', assignedName: 'Lisa S.', response: 'Remote', $createdAt: new Date().toISOString() },
|
||||
{ $id: '3', woid: '10003', title: 'Software Installation', description: 'Office 365 Setup', status: 'Assigned', priority: 1, type: 'Installation', customerName: 'Kunde C', assignedName: 'Tom K.', response: 'Onsite', $createdAt: new Date().toISOString() },
|
||||
{ $id: '4', woid: '10004', title: 'Drucker defekt', description: 'Papierstau', status: 'Awaiting', priority: 2, type: 'Hardware', customerName: 'Kunde D', assignedName: '', response: 'Pickup', $createdAt: new Date().toISOString() },
|
||||
|
||||
@@ -3,28 +3,144 @@ import { databases, DATABASE_ID, COLLECTIONS, Query, ID } from '../lib/appwrite'
|
||||
|
||||
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
|
||||
|
||||
// Demo data für Testing
|
||||
// Demo data für Testing - Vollständiges Dummy-Ticket 10001 mit allen Worksheets
|
||||
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
const twoDaysAgo = new Date(Date.now() - 48 * 60 * 60 * 1000)
|
||||
const threeDaysAgo = new Date(Date.now() - 72 * 60 * 60 * 1000)
|
||||
|
||||
const DEMO_WORKSHEETS = [
|
||||
{
|
||||
$id: '1',
|
||||
$id: 'ws-10001-001',
|
||||
wsid: '100001',
|
||||
woid: '10001',
|
||||
workorderId: '1',
|
||||
employeeId: 'emp1',
|
||||
employeeName: 'Max Müller',
|
||||
employeeShort: 'MAMU',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-max-id',
|
||||
employeeName: 'Max Mustermann',
|
||||
employeeShort: 'MM',
|
||||
serviceType: 'Remote',
|
||||
oldStatus: 'Open',
|
||||
newStatus: 'Occupied',
|
||||
oldResponseLevel: '',
|
||||
newResponseLevel: '24/7',
|
||||
totalTime: 30,
|
||||
startDate: '29.12.2025',
|
||||
startTime: '1000',
|
||||
endDate: '29.12.2025',
|
||||
endTime: '1030',
|
||||
details: 'Router neu gestartet',
|
||||
startDate: '23.12.2025',
|
||||
startTime: '0800',
|
||||
endDate: '23.12.2025',
|
||||
endTime: '0830',
|
||||
details: 'Erste Analyse durchgeführt. Server komplett offline. Keine Remote-Verbindung möglich. Vor-Ort-Einsatz erforderlich.',
|
||||
isComment: false,
|
||||
$createdAt: new Date().toISOString()
|
||||
$createdAt: threeDaysAgo.toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-002',
|
||||
wsid: '100002',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-lisa-id',
|
||||
employeeName: 'Lisa Schneider',
|
||||
employeeShort: 'LS',
|
||||
serviceType: 'On Site',
|
||||
oldStatus: 'Occupied',
|
||||
newStatus: 'Assigned',
|
||||
oldResponseLevel: '24/7',
|
||||
newResponseLevel: '24/7',
|
||||
totalTime: 120,
|
||||
startDate: '23.12.2025',
|
||||
startTime: '1000',
|
||||
endDate: '23.12.2025',
|
||||
endTime: '1200',
|
||||
details: 'Vor-Ort-Einsatz: Hardware-Check durchgeführt. Netzteil des Hauptservers defekt. Ersatzteil bestellt. Notfall-Backup-Server gestartet.',
|
||||
isComment: false,
|
||||
$createdAt: threeDaysAgo.toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-003',
|
||||
wsid: '100003',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-tom-id',
|
||||
employeeName: 'Tom Klein',
|
||||
employeeShort: 'TK',
|
||||
serviceType: 'On Site',
|
||||
oldStatus: 'Assigned',
|
||||
newStatus: 'Assigned',
|
||||
oldResponseLevel: '24/7',
|
||||
newResponseLevel: '24/7',
|
||||
totalTime: 0,
|
||||
startDate: '24.12.2025',
|
||||
startTime: '1400',
|
||||
endDate: '24.12.2025',
|
||||
endTime: '1400',
|
||||
details: 'Warte auf Ersatzteil-Lieferung. Kunde informiert. Backup-System läuft stabil.',
|
||||
isComment: true,
|
||||
$createdAt: twoDaysAgo.toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-004',
|
||||
wsid: '100004',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-max-id',
|
||||
employeeName: 'Max Mustermann',
|
||||
employeeShort: 'MM',
|
||||
serviceType: 'On Site',
|
||||
oldStatus: 'Assigned',
|
||||
newStatus: 'In Test',
|
||||
oldResponseLevel: '24/7',
|
||||
newResponseLevel: '24/7',
|
||||
totalTime: 180,
|
||||
startDate: '25.12.2025',
|
||||
startTime: '0900',
|
||||
endDate: '25.12.2025',
|
||||
endTime: '1200',
|
||||
details: 'Ersatzteil eingebaut. Server gestartet. Alle Dienste wiederhergestellt. System-Tests durchgeführt. Datenbank-Verbindungen geprüft.',
|
||||
isComment: false,
|
||||
$createdAt: twoDaysAgo.toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-005',
|
||||
wsid: '100005',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-lisa-id',
|
||||
employeeName: 'Lisa Schneider',
|
||||
employeeShort: 'LS',
|
||||
serviceType: 'Remote',
|
||||
oldStatus: 'In Test',
|
||||
newStatus: 'Awaiting',
|
||||
oldResponseLevel: '24/7',
|
||||
newResponseLevel: 'Support',
|
||||
totalTime: 45,
|
||||
startDate: '26.12.2025',
|
||||
startTime: '1000',
|
||||
endDate: '26.12.2025',
|
||||
endTime: '1045',
|
||||
details: 'Remote-Monitoring eingerichtet. Warte auf Kunden-Feedback nach 24h Testphase. Alle Systeme laufen stabil.',
|
||||
isComment: false,
|
||||
$createdAt: yesterday.toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-006',
|
||||
wsid: '100006',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-tom-id',
|
||||
employeeName: 'Tom Klein',
|
||||
employeeShort: 'TK',
|
||||
serviceType: 'COMMENT',
|
||||
oldStatus: 'Awaiting',
|
||||
newStatus: 'Closed',
|
||||
oldResponseLevel: 'Support',
|
||||
newResponseLevel: 'Backoffice',
|
||||
totalTime: 0,
|
||||
startDate: '30.12.2025',
|
||||
startTime: '0900',
|
||||
endDate: '30.12.2025',
|
||||
endTime: '0900',
|
||||
details: 'Kunde bestätigt: Alle Systeme funktionieren einwandfrei. Problem vollständig behoben. Ticket kann geschlossen werden.',
|
||||
isComment: true,
|
||||
$createdAt: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
|
||||
export function useWorksheets(woid = null) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import LetterGlitch from '../components/LetterGlitch'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isRegistering, setIsRegistering] = useState(false)
|
||||
|
||||
const { login, register } = useAuth()
|
||||
const { login } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
@@ -19,9 +18,7 @@ export default function LoginPage() {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const result = isRegistering
|
||||
? await register(email, password, name || email.split('@')[0])
|
||||
: await login(email, password)
|
||||
const result = await login(email, password)
|
||||
|
||||
if (result.success) {
|
||||
navigate('/tickets')
|
||||
@@ -31,12 +28,6 @@ export default function LoginPage() {
|
||||
|
||||
if (errorMessage.includes('Invalid credentials') || errorMessage.includes('401')) {
|
||||
errorMessage = 'Ungültige Email oder Passwort. Bitte überprüfe deine Eingaben.'
|
||||
} else if (errorMessage.includes('User already exists')) {
|
||||
errorMessage = 'Ein Benutzer mit dieser Email existiert bereits. Bitte logge dich ein.'
|
||||
setIsRegistering(false)
|
||||
} else if (errorMessage.includes('User with the same email already exists')) {
|
||||
errorMessage = 'Diese Email ist bereits registriert. Bitte logge dich ein.'
|
||||
setIsRegistering(false)
|
||||
} else if (errorMessage.includes('Email/Password') || errorMessage.includes('auth')) {
|
||||
errorMessage = 'Email/Password Authentifizierung ist möglicherweise nicht aktiviert. Bitte überprüfe deine Appwrite-Konfiguration.'
|
||||
}
|
||||
@@ -52,95 +43,100 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#f1f1f1'
|
||||
width: '100%',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div className="card" style={{ width: '400px' }}>
|
||||
<div className="card-header text-center">
|
||||
<h2>Webklar WOMS 2.0</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red text-white p-1 mb-2" style={{ borderRadius: '4px' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRegistering && (
|
||||
{/* LetterGlitch Hintergrund */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 0
|
||||
}}>
|
||||
<LetterGlitch
|
||||
glitchSpeed={50}
|
||||
centerVignette={true}
|
||||
outerVignette={false}
|
||||
smooth={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Login-Formular */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '400px',
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.5)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
background: 'transparent',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
background: 'transparent',
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h2>Webklar WOMS 2.0</h2>
|
||||
</div>
|
||||
<div style={{ padding: '16px', color: '#e2e8f0' }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red text-white p-1 mb-2" style={{ borderRadius: '4px' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Name (optional)</label>
|
||||
<label className="form-label">Email</label>
|
||||
<input
|
||||
type="text"
|
||||
type="email"
|
||||
className="form-control"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Dein Name"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
placeholder="deine@email.com"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
placeholder="deine@email.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
placeholder="••••••••"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
placeholder="••••••••"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-green"
|
||||
style={{ width: '100%', marginBottom: '10px' }}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading
|
||||
? (isRegistering ? 'Registrierung läuft...' : 'Login läuft...')
|
||||
: (isRegistering ? 'Registrieren' : 'Login')
|
||||
}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: '1px solid #ccc',
|
||||
color: '#333'
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsRegistering(!isRegistering)
|
||||
setError('')
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{isRegistering
|
||||
? 'Bereits registriert? Hier einloggen'
|
||||
: 'Noch kein Account? Hier registrieren'
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-green"
|
||||
style={{ width: '100%' }}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Login läuft...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,30 +9,50 @@ import QuickOverviewModal from '../components/QuickOverviewModal'
|
||||
|
||||
export default function TicketsPage() {
|
||||
const [limit, setLimit] = useState(10)
|
||||
// Aktive Filter (werden für API-Calls verwendet)
|
||||
const [filters, setFilters] = useState({
|
||||
status: ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info'],
|
||||
type: [],
|
||||
priority: [],
|
||||
limit: 10
|
||||
})
|
||||
// Lokale Filter-Eingaben (werden nur beim Apply angewendet)
|
||||
const [localFilters, setLocalFilters] = useState({
|
||||
woid: '',
|
||||
customer: '',
|
||||
userTopic: '',
|
||||
createdDate: '',
|
||||
type: '',
|
||||
system: '',
|
||||
priority: ''
|
||||
})
|
||||
|
||||
const { workorders, loading, error, refresh, updateWorkorder, createWorkorder } = useWorkorders(filters)
|
||||
const { customers } = useCustomers()
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [showOverviewModal, setShowOverviewModal] = useState(false)
|
||||
|
||||
const handleFilterChange = (newFilters) => {
|
||||
setFilters({ ...newFilters, limit })
|
||||
}
|
||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
|
||||
|
||||
const handleApplyFilters = () => {
|
||||
refresh()
|
||||
// Wende lokale Filter auf aktive Filter an
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
woid: localFilters.woid || undefined,
|
||||
customer: localFilters.customer || undefined,
|
||||
userTopic: localFilters.userTopic || undefined,
|
||||
createdDate: localFilters.createdDate || undefined,
|
||||
type: localFilters.type ? [localFilters.type] : [],
|
||||
system: localFilters.system ? [localFilters.system] : [],
|
||||
priority: localFilters.priority ? [parseInt(localFilters.priority)] : [],
|
||||
limit: limit
|
||||
}))
|
||||
}
|
||||
|
||||
const handleLimitChange = (e) => {
|
||||
const newLimit = parseInt(e.target.value)
|
||||
setLimit(newLimit)
|
||||
// Limit-Änderung wird sofort angewendet (kein Apply nötig)
|
||||
setFilters(prev => ({ ...prev, limit: newLimit }))
|
||||
}
|
||||
|
||||
@@ -55,133 +75,98 @@ export default function TicketsPage() {
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
{/* Sticky Header Container */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.95)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
padding: '24px',
|
||||
marginBottom: '24px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<h2 style={{
|
||||
color: 'var(--dark-text)',
|
||||
marginBottom: '12px',
|
||||
fontSize: '28px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
Active Tickets Overview
|
||||
</h2>
|
||||
<p style={{ color: '#a0aec0', marginBottom: '8px' }}>
|
||||
Work Order loading limit is set to <span style={{
|
||||
fontSize: '24px',
|
||||
color: 'var(--green-primary)',
|
||||
fontWeight: 'bold'
|
||||
}}>{limit}</span>.
|
||||
Reduce value to increase reload speed.
|
||||
</p>
|
||||
<p style={{ color: '#718096', fontSize: '12px' }}>
|
||||
Last page reload: {format(new Date(), 'dd.MM.yyyy, HH:mm:ss')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Unified Control Panel - All in One */}
|
||||
<div style={{
|
||||
background: 'rgba(45, 55, 72, 0.95)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||
overflow: 'hidden',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
{/* Extended Filters + Quick Selection - TOP */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderBottom: '1px solid rgba(16, 185, 129, 0.2)'
|
||||
{/* Compact Control Panel */}
|
||||
<div style={{
|
||||
background: 'rgba(26, 32, 44, 0.4)',
|
||||
backdropFilter: 'blur(25px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(25px) saturate(180%)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(16, 185, 129, 0.3)',
|
||||
overflow: 'hidden',
|
||||
padding: '16px',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
{/* Main Filter Row */}
|
||||
{/* Title Row */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '16px',
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<h2 style={{
|
||||
color: 'var(--dark-text)',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
margin: 0
|
||||
}}>
|
||||
Tickets
|
||||
</h2>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
CREATE NEW TICKET
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={() => setShowOverviewModal(true)}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
QUICK OVERVIEW
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{showAdvancedFilters ? '▲ Hide Filters' : '▼ Show Filters'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Search Bar */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="WOID"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.woid || ''}
|
||||
onChange={(e) => setFilters({ ...filters, woid: e.target.value })}
|
||||
value={localFilters.woid || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, woid: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Created Date"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.createdDate || ''}
|
||||
onChange={(e) => setFilters({ ...filters, createdDate: e.target.value })}
|
||||
/>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.type?.[0] || ''}
|
||||
onChange={(e) => setFilters({ ...filters, type: e.target.value ? [e.target.value] : [] })}
|
||||
>
|
||||
<option value="">Type / Location</option>
|
||||
<option>Home Office</option>
|
||||
<option>Holidays</option>
|
||||
<option>Trip</option>
|
||||
<option>Supportrequest</option>
|
||||
<option>Change Request</option>
|
||||
<option>Maintenance</option>
|
||||
<option>Project</option>
|
||||
<option>Procurement</option>
|
||||
<option>Emergency Call</option>
|
||||
</select>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.system?.[0] || ''}
|
||||
onChange={(e) => setFilters({ ...filters, system: e.target.value ? [e.target.value] : [] })}
|
||||
>
|
||||
<option value="">System</option>
|
||||
<option>Client</option>
|
||||
<option>Server</option>
|
||||
<option>Network</option>
|
||||
<option>EDI</option>
|
||||
<option>TOS</option>
|
||||
<option>Reports</option>
|
||||
<option>n/a</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Customer"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.customer || ''}
|
||||
onChange={(e) => setFilters({ ...filters, customer: e.target.value })}
|
||||
value={localFilters.customer || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, customer: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Topic / User"
|
||||
placeholder="User"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.userTopic || ''}
|
||||
onChange={(e) => setFilters({ ...filters, userTopic: e.target.value })}
|
||||
value={localFilters.userTopic || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, userTopic: e.target.value })}
|
||||
/>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={filters.priority?.[0] ?? ''}
|
||||
onChange={(e) => setFilters({ ...filters, priority: e.target.value ? [parseInt(e.target.value)] : [] })}
|
||||
>
|
||||
<option value="">Priority</option>
|
||||
<option value="0">None</option>
|
||||
<option value="1">Low</option>
|
||||
<option value="2">Medium</option>
|
||||
<option value="3">High</option>
|
||||
<option value="4">Critical</option>
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={handleApplyFilters}
|
||||
@@ -191,89 +176,164 @@ export default function TicketsPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quick Selection Buttons */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
paddingTop: '16px',
|
||||
borderTop: '1px solid rgba(16, 185, 129, 0.1)'
|
||||
}}>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, type: ['Procurement'] })); handleApplyFilters(); }}
|
||||
>
|
||||
Procurements
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, priority: [4] })); handleApplyFilters(); }}
|
||||
>
|
||||
Criticals
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setFilters(prev => ({ ...prev, priority: [3] })); handleApplyFilters(); }}
|
||||
>
|
||||
Highs
|
||||
</button>
|
||||
{/* Advanced Filters - Collapsible */}
|
||||
{showAdvancedFilters && (
|
||||
<div style={{
|
||||
width: '1px',
|
||||
height: '32px',
|
||||
background: 'rgba(16, 185, 129, 0.3)',
|
||||
margin: '0 8px'
|
||||
}}></div>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setLimit(10); setFilters(prev => ({ ...prev, limit: 10 })) }}
|
||||
>
|
||||
10
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => { setLimit(25); setFilters(prev => ({ ...prev, limit: 25 })) }}
|
||||
>
|
||||
25
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
marginTop: '16px',
|
||||
paddingTop: '16px',
|
||||
borderTop: '1px solid rgba(16, 185, 129, 0.2)'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Created Date"
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={localFilters.createdDate || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, createdDate: e.target.value })}
|
||||
/>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={localFilters.type || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, type: e.target.value })}
|
||||
>
|
||||
<option value="">Type / Location</option>
|
||||
<option>Home Office</option>
|
||||
<option>Holidays</option>
|
||||
<option>Trip</option>
|
||||
<option>Supportrequest</option>
|
||||
<option>Change Request</option>
|
||||
<option>Maintenance</option>
|
||||
<option>Project</option>
|
||||
<option>Procurement</option>
|
||||
<option>Emergency Call</option>
|
||||
</select>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={localFilters.system || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, system: e.target.value })}
|
||||
>
|
||||
<option value="">System</option>
|
||||
<option>Client</option>
|
||||
<option>Server</option>
|
||||
<option>Network</option>
|
||||
<option>EDI</option>
|
||||
<option>TOS</option>
|
||||
<option>Reports</option>
|
||||
<option>n/a</option>
|
||||
</select>
|
||||
<select
|
||||
className="form-control"
|
||||
style={{ margin: 0 }}
|
||||
value={localFilters.priority || ''}
|
||||
onChange={(e) => setLocalFilters({ ...localFilters, priority: e.target.value })}
|
||||
>
|
||||
<option value="">Priority</option>
|
||||
<option value="0">None</option>
|
||||
<option value="1">Low</option>
|
||||
<option value="2">Medium</option>
|
||||
<option value="3">High</option>
|
||||
<option value="4">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Slider + Action Buttons - BOTTOM */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<div style={{ flex: '1', minWidth: '200px' }}>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="50"
|
||||
value={limit}
|
||||
className="slider"
|
||||
onChange={handleLimitChange}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'nowrap' }}>
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
CREATE NEW TICKET
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-dark"
|
||||
onClick={() => setShowOverviewModal(true)}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
QUICK OVERVIEW
|
||||
</button>
|
||||
</div>
|
||||
{/* Quick Selection Buttons */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => {
|
||||
setLocalFilters(prev => ({ ...prev, type: 'Procurement' }))
|
||||
setFilters(prev => ({ ...prev, type: ['Procurement'] }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
Procurements
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => {
|
||||
setLocalFilters(prev => ({ ...prev, priority: '4' }))
|
||||
setFilters(prev => ({ ...prev, priority: [4] }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
Criticals
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => {
|
||||
setLocalFilters(prev => ({ ...prev, priority: '3' }))
|
||||
setFilters(prev => ({ ...prev, priority: [3] }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
Highs
|
||||
</button>
|
||||
<div style={{
|
||||
width: '1px',
|
||||
height: '32px',
|
||||
background: 'rgba(16, 185, 129, 0.3)',
|
||||
margin: '0 8px'
|
||||
}}></div>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => {
|
||||
setLimit(10)
|
||||
setFilters(prev => ({ ...prev, limit: 10 }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
10
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-green"
|
||||
onClick={() => {
|
||||
setLimit(25)
|
||||
setFilters(prev => ({ ...prev, limit: 25 }))
|
||||
setTimeout(() => refresh(), 0)
|
||||
}}
|
||||
>
|
||||
25
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Slider */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span style={{ color: 'var(--dark-text)', minWidth: '120px' }}>
|
||||
Load Limit: {limit}
|
||||
</span>
|
||||
<div style={{ flex: '1' }}>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="50"
|
||||
value={limit}
|
||||
className="slider"
|
||||
onChange={handleLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -329,6 +329,7 @@ textarea.form-control {
|
||||
.ticket-row td {
|
||||
background: rgba(45, 55, 72, 0.95);
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Second row of ticket (no top border to avoid line in rowspan cells) */
|
||||
@@ -345,6 +346,60 @@ textarea.form-control {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Collapsed Ticket - All 4 outermost corners rounded */
|
||||
/* WOID cell (left side) - top and bottom corners */
|
||||
.ticket-collapsed .ticket-id {
|
||||
border-top-left-radius: 12px !important;
|
||||
border-bottom-left-radius: 12px !important;
|
||||
}
|
||||
|
||||
/* Lock cell (right side) - top and bottom corners */
|
||||
.ticket-collapsed td.bg-dark-grey {
|
||||
border-top-right-radius: 12px !important;
|
||||
border-bottom-right-radius: 12px !important;
|
||||
}
|
||||
|
||||
/* Expanded Ticket - Only top 2 outermost corners rounded */
|
||||
/* WOID cell (left side) - only top corner, bottom stays square */
|
||||
.ticket-expanded .ticket-id {
|
||||
border-top-left-radius: 12px !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Lock cell (right side) - only top corner, bottom stays square */
|
||||
.ticket-expanded td.bg-dark-grey {
|
||||
border-top-right-radius: 12px !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Worksheet Expansion with Animation */
|
||||
.worksheet-expansion {
|
||||
animation: slideDown 0.4s ease-out;
|
||||
}
|
||||
|
||||
.worksheet-cell {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.worksheet-cell .card {
|
||||
border-radius: 0 0 12px 12px !important;
|
||||
margin-top: -1px;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.ticket-id {
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%) !important;
|
||||
color: #fff;
|
||||
@@ -354,6 +409,16 @@ textarea.form-control {
|
||||
font-weight: bold;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(16, 185, 129, 0.2) !important;
|
||||
transition: border-radius 0.3s ease;
|
||||
}
|
||||
|
||||
/* WOID rounded when collapsed, square bottom when expanded */
|
||||
.ticket-id:not(.ticket-id-expanded) {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.ticket-id-expanded {
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.ticket-time {
|
||||
@@ -385,6 +450,21 @@ textarea.form-control {
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown .btn {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: inherit !important;
|
||||
color: inherit !important;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
@@ -396,6 +476,8 @@ textarea.form-control {
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 6px;
|
||||
z-index: 100;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.dropdown:hover .dropdown-content {
|
||||
|
||||
233
src/utils/createDummyTicket.js
Normal file
233
src/utils/createDummyTicket.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Erstellt ein vollständiges Dummy-Ticket mit WOID 10001
|
||||
* Zeigt alle möglichen Funktionen, Felder und Kombinationen
|
||||
*/
|
||||
|
||||
export function createDummyTicket10001() {
|
||||
const now = new Date()
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
const lastWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
|
||||
// Haupt-Ticket mit allen Feldern ausgefüllt
|
||||
const dummyTicket = {
|
||||
$id: 'dummy-10001',
|
||||
woid: '10001',
|
||||
topic: 'Kompletter Systemausfall - Server & Netzwerk',
|
||||
status: 'Assigned',
|
||||
priority: 4, // Critical
|
||||
type: 'Emergency Call',
|
||||
systemType: 'Server',
|
||||
responseLevel: '24/7',
|
||||
serviceType: 'On Site',
|
||||
customerId: 'customer-a-id',
|
||||
customerName: 'Kunde A',
|
||||
customerLocation: 'Hauptstraße 123, 12345 Musterstadt',
|
||||
assignedTo: 'user-max-id', // Max Mustermann
|
||||
requestedBy: 'Dr. Anna Schmidt',
|
||||
requestedFor: 'IT-Abteilung Kunde A',
|
||||
startDate: '30.12.2025',
|
||||
startTime: '0800',
|
||||
deadline: '31.12.2025',
|
||||
endTime: '1800',
|
||||
estimate: '480',
|
||||
mailCopyTo: 'admin@kunde-a.de, it@kunde-a.de',
|
||||
sendNotification: true,
|
||||
details: `KRITISCHER SYSTEMAUSFALL - SOFORTIGE BEARBEITUNG ERFORDERLICH
|
||||
|
||||
Problembeschreibung:
|
||||
- Kompletter Serverausfall im Rechenzentrum
|
||||
- Alle Server sind offline (keine Verbindung möglich)
|
||||
- Netzwerk-Infrastruktur betroffen
|
||||
- Keine Backup-Systeme verfügbar
|
||||
|
||||
Betroffene Systeme:
|
||||
- Hauptserver (Windows Server 2022)
|
||||
- Datenbankserver (SQL Server 2019)
|
||||
- Fileserver
|
||||
- Exchange Server
|
||||
- Netzwerk-Switches
|
||||
|
||||
Auswirkungen:
|
||||
- Keine E-Mail-Kommunikation möglich
|
||||
- Alle Anwendungen offline
|
||||
- Kein Zugriff auf Datenbanken
|
||||
- Produktion steht still
|
||||
|
||||
Dringlichkeit: KRITISCH - Produktionsausfall
|
||||
|
||||
Erwartete Bearbeitungszeit: 8 Stunden
|
||||
Benötigte Ressourcen: 2 Techniker, Hardware-Ersatzteile`,
|
||||
approvalStatus: 'approved',
|
||||
createdAt: lastWeek.toISOString(),
|
||||
$createdAt: lastWeek.toISOString()
|
||||
}
|
||||
|
||||
// Mehrere Worksheets mit verschiedenen Status-Änderungen und Benutzern
|
||||
const dummyWorksheets = [
|
||||
{
|
||||
$id: 'ws-10001-001',
|
||||
wsid: '100001',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-max-id',
|
||||
employeeName: 'Max Mustermann',
|
||||
employeeShort: 'MM',
|
||||
serviceType: 'Remote',
|
||||
oldStatus: 'Open',
|
||||
newStatus: 'Occupied',
|
||||
oldResponseLevel: '',
|
||||
newResponseLevel: '24/7',
|
||||
totalTime: 30,
|
||||
startDate: '23.12.2025',
|
||||
startTime: '0800',
|
||||
endDate: '23.12.2025',
|
||||
endTime: '0830',
|
||||
details: 'Erste Analyse durchgeführt. Server komplett offline. Keine Remote-Verbindung möglich. Vor-Ort-Einsatz erforderlich.',
|
||||
isComment: false,
|
||||
createdAt: yesterday.toISOString(),
|
||||
$createdAt: yesterday.toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-002',
|
||||
wsid: '100002',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-lisa-id',
|
||||
employeeName: 'Lisa Schneider',
|
||||
employeeShort: 'LS',
|
||||
serviceType: 'On Site',
|
||||
oldStatus: 'Occupied',
|
||||
newStatus: 'Assigned',
|
||||
oldResponseLevel: '24/7',
|
||||
newResponseLevel: '24/7',
|
||||
totalTime: 120,
|
||||
startDate: '23.12.2025',
|
||||
startTime: '1000',
|
||||
endDate: '23.12.2025',
|
||||
endTime: '1200',
|
||||
details: 'Vor-Ort-Einsatz: Hardware-Check durchgeführt. Netzteil des Hauptservers defekt. Ersatzteil bestellt. Notfall-Backup-Server gestartet.',
|
||||
isComment: false,
|
||||
createdAt: yesterday.toISOString(),
|
||||
$createdAt: yesterday.toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-003',
|
||||
wsid: '100003',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-tom-id',
|
||||
employeeName: 'Tom Klein',
|
||||
employeeShort: 'TK',
|
||||
serviceType: 'On Site',
|
||||
oldStatus: 'Assigned',
|
||||
newStatus: 'Assigned',
|
||||
oldResponseLevel: '24/7',
|
||||
newResponseLevel: '24/7',
|
||||
totalTime: 0,
|
||||
startDate: '24.12.2025',
|
||||
startTime: '1400',
|
||||
endDate: '24.12.2025',
|
||||
endTime: '1400',
|
||||
details: 'Warte auf Ersatzteil-Lieferung. Kunde informiert. Backup-System läuft stabil.',
|
||||
isComment: true,
|
||||
createdAt: new Date(yesterday.getTime() - 12 * 60 * 60 * 1000).toISOString(),
|
||||
$createdAt: new Date(yesterday.getTime() - 12 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-004',
|
||||
wsid: '100004',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-max-id',
|
||||
employeeName: 'Max Mustermann',
|
||||
employeeShort: 'MM',
|
||||
serviceType: 'On Site',
|
||||
oldStatus: 'Assigned',
|
||||
newStatus: 'In Test',
|
||||
oldResponseLevel: '24/7',
|
||||
newResponseLevel: '24/7',
|
||||
totalTime: 180,
|
||||
startDate: '25.12.2025',
|
||||
startTime: '0900',
|
||||
endDate: '25.12.2025',
|
||||
endTime: '1200',
|
||||
details: 'Ersatzteil eingebaut. Server gestartet. Alle Dienste wiederhergestellt. System-Tests durchgeführt. Datenbank-Verbindungen geprüft.',
|
||||
isComment: false,
|
||||
createdAt: new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString(),
|
||||
$createdAt: new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-005',
|
||||
wsid: '100005',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-lisa-id',
|
||||
employeeName: 'Lisa Schneider',
|
||||
employeeShort: 'LS',
|
||||
serviceType: 'Remote',
|
||||
oldStatus: 'In Test',
|
||||
newStatus: 'Awaiting',
|
||||
oldResponseLevel: '24/7',
|
||||
newResponseLevel: 'Support',
|
||||
totalTime: 45,
|
||||
startDate: '26.12.2025',
|
||||
startTime: '1000',
|
||||
endDate: '26.12.2025',
|
||||
endTime: '1045',
|
||||
details: 'Remote-Monitoring eingerichtet. Warte auf Kunden-Feedback nach 24h Testphase. Alle Systeme laufen stabil.',
|
||||
isComment: false,
|
||||
createdAt: new Date(yesterday.getTime() - 48 * 60 * 60 * 1000).toISOString(),
|
||||
$createdAt: new Date(yesterday.getTime() - 48 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
$id: 'ws-10001-006',
|
||||
wsid: '100006',
|
||||
woid: '10001',
|
||||
workorderId: 'dummy-10001',
|
||||
employeeId: 'user-tom-id',
|
||||
employeeName: 'Tom Klein',
|
||||
employeeShort: 'TK',
|
||||
serviceType: 'COMMENT',
|
||||
oldStatus: 'Awaiting',
|
||||
newStatus: 'Closed',
|
||||
oldResponseLevel: 'Support',
|
||||
newResponseLevel: 'Backoffice',
|
||||
totalTime: 0,
|
||||
startDate: '30.12.2025',
|
||||
startTime: '0900',
|
||||
endDate: '30.12.2025',
|
||||
endTime: '0900',
|
||||
details: 'Kunde bestätigt: Alle Systeme funktionieren einwandfrei. Problem vollständig behoben. Ticket kann geschlossen werden.',
|
||||
isComment: true,
|
||||
createdAt: now.toISOString(),
|
||||
$createdAt: now.toISOString()
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
ticket: dummyTicket,
|
||||
worksheets: dummyWorksheets
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt das Dummy-Ticket zu den Demo-Daten hinzu
|
||||
*/
|
||||
export function addDummyTicketToDemo(workorders, worksheets) {
|
||||
const { ticket, worksheets: ticketWorksheets } = createDummyTicket10001()
|
||||
|
||||
// Prüfe ob Ticket bereits existiert
|
||||
const exists = workorders.some(wo => wo.woid === '10001')
|
||||
if (exists) {
|
||||
console.log('Dummy-Ticket 10001 existiert bereits')
|
||||
return { workorders, worksheets }
|
||||
}
|
||||
|
||||
return {
|
||||
workorders: [ticket, ...workorders],
|
||||
worksheets: [...ticketWorksheets, ...worksheets]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user