first commit
This commit is contained in:
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
**/build/
|
||||||
|
**/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/app/android/app/debug
|
||||||
|
/app/android/app/profile
|
||||||
|
/app/android/app/release
|
||||||
|
|
||||||
|
# Java / Maven / Vaadin
|
||||||
|
**/target/
|
||||||
|
**/node_modules/
|
||||||
|
backend/.vaadin/
|
||||||
47
.vscode/check_emulator.sh
vendored
Executable file
47
.vscode/check_emulator.sh
vendored
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Skript zum Prüfen ob Emulator läuft
|
||||||
|
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo "🔍 Prüfe auf Android Emulator..."
|
||||||
|
|
||||||
|
# Prüfe ob Emulator läuft
|
||||||
|
DEVICE=$(flutter devices 2>/dev/null | grep "emulator" | head -1)
|
||||||
|
|
||||||
|
if [ -n "$DEVICE" ]; then
|
||||||
|
echo -e "${GREEN}✅ Emulator läuft:${NC}"
|
||||||
|
echo " $DEVICE"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Kein Emulator gefunden!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Verfügbare Emulatoren:"
|
||||||
|
flutter emulators
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Starte Emulator...${NC}"
|
||||||
|
flutter emulators --launch Pixel_8_Pro_API_29 2>/dev/null &
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Warte auf Emulator-Start (ca. 30-60 Sekunden)..."
|
||||||
|
|
||||||
|
# Warte bis Emulator verfügbar ist
|
||||||
|
for i in {1..30}; do
|
||||||
|
sleep 2
|
||||||
|
DEVICE=$(flutter devices 2>/dev/null | grep "emulator" | head -1)
|
||||||
|
if [ -n "$DEVICE" ]; then
|
||||||
|
echo -e "${GREEN}✅ Emulator bereit!${NC}"
|
||||||
|
echo " $DEVICE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo -n "."
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}⚠️ Emulator startet noch...${NC}"
|
||||||
|
echo " Bitte warten und dann nochmal versuchen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
26
.vscode/extensions.json
vendored
Normal file
26
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// Flutter & Dart
|
||||||
|
"Dart-Code.flutter",
|
||||||
|
"Dart-Code.dart-code",
|
||||||
|
|
||||||
|
// UI-Entwicklung
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"usernamehw.errorlens",
|
||||||
|
"PKief.material-icon-theme",
|
||||||
|
|
||||||
|
// Produktivität
|
||||||
|
"formulahendry.auto-rename-tag",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"wmaurer.change-case",
|
||||||
|
"Tyriar.sort-lines",
|
||||||
|
|
||||||
|
// Flutter Tools
|
||||||
|
"alexisvt.flutter-snippets",
|
||||||
|
"Nash.awesome-flutter-snippets",
|
||||||
|
"miquelddg.dart-barrel-file-generator"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"vscjava.vscode-java-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
89
.vscode/launch.json
vendored
Normal file
89
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "🚀 HHA - Android Emulator (Debug)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/app/lib/main.dart",
|
||||||
|
"flutterMode": "debug",
|
||||||
|
"deviceId": "emulator-5554",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"args": [
|
||||||
|
"--enable-software-rendering",
|
||||||
|
"--no-enable-impeller"
|
||||||
|
],
|
||||||
|
"console": "debugConsole",
|
||||||
|
"toolArgs": [
|
||||||
|
"--hot"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}/app",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "🚀 HHA - Android Emulator (Verbose)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/app/lib/main.dart",
|
||||||
|
"flutterMode": "debug",
|
||||||
|
"deviceId": "emulator-5554",
|
||||||
|
"preLaunchTask": "flutter: pub get",
|
||||||
|
"args": [
|
||||||
|
"--enable-software-rendering",
|
||||||
|
"--no-enable-impeller",
|
||||||
|
"--verbose"
|
||||||
|
],
|
||||||
|
"console": "terminal",
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "📱 HHA - Android Device (Debug)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/app/lib/main.dart",
|
||||||
|
"flutterMode": "debug",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "🌐 HHA - Chrome (Debug)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/app/lib/main.dart",
|
||||||
|
"flutterMode": "debug",
|
||||||
|
"deviceId": "chrome",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "📊 HHA - Profile Mode",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/app/lib/main.dart",
|
||||||
|
"flutterMode": "profile",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "⚡ HHA - Release Mode",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/app/lib/main.dart",
|
||||||
|
"flutterMode": "release",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "🔌 HHA - Attach to Running App",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "attach",
|
||||||
|
"program": "${workspaceFolder}/app/lib/main.dart",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
60
.vscode/settings.json
vendored
Normal file
60
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
// Dart & Flutter
|
||||||
|
"dart.flutterSdkPath": null,
|
||||||
|
"dart.openDevTools": "flutter",
|
||||||
|
"dart.previewFlutterUiGuides": true,
|
||||||
|
"dart.previewFlutterUiGuidesCustomTracking": true,
|
||||||
|
"dart.showInspectorNotificationsForWidgetErrors": true,
|
||||||
|
"dart.hotReloadOnSave": "always",
|
||||||
|
"dart.flutterHotReloadOnSave": "always",
|
||||||
|
"dart.debugExternalPackageLibraries": false,
|
||||||
|
"dart.debugSdkLibraries": false,
|
||||||
|
|
||||||
|
// Formatierung
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnType": true,
|
||||||
|
"editor.rulers": [80, 120],
|
||||||
|
"editor.wordWrap": "wordWrapColumn",
|
||||||
|
"editor.wordWrapColumn": 120,
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"editor.guides.bracketPairs": true,
|
||||||
|
|
||||||
|
// Dart-spezifisch
|
||||||
|
"[dart]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnType": true,
|
||||||
|
"editor.rulers": [80],
|
||||||
|
"editor.selectionHighlight": false,
|
||||||
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
|
"editor.suggestSelection": "first",
|
||||||
|
"editor.tabCompletion": "onlySnippets",
|
||||||
|
"editor.wordBasedSuggestions": "off"
|
||||||
|
},
|
||||||
|
|
||||||
|
// File Explorer
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.dart_tool": true,
|
||||||
|
"**/.packages": true,
|
||||||
|
"**/.pub": true,
|
||||||
|
"**/build": true,
|
||||||
|
"**/*.freezed.dart": true,
|
||||||
|
"**/*.g.dart": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Linting
|
||||||
|
"dart.analysisExcludedFolders": [
|
||||||
|
"app/build",
|
||||||
|
"app/.dart_tool",
|
||||||
|
"backend/target"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Emulator
|
||||||
|
"dart.flutterRunAdditionalArgs": [
|
||||||
|
"--enable-software-rendering"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
"dart.testAdditionalArgs": [
|
||||||
|
"--concurrency=4"
|
||||||
|
]
|
||||||
|
}
|
||||||
177
.vscode/tasks.json
vendored
Normal file
177
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "flutter: clean",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": ["clean"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "flutter: pub get",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": ["pub", "get"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "check: emulator",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "${workspaceFolder}/.vscode/check_emulator.sh",
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"clear": true
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "flutter: build debug",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"apk",
|
||||||
|
"--debug",
|
||||||
|
"-t",
|
||||||
|
"lib/main.dart"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true
|
||||||
|
},
|
||||||
|
"problemMatcher": {
|
||||||
|
"pattern": {
|
||||||
|
"regexp": "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "flutter: run (terminal)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"-d",
|
||||||
|
"emulator-5554",
|
||||||
|
"--debug"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"isBackground": true,
|
||||||
|
"dependsOn": ["check: emulator", "flutter: pub get"],
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"panel": "shared",
|
||||||
|
"showReuseMessage": false,
|
||||||
|
"clear": true
|
||||||
|
},
|
||||||
|
"problemMatcher": {
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": ".",
|
||||||
|
"file": 1,
|
||||||
|
"location": 2,
|
||||||
|
"message": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": "^\\[\\+\\]",
|
||||||
|
"endsPattern": "^\\[.*\\]\\s*Synced.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "flutter: devices",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": ["devices"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"clear": true
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "flutter: doctor",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": ["doctor", "-v"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared",
|
||||||
|
"clear": true
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "prelaunch: check deps",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": ["pub", "get"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/app"
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"echo": false,
|
||||||
|
"reveal": "silent",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# HHA Repository
|
||||||
|
|
||||||
|
Dieses Repository ist jetzt als Monorepo fuer zwei getrennte Anwendungen aufgebaut:
|
||||||
|
|
||||||
|
- `app/` enthaelt die Flutter-App
|
||||||
|
- `backend/` enthaelt das Spring Boot + Vaadin Backoffice
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
```text
|
||||||
|
HHA/
|
||||||
|
|- app/
|
||||||
|
| |- lib/
|
||||||
|
| |- android/
|
||||||
|
| |- pubspec.yaml
|
||||||
|
| `- README.md
|
||||||
|
`- backend/
|
||||||
|
|- src/main/java/
|
||||||
|
|- src/main/resources/
|
||||||
|
|- pom.xml
|
||||||
|
`- mvnw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
Flutter-App:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
flutter pub get
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- Flutter-spezifische Quellen, Build-Dateien und Artefakte liegen unter `app/`.
|
||||||
|
- Das Backend bleibt als eigenstaendiges Maven-Modul unter `backend/`.
|
||||||
|
- Weitere Details zur mobilen App stehen in `app/README.md`.
|
||||||
30
app/.metadata
Normal file
30
app/.metadata
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "8b872868494e429d94fa06dca855c306438b22c0"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
- platform: android
|
||||||
|
create_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
base_revision: 8b872868494e429d94fa06dca855c306438b22c0
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
89
app/.vscode/launch.json
vendored
Normal file
89
app/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "🚀 HHA - Android Emulator (Debug)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "lib/main.dart",
|
||||||
|
"flutterMode": "debug",
|
||||||
|
"deviceId": "emulator-5554",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"args": [
|
||||||
|
"--enable-software-rendering",
|
||||||
|
"--no-enable-impeller"
|
||||||
|
],
|
||||||
|
"console": "debugConsole",
|
||||||
|
"toolArgs": [
|
||||||
|
"--hot"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "🚀 HHA - Android Emulator (Verbose)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "lib/main.dart",
|
||||||
|
"flutterMode": "debug",
|
||||||
|
"deviceId": "emulator-5554",
|
||||||
|
"preLaunchTask": "flutter: pub get",
|
||||||
|
"args": [
|
||||||
|
"--enable-software-rendering",
|
||||||
|
"--no-enable-impeller",
|
||||||
|
"--verbose"
|
||||||
|
],
|
||||||
|
"console": "terminal",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "📱 HHA - Android Device (Debug)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "lib/main.dart",
|
||||||
|
"flutterMode": "debug",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "🌐 HHA - Chrome (Debug)",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "lib/main.dart",
|
||||||
|
"flutterMode": "debug",
|
||||||
|
"deviceId": "chrome",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "📊 HHA - Profile Mode",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "lib/main.dart",
|
||||||
|
"flutterMode": "profile",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "⚡ HHA - Release Mode",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "lib/main.dart",
|
||||||
|
"flutterMode": "release",
|
||||||
|
"preLaunchTask": "prelaunch: check deps",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "🔌 HHA - Attach to Running App",
|
||||||
|
"type": "dart",
|
||||||
|
"request": "attach",
|
||||||
|
"program": "lib/main.dart",
|
||||||
|
"console": "debugConsole",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
app/.vscode/tasks.json
vendored
Normal file
38
app/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "flutter: pub get",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": ["pub", "get"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"presentation": {
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "prelaunch: check deps",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": ["pub", "get"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"echo": false,
|
||||||
|
"reveal": "silent",
|
||||||
|
"focus": false,
|
||||||
|
"panel": "shared"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
381
app/ANALYSE.md
Normal file
381
app/ANALYSE.md
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
# HHA Lua Workflow Analyse
|
||||||
|
|
||||||
|
## Überblick
|
||||||
|
|
||||||
|
Die analysierten Lua-Skripte implementieren einen umfangreichen Logistik-Workflow für die Hamburger Hochbahn (HHA). Die App verwaltet den Transport und die Zustandsänderungen verschiedener Objekte zwischen verschiedenen Stationen.
|
||||||
|
|
||||||
|
## Architektur der Lua-App
|
||||||
|
|
||||||
|
### Dateistruktur
|
||||||
|
- `hha_mainscript.lua` - Initialisierung und Barcode-Handling
|
||||||
|
- `hha_main.lua` - Hauptlogik mit UI und State-Machine
|
||||||
|
- `hha_db_migration_V0_initial.lua` - Datenbank-Schema
|
||||||
|
|
||||||
|
### UI-Framework
|
||||||
|
Die App verwendet ein proprietäres UI-Framework (ASUI) mit folgenden Komponenten:
|
||||||
|
- ASUIStackLayout - Layout-Container
|
||||||
|
- ASUILabel - Textanzeige
|
||||||
|
- ASUIButton - Schaltflächen
|
||||||
|
- ASUIScrollView - Scrollbare Bereiche
|
||||||
|
- ASUIPerfListViewM - Performante Listen
|
||||||
|
- ASUIBoxView / ASUIRelativeLayout - Container
|
||||||
|
|
||||||
|
## Objekt-Typen
|
||||||
|
|
||||||
|
### 1. Geldkassetten (GK)
|
||||||
|
- **Typ-ID**: 3
|
||||||
|
- **Codes**: MEKxxx, BEKxxx
|
||||||
|
- **Subtypen**: meka, mekb, mekc, mekd, beka, bekb, bekc, bekd
|
||||||
|
- **Workflow**: Lager → Fahrzeug → Station → Fahrscheinautomat → Geldinstitut
|
||||||
|
|
||||||
|
### 2. HP Patronen (HP)
|
||||||
|
- **Typ-ID**: 4
|
||||||
|
- **Codes**: HOPxxx, H1Pxxx, H2Pxxx, H3Pxxx
|
||||||
|
- **Subtypen**: hp1a, hp1b, hp1c, hp2a, hp2b, hp2c, hp3a, hp3b, hp3c
|
||||||
|
- **Workflow**: Lager → Fahrzeug → Fahrscheinautomat
|
||||||
|
|
||||||
|
### 3. Fahrkartenrollen (FR)
|
||||||
|
- **Typ-ID**: 7
|
||||||
|
- **Codes**: FRxxx
|
||||||
|
- **Subtypen**: fra
|
||||||
|
- **Workflow**: Lager → Fahrzeug → Fahrscheinautomat
|
||||||
|
|
||||||
|
### 4. Safebags (SB)
|
||||||
|
- **Typ-ID**: 5
|
||||||
|
- **Workflow**: Versorgungsstelle → Geldinstitut
|
||||||
|
|
||||||
|
### 5. Abfallbehälter (ABS)
|
||||||
|
- **Typ-ID**: 9
|
||||||
|
- **Workflow**: Versorgungsstelle → Dienststelle
|
||||||
|
|
||||||
|
### 6. Container (CNTR)
|
||||||
|
- **Typ-ID**: ?
|
||||||
|
- **Subtypen**: cntra (Geldinstitut), cntrb (Dienststelle)
|
||||||
|
- **Workflow**: Sammlung mehrerer Objekte
|
||||||
|
|
||||||
|
## Zustände (States)
|
||||||
|
|
||||||
|
### Hauptzustände
|
||||||
|
```
|
||||||
|
unknown → to_delivery → delivery → station → in_fa
|
||||||
|
↓
|
||||||
|
┌───────────────────────┼───────────────────────┐
|
||||||
|
↓ ↓ ↓
|
||||||
|
ret_gi ret_fail ret_ds
|
||||||
|
↓ ↓ ↓
|
||||||
|
ret_gi_fzg ret_fail_fzg ret_ds_fzg
|
||||||
|
↓ ↓ ↓
|
||||||
|
fin_gi ret_fail_stk ret_ds_stk
|
||||||
|
↓ ↓
|
||||||
|
fin_ds_fail fin_ds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zustandsbeschreibungen
|
||||||
|
|
||||||
|
| State | Beschreibung | Farbe |
|
||||||
|
|-------|-------------|-------|
|
||||||
|
| `unknown` | Unbekannt/Neu | Weiß |
|
||||||
|
| `to_delivery` | Zum Fahrzeug | Grau |
|
||||||
|
| `delivery` | Im Fahrzeug | Grau |
|
||||||
|
| `station` | An Station | Gelb |
|
||||||
|
| `in_fa` | Im Fahrscheinautomat | Grün |
|
||||||
|
| `in_vs` | In Versorgungsstelle | Gelb |
|
||||||
|
| `ret_fail` | Fehlerhaft zur Dienststelle | Rot |
|
||||||
|
| `ret_fail_fzg` | Fehlerhaft im Fahrzeug | Rot |
|
||||||
|
| `ret_fail_stk` | Fehlerhaft in Dienststelle | Rot |
|
||||||
|
| `ret_gi` | Zum Geldinstitut | Hellblau |
|
||||||
|
| `ret_gi_fzg` | Zum Geldinstitut (Fzg) | Hellblau |
|
||||||
|
| `ret_gi_stk` | Zum Geldinstitut (Stk) | Hellblau |
|
||||||
|
| `fin_gi` | Abgeschlossen im Geldinstitut | Blau |
|
||||||
|
| `ret_ds` | Zur Dienststelle | Hellblau |
|
||||||
|
| `ret_ds_fzg` | Zur Dienststelle (Fzg) | Hellblau |
|
||||||
|
| `ret_ds_stk` | Zur Dienststelle (Stk) | Hellblau |
|
||||||
|
| `ret_ds_err` | Fehlerhaft zur Dienststelle | Hellblau |
|
||||||
|
| `fin_ds` | Abgeschlossen in Dienststelle | Blau |
|
||||||
|
| `fin_ds_fail` | Fehlerhaft abgeschlossen | Blau |
|
||||||
|
| `fin_ds_err` | Fehlerhaft abgeschlossen | Blau |
|
||||||
|
| `hdl` | Handel | Grau |
|
||||||
|
| `ret_ds_empty` | Leer zur Dienststelle | Hellblau |
|
||||||
|
|
||||||
|
## Tour-Stationen
|
||||||
|
|
||||||
|
### Stationstypen
|
||||||
|
|
||||||
|
| Type | Beschreibung | Icon |
|
||||||
|
|------|-------------|------|
|
||||||
|
| `stock_start` | Lager - Beladung | Lager |
|
||||||
|
| `stock` | Lager | Lager |
|
||||||
|
| `stock_end` | Lager - Rückgabe | Lager |
|
||||||
|
| `start` | Dienststelle - Start | Dienststelle |
|
||||||
|
| `end` | Dienststelle - Ende | Dienststelle |
|
||||||
|
| `st` | Haltestelle | Haltestelle |
|
||||||
|
| `hls` | Hochbahnstation | HLS |
|
||||||
|
| `fsa` | Fahrscheinautomat | FSA |
|
||||||
|
| `vs` | Versorgungsstelle | VS |
|
||||||
|
| `gi` | Geldinstitut | Bank |
|
||||||
|
| `veh_start` | Fahrzeug - Beladung | Fahrzeug |
|
||||||
|
| `veh` | Fahrzeug | Fahrzeug |
|
||||||
|
| `veh_bulk` | Fahrzeug - Massenladung | Fahrzeug |
|
||||||
|
| `veh_vs` | Fahrzeug - Versorgung | Fahrzeug |
|
||||||
|
| `veh_end` | Fahrzeug - Entladung | Fahrzeug |
|
||||||
|
|
||||||
|
### Seiten (Pages)
|
||||||
|
Jede Tour hat mehrere Seiten, die bestimmte Aktionen definieren:
|
||||||
|
- **Page-ID**: Eindeutige Identifikation
|
||||||
|
- **Code**: Barcode zum Aktivieren der Seite
|
||||||
|
- **Label**: Anzeigename
|
||||||
|
- **Pickup**: Objekte, die aufgenommen werden sollen
|
||||||
|
- **Swap**: Objekte, die ausgetauscht werden sollen
|
||||||
|
|
||||||
|
## State Machines
|
||||||
|
|
||||||
|
### 1. Stock Start State Machine
|
||||||
|
```
|
||||||
|
unknown → to_delivery (Auf Fahrzeug laden)
|
||||||
|
fin_gi_tmp → ret_gi (Rückgabe an Geldinstitut)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Vehicle Start State Machine
|
||||||
|
```
|
||||||
|
to_delivery → delivery (Bestätigung Ladung)
|
||||||
|
ret_gi → ret_gi_fzg (Übernahme vom Geldinstitut)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Vehicle State Machine
|
||||||
|
```
|
||||||
|
delivery → station (An Haltestelle ausgeben)
|
||||||
|
ret_fail → ret_fail_fzg (Fehlerhafte übernehmen)
|
||||||
|
ret_gi → ret_gi_fzg (Geldinstitut-Objekte übernehmen)
|
||||||
|
ret_ds → ret_ds_fzg (Dienststellen-Objekte übernehmen)
|
||||||
|
station → delivery (Rücknahme)
|
||||||
|
in_fa → hdl (Handel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. FSA (Fahrscheinautomat) State Machine
|
||||||
|
|
||||||
|
#### Geldkassette (GK)
|
||||||
|
```
|
||||||
|
station → in_fa (Einbau)
|
||||||
|
in_fa → ret_fail (Fehlerhaft ausbauen)
|
||||||
|
unknown → ret_gi (Neue unbekannte Kassette)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HP Patrone
|
||||||
|
```
|
||||||
|
station → in_fa (Einbau)
|
||||||
|
in_fa → station (Ausbau)
|
||||||
|
unknown → ret_ds (Neue unbekannte Patrone)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fahrkartenrolle
|
||||||
|
```
|
||||||
|
station → in_fa (Einbau)
|
||||||
|
in_fa → station (Ausbau)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Versorgungsstelle (VS) State Machine
|
||||||
|
```
|
||||||
|
Container Type A (cntra):
|
||||||
|
- SB (Safebag) in_vs → ret_gi
|
||||||
|
|
||||||
|
Container Type B (cntrb):
|
||||||
|
- ABS (Abfall) in_vs → ret_ds
|
||||||
|
|
||||||
|
Container Barcode:
|
||||||
|
- cntra → Container A aktivieren
|
||||||
|
- cntrb → Container B aktivieren
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Geldinstitut (GI) State Machine
|
||||||
|
```
|
||||||
|
ret_gi_fzg → fin_gi (Übergabe an Bank)
|
||||||
|
unknown → ret_ds_empty (Leere Kassette)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Vehicle End State Machine
|
||||||
|
```
|
||||||
|
ret_fail_fzg → ret_fail_stk
|
||||||
|
ret_ds_fzg → ret_ds_stk
|
||||||
|
delivery → ret_ds_stk
|
||||||
|
ret_ds_empty → ret_ds_stk
|
||||||
|
ret_gi_fzg → ret_gi_stk
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Stock End State Machine
|
||||||
|
```
|
||||||
|
ret_fail_stk → fin_ds_fail
|
||||||
|
ret_ds_stk → fin_ds
|
||||||
|
ret_ds_err → fin_ds_err
|
||||||
|
ret_gi_stk → fin_gi_tmp
|
||||||
|
```
|
||||||
|
|
||||||
|
## API-Endpunkte
|
||||||
|
|
||||||
|
### Initialisierung
|
||||||
|
```
|
||||||
|
GET /hha?init={"update":<version>, "imei":<imei>}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status-Update
|
||||||
|
```
|
||||||
|
GET /hha?set_obj_state={"obj":[...], "imei":<imei>}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tour abschließen
|
||||||
|
```
|
||||||
|
GET /hha?fin_tour={"id":<tour_id>, "time":<timestamp>, "imei":<imei>}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neues Objekt
|
||||||
|
```
|
||||||
|
GET /hha?new_obj={"type":<type>, "man":true, "code":<code>, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quittung
|
||||||
|
```
|
||||||
|
GET /hha?new_receipt={"data":<barcode>, "time":<timestamp>, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neuer Safebag
|
||||||
|
```
|
||||||
|
GET /hha?new_safebag={"code":<code>, "time":<timestamp>, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Datenbank-Schema
|
||||||
|
|
||||||
|
### Tabellen
|
||||||
|
|
||||||
|
#### tour
|
||||||
|
- id, job_id, tour_id, version, state, type, sort
|
||||||
|
- loc_id, loc_code, loc_code2, rem, menu, modified, del_code
|
||||||
|
|
||||||
|
#### object
|
||||||
|
- id, object_id, type, version, loc_id, code, rem, state, subtype
|
||||||
|
- origin, manual, last_modified
|
||||||
|
|
||||||
|
#### location
|
||||||
|
- id, location_id, version, name, street, num, zip, city, lat, long, rem
|
||||||
|
|
||||||
|
#### page
|
||||||
|
- id, tour_id, page_number, page_id, type, code, label
|
||||||
|
|
||||||
|
#### page_swap_count / page_pickup_count
|
||||||
|
- id, tour_id, page_id, object_type, object_count
|
||||||
|
|
||||||
|
#### sendqueue
|
||||||
|
- id, url (Base64-kodiert)
|
||||||
|
|
||||||
|
#### receipt
|
||||||
|
- id, code
|
||||||
|
|
||||||
|
#### container_objects
|
||||||
|
- id, container_id, type, object_id
|
||||||
|
|
||||||
|
## Besonderheiten
|
||||||
|
|
||||||
|
### Counter-Labels
|
||||||
|
Die App zeigt Zähler für verschiedene Objekttypen an:
|
||||||
|
- **MEK**: Münzgeldkassette
|
||||||
|
- **BEK**: Bargeldentnahmekassette
|
||||||
|
- **H1, H2, H3**: HP Patronen
|
||||||
|
- **P**: Fahrkartenrolle
|
||||||
|
- **SB**: Safebag
|
||||||
|
- **ABS**: Abfallbehälter
|
||||||
|
|
||||||
|
### Beladezähler
|
||||||
|
- SST: Sonder-Sonder-Tour
|
||||||
|
- CR: Collection-Route
|
||||||
|
- HADAG: Fährverbindung
|
||||||
|
|
||||||
|
### Quittungen
|
||||||
|
Für Geldkassetten müssen Quittungen gescannt werden:
|
||||||
|
- 2D-Barcode enthält: FA-#, GK-#, Betrag, Datum, Zeit
|
||||||
|
- Keine Quittung = Warnhinweis
|
||||||
|
|
||||||
|
### Manueller Modus
|
||||||
|
- Neue Objekte können manuell angelegt werden
|
||||||
|
- Server-Validierung des Barcodes
|
||||||
|
- Automatische Typ-Erkennung über Präfix
|
||||||
|
|
||||||
|
### Container-Logik
|
||||||
|
- Container werden über Barcode aktiviert
|
||||||
|
- Mehrere Objekte können einem Container zugeordnet werden
|
||||||
|
- Container-Abschluss überträgt Status auf alle enthaltenen Objekte
|
||||||
|
|
||||||
|
## Offline-Verhalten
|
||||||
|
|
||||||
|
1. **Daten werden lokal gespeichert** (SQLite)
|
||||||
|
2. **API-Aufrufe werden in Queue eingereiht**
|
||||||
|
3. **Automatische Synchronisation** bei Verbindung
|
||||||
|
4. **Konfliktauflösung** über Versionsnummern
|
||||||
|
|
||||||
|
## Synchronisation
|
||||||
|
|
||||||
|
### Trigger
|
||||||
|
- App-Start
|
||||||
|
- Manueller Refresh
|
||||||
|
- Intervall (60 Sekunden)
|
||||||
|
- Tour-Abschluss
|
||||||
|
|
||||||
|
### Daten
|
||||||
|
- Metadaten (Objekttypen)
|
||||||
|
- Standorte
|
||||||
|
- Objekte
|
||||||
|
- Jobs & Touren
|
||||||
|
- Page-Counts
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
|
||||||
|
- Geräte-Authentifizierung über IMEI
|
||||||
|
- Keine Passwörter im Code
|
||||||
|
- HTTPS für API-Kommunikation
|
||||||
|
- Lokale Daten verschlüsselt (SQLite)
|
||||||
|
|
||||||
|
## Flutter-Implementierung
|
||||||
|
|
||||||
|
### Architektur-Mapping
|
||||||
|
|
||||||
|
| Lua | Flutter |
|
||||||
|
|-----|---------|
|
||||||
|
| ASUIStackLayout | Column/Row |
|
||||||
|
| ASUILabel | Text |
|
||||||
|
| ASUIButton | ElevatedButton |
|
||||||
|
| ASUIScrollView | SingleChildScrollView |
|
||||||
|
| database:Query | Repository Pattern |
|
||||||
|
| BarcodeScanned | MobileScanner |
|
||||||
|
| State Machine | BLoC Pattern |
|
||||||
|
|
||||||
|
### UI-Vergleich
|
||||||
|
|
||||||
|
| Lua-UI | Flutter-UI |
|
||||||
|
|--------|-----------|
|
||||||
|
| Hintergrund #220220220 | Colors.grey.shade200 |
|
||||||
|
| HHA Rot | Color(0xFFE3001B) |
|
||||||
|
| Grüner Header #188230165 | Color(0xFFBCA6A5) |
|
||||||
|
| Status-Farben | ObjectStateInfo.getColorForState |
|
||||||
|
|
||||||
|
### State-Management
|
||||||
|
|
||||||
|
Die komplexen Lua-State-Machines wurden in BLoC-Pattern überführt:
|
||||||
|
- `ScanBloc` verwaltet Barcode-Scanning
|
||||||
|
- `TourBloc` verwaltet Touren und Synchronisation
|
||||||
|
- Zustandsübergänge sind explizit und testbar
|
||||||
|
|
||||||
|
### Vorteile der Flutter-Implementierung
|
||||||
|
|
||||||
|
1. **Type Safety**: Dart's Typisierung verhindert Laufzeitfehler
|
||||||
|
2. **Hot Reload**: Schnellere Entwicklung
|
||||||
|
3. **Moderne UI**: Material Design 3, Animationen
|
||||||
|
4. **Wartbarkeit**: Klare Architektur, Trennung von Concerns
|
||||||
|
5. **Testbarkeit**: Unit-Tests für Business-Logik
|
||||||
|
6. **Offline-First**: SQFlite mit Repository-Pattern
|
||||||
|
7. **State Management**: BLoC für vorhersagbare Zustände
|
||||||
|
|
||||||
|
## Fazit
|
||||||
|
|
||||||
|
Die Lua-App implementiert einen ausgereiften, komplexen Workflow für die Logistik der Hamburger Hochbahn. Die Flutter-Neuimplementierung behält die komplette fachliche Logik bei, verbessert jedoch:
|
||||||
|
- Architektur und Wartbarkeit
|
||||||
|
- UI/UX mit modernem Design
|
||||||
|
- Offline-Fähigkeiten
|
||||||
|
- Testbarkeit und Qualität
|
||||||
|
- Zukunftssicherheit und Erweiterbarkeit
|
||||||
196
app/README.md
Normal file
196
app/README.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# HHA Logistics - Flutter App
|
||||||
|
|
||||||
|
Diese Flutter-Anwendung liegt im Monorepo unter `app/`.
|
||||||
|
|
||||||
|
Das zugehoerige Spring Boot + Vaadin Backend liegt daneben unter `../backend/`.
|
||||||
|
|
||||||
|
Eine moderne, optisch ansprechende Flutter-App für die Hamburger Hochbahn (HHA) zur Verwaltung von Logistik-Prozessen.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Kernfunktionen
|
||||||
|
- **Tour-Management**: Übersichtliche Darstellung aller Touren und Stationen
|
||||||
|
- **Barcode-Scanning**: Schnelles Erfassen von Objekten per Kamera
|
||||||
|
- **State-Management**: Automatische Zustandsübergänge für Objekte
|
||||||
|
- **Offline-Unterstützung**: Lokale Datenspeicherung mit Synchronisation
|
||||||
|
- **Echtzeit-Sync**: Automatische Aktualisierung der Daten
|
||||||
|
|
||||||
|
### Objekttypen
|
||||||
|
- **Geldkassetten** (MEK, BEK)
|
||||||
|
- **HP Patronen** (H1, H2, H3)
|
||||||
|
- **Fahrkartenrollen**
|
||||||
|
- **Safebags** (Wertsachenbehälter)
|
||||||
|
- **Container**
|
||||||
|
|
||||||
|
### Stationstypen
|
||||||
|
- Lager (Beladung/Rückgabe)
|
||||||
|
- Dienststellen
|
||||||
|
- Haltestellen
|
||||||
|
- Fahrscheinautomaten
|
||||||
|
- Versorgungsstellen
|
||||||
|
- Geldinstitute
|
||||||
|
|
||||||
|
## Technischer Stack
|
||||||
|
|
||||||
|
### Architektur
|
||||||
|
- **Clean Architecture**: Trennung von Domain, Data und Presentation Layer
|
||||||
|
- **BLoC Pattern**: State Management mit flutter_bloc
|
||||||
|
- **Repository Pattern**: Abstraktion der Datenquellen
|
||||||
|
|
||||||
|
### UI/UX
|
||||||
|
- **Material Design 3**: Modernes, konsistentes Design
|
||||||
|
- **Flutter Animate**: Flüssige Animationen
|
||||||
|
- **Phosphor Icons**: Hochwertige Icons
|
||||||
|
- **Google Fonts**: Inter als Hausschrift
|
||||||
|
|
||||||
|
### Technologien
|
||||||
|
- **Mobile Scanner**: Barcode-Scanning
|
||||||
|
- **SQFlite**: Lokale Datenbank
|
||||||
|
- **Dio**: HTTP-Client für API-Kommunikation
|
||||||
|
- **Shared Preferences**: Lokale Einstellungen
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── lib/
|
||||||
|
│ ├── core/
|
||||||
|
│ ├── data/
|
||||||
|
│ ├── domain/
|
||||||
|
│ └── presentation/
|
||||||
|
├── android/
|
||||||
|
├── build/
|
||||||
|
├── pubspec.yaml
|
||||||
|
└── run_app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
- Flutter SDK >= 3.0.0
|
||||||
|
- Dart SDK >= 3.0.0
|
||||||
|
- Android Studio / Xcode
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Repository klonen:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd hha_logistics
|
||||||
|
cd app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Dependencies installieren:
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Code-Generierung ausführen:
|
||||||
|
```bash
|
||||||
|
flutter pub run build_runner build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. App starten:
|
||||||
|
```bash
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### API-Endpunkt
|
||||||
|
Die API-URL wird in `lib/core/constants/app_constants.dart` konfiguriert:
|
||||||
|
```dart
|
||||||
|
static const String baseUrl = 'https://hha-app1.assecutor.de/hha';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Berechtigungen
|
||||||
|
|
||||||
|
#### Android (android/app/src/main/AndroidManifest.xml)
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### iOS (ios/Runner/Info.plist)
|
||||||
|
```xml
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Kamera wird für Barcode-Scanning benötigt</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fachlicher Workflow
|
||||||
|
|
||||||
|
### 1. Login & Datensynchronisation
|
||||||
|
- Anmeldung mit Geräte-IMEI
|
||||||
|
- Automatischer Datenabgleich mit Server
|
||||||
|
- Lokale Speicherung für Offline-Nutzung
|
||||||
|
|
||||||
|
### 2. Tour-Übersicht
|
||||||
|
- Liste aller Stationen der aktuellen Tour
|
||||||
|
- Fortschrittsanzeige
|
||||||
|
- Unterscheidung zwischen offenen und erledigten Stationen
|
||||||
|
|
||||||
|
### 3. Station-Arbeit
|
||||||
|
- Barcode-Scanning zur Identifikation
|
||||||
|
- Automatische State-Machine für Zustandsübergänge
|
||||||
|
- Manuelle Barcode-Eingabe möglich
|
||||||
|
|
||||||
|
### 4. State-Machine
|
||||||
|
Die App implementiert folgende Zustandsübergänge:
|
||||||
|
- `unknown` → `to_delivery` → `delivery` → `station` → `in_fa` → `ret_gi` → `fin_gi`
|
||||||
|
- Fehlerfälle: `ret_fail` → `ret_fail_fzg` → `ret_fail_stk`
|
||||||
|
- Dienststelle: `ret_ds` → `ret_ds_fzg` → `fin_ds`
|
||||||
|
|
||||||
|
### 5. Bestandsführung
|
||||||
|
- Echtzeit-Anzeige aller Objekte
|
||||||
|
- Filterung nach Status und Typ
|
||||||
|
- Quittungsverwaltung für Geldkassetten
|
||||||
|
|
||||||
|
## Design-System
|
||||||
|
|
||||||
|
### Farben
|
||||||
|
- **Primary**: HHA Rot (#E3001B)
|
||||||
|
- **Success**: Grün (#4CAF50)
|
||||||
|
- **Warning**: Orange (#FF9800)
|
||||||
|
- **Error**: Rot (#E3001B)
|
||||||
|
|
||||||
|
### State Colors
|
||||||
|
- `delivery`: Grau (#B3B3B3)
|
||||||
|
- `station`: Gelb (#FFDD00)
|
||||||
|
- `in_fa`: Grün (#9CDA7A)
|
||||||
|
- `ret_fail`: Rot (#FF9081)
|
||||||
|
- `ret_ds`: Hellblau (#AFE0ED)
|
||||||
|
- `fin_ds`: Blau (#29B7FB)
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
### Code-Generierung
|
||||||
|
Nach Änderungen an Modellen:
|
||||||
|
```bash
|
||||||
|
flutter pub run build_runner build --delete-conflicting-outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests ausführen
|
||||||
|
```bash
|
||||||
|
flutter test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release Build
|
||||||
|
```bash
|
||||||
|
# Android
|
||||||
|
flutter build apk --release
|
||||||
|
flutter build appbundle --release
|
||||||
|
|
||||||
|
# iOS
|
||||||
|
flutter build ios --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
Copyright © 2024 Hamburger Hochbahn AG
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Bei Fragen oder Problemen wenden Sie sich an:
|
||||||
|
- IT-Support: support@hha.de
|
||||||
|
- Entwicklung: dev@hha.de
|
||||||
22
app/analysis_options.yaml
Normal file
22
app/analysis_options.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
avoid_print: true
|
||||||
|
prefer_single_quotes: true
|
||||||
|
prefer_const_constructors: true
|
||||||
|
prefer_const_literals_to_create_immutables: true
|
||||||
|
prefer_final_fields: true
|
||||||
|
prefer_final_locals: true
|
||||||
|
avoid_unnecessary_containers: true
|
||||||
|
avoid_redundant_argument_values: true
|
||||||
|
use_super_parameters: true
|
||||||
|
use_build_context_synchronously: true
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- "**/*.g.dart"
|
||||||
|
- "**/*.freezed.dart"
|
||||||
|
- "**/generated_plugin_registrant.dart"
|
||||||
|
errors:
|
||||||
|
invalid_annotation_target: ignore
|
||||||
14
app/android/.gitignore
vendored
Normal file
14
app/android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
44
app/android/app/build.gradle.kts
Normal file
44
app/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.hha_logistics"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.example.hha_logistics"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
7
app/android/app/src/debug/AndroidManifest.xml
Normal file
7
app/android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
45
app/android/app/src/main/AndroidManifest.xml
Normal file
45
app/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="hha_logistics"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.example.hha_logistics
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
12
app/android/app/src/main/res/drawable/launch_background.xml
Normal file
12
app/android/app/src/main/res/drawable/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
BIN
app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 B |
BIN
app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 721 B |
BIN
app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
18
app/android/app/src/main/res/values-night/styles.xml
Normal file
18
app/android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
app/android/app/src/main/res/values/styles.xml
Normal file
18
app/android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
app/android/app/src/profile/AndroidManifest.xml
Normal file
7
app/android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
24
app/android/build.gradle.kts
Normal file
24
app/android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
2
app/android/gradle.properties
Normal file
2
app/android/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
5
app/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
app/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
26
app/android/settings.gradle.kts
Normal file
26
app/android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.11.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
3
app/devtools_options.yaml
Normal file
3
app/devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
132
app/lib/core/constants/app_constants.dart
Normal file
132
app/lib/core/constants/app_constants.dart
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
class AppConstants {
|
||||||
|
// API
|
||||||
|
static const String baseUrl = 'https://hha-app1.assecutor.de/hha';
|
||||||
|
static const int connectionTimeout = 30000;
|
||||||
|
static const int receiveTimeout = 30000;
|
||||||
|
|
||||||
|
// Database
|
||||||
|
static const String databaseName = 'hha_logistics.db';
|
||||||
|
static const int databaseVersion = 1;
|
||||||
|
|
||||||
|
// App Info
|
||||||
|
static const String appName = 'HHA Logistics';
|
||||||
|
static const String appVersion = '2.0.0';
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
static const Duration syncInterval = Duration(minutes: 1);
|
||||||
|
static const Duration locationUpdateInterval = Duration(seconds: 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectStates {
|
||||||
|
static const String unknown = 'unknown';
|
||||||
|
static const String toDelivery = 'to_delivery';
|
||||||
|
static const String delivery = 'delivery';
|
||||||
|
static const String station = 'station';
|
||||||
|
static const String inFA = 'in_fa';
|
||||||
|
static const String inVS = 'in_vs';
|
||||||
|
static const String retFail = 'ret_fail';
|
||||||
|
static const String retFailFzg = 'ret_fail_fzg';
|
||||||
|
static const String retFailStk = 'ret_fail_stk';
|
||||||
|
static const String retGI = 'ret_gi';
|
||||||
|
static const String retGIFzg = 'ret_gi_fzg';
|
||||||
|
static const String retGIStk = 'ret_gi_stk';
|
||||||
|
static const String retDS = 'ret_ds';
|
||||||
|
static const String retDSFzg = 'ret_ds_fzg';
|
||||||
|
static const String retDSStk = 'ret_ds_stk';
|
||||||
|
static const String retDSErr = 'ret_ds_err';
|
||||||
|
static const String retDSEmpty = 'ret_ds_empty';
|
||||||
|
static const String finDS = 'fin_ds';
|
||||||
|
static const String finGI = 'fin_gi';
|
||||||
|
static const String finDSFail = 'fin_ds_fail';
|
||||||
|
static const String finDSErr = 'fin_ds_err';
|
||||||
|
static const String hdl = 'hdl';
|
||||||
|
static const String stkHadag = 'stk_hadag';
|
||||||
|
static const String finGITmp = 'fin_gi_tmp';
|
||||||
|
static const String retcGI = 'retc_gi';
|
||||||
|
static const String retcDS = 'retc_ds';
|
||||||
|
static const String retDSFix = 'ret_ds_fix';
|
||||||
|
static const String retFixStk = 'ret_fix_stk';
|
||||||
|
static const String finFix = 'fin_fix';
|
||||||
|
static const String trig = 'trig';
|
||||||
|
}
|
||||||
|
|
||||||
|
class TourTypes {
|
||||||
|
static const String stockStart = 'stock_start';
|
||||||
|
static const String stock = 'stock';
|
||||||
|
static const String start = 'start';
|
||||||
|
static const String station = 'st';
|
||||||
|
static const String hls = 'hls';
|
||||||
|
static const String vs = 'vs';
|
||||||
|
static const String stockEnd = 'stock_end';
|
||||||
|
static const String end = 'end';
|
||||||
|
static const String fsa = 'fsa';
|
||||||
|
static const String gi = 'gi';
|
||||||
|
static const String veh = 'veh';
|
||||||
|
static const String vehStart = 'veh_start';
|
||||||
|
static const String vehBulk = 'veh_bulk';
|
||||||
|
static const String vehVs = 'veh_vs';
|
||||||
|
static const String vehEnd = 'veh_end';
|
||||||
|
static const String menu = 'me';
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectTypes {
|
||||||
|
static const String gk = 'gk'; // Geldkassette
|
||||||
|
static const String hp = 'hp'; // HP Patronen
|
||||||
|
static const String fr = 'fr'; // Fahrkartenrolle
|
||||||
|
static const String sb = 'sb'; // Safebag
|
||||||
|
static const String abs = 'abs'; // Abfallbehälter
|
||||||
|
static const String cntr = 'cntr'; // Container
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectSubtypes {
|
||||||
|
// Geldkassetten
|
||||||
|
static const String meka = 'meka';
|
||||||
|
static const String mekb = 'mekb';
|
||||||
|
static const String mekc = 'mekc';
|
||||||
|
static const String mekd = 'mekd';
|
||||||
|
static const String beka = 'beka';
|
||||||
|
static const String bekb = 'bekb';
|
||||||
|
static const String bekc = 'bekc';
|
||||||
|
static const String bekd = 'bekd';
|
||||||
|
|
||||||
|
// HP Patronen
|
||||||
|
static const String hp1a = 'hp1a';
|
||||||
|
static const String hp1b = 'hp1b';
|
||||||
|
static const String hp1c = 'hp1c';
|
||||||
|
static const String hp2a = 'hp2a';
|
||||||
|
static const String hp2b = 'hp2b';
|
||||||
|
static const String hp2c = 'hp2c';
|
||||||
|
static const String hp3a = 'hp3a';
|
||||||
|
static const String hp3b = 'hp3b';
|
||||||
|
static const String hp3c = 'hp3c';
|
||||||
|
|
||||||
|
// Fahrkartenrollen
|
||||||
|
static const String fra = 'fra';
|
||||||
|
|
||||||
|
// Container
|
||||||
|
static const String cntra = 'cntra';
|
||||||
|
static const String cntrb = 'cntrb';
|
||||||
|
}
|
||||||
|
|
||||||
|
class CounterLabels {
|
||||||
|
static const Map<String, String> labels = {
|
||||||
|
'meka': 'MEK',
|
||||||
|
'beka': 'BEK',
|
||||||
|
'hp1a': 'H1',
|
||||||
|
'hp2a': 'H2',
|
||||||
|
'hp3a': 'H3',
|
||||||
|
'fra': 'P',
|
||||||
|
'sb': 'SB',
|
||||||
|
'abs': 'ABS',
|
||||||
|
'mekb': 'MEK-B',
|
||||||
|
'bekb': 'BEK-B',
|
||||||
|
'hp1b': 'H1-B',
|
||||||
|
'hp2b': 'H2-B',
|
||||||
|
'mekc': 'MEK-SST',
|
||||||
|
'bekc': 'BEK-SST',
|
||||||
|
'hp1c': 'H1-SST',
|
||||||
|
'hp2c': 'H2-SST',
|
||||||
|
'mekd': 'MEK-CR',
|
||||||
|
'bekd': 'BEK-CR',
|
||||||
|
};
|
||||||
|
}
|
||||||
70
app/lib/core/errors/failures.dart
Normal file
70
app/lib/core/errors/failures.dart
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
abstract class Failure extends Equatable {
|
||||||
|
final String message;
|
||||||
|
final String? code;
|
||||||
|
|
||||||
|
const Failure({
|
||||||
|
required this.message,
|
||||||
|
this.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message, code];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerFailure extends Failure {
|
||||||
|
const ServerFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class CacheFailure extends Failure {
|
||||||
|
const CacheFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkFailure extends Failure {
|
||||||
|
const NetworkFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValidationFailure extends Failure {
|
||||||
|
const ValidationFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotFoundFailure extends Failure {
|
||||||
|
const NotFoundFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnauthorizedFailure extends Failure {
|
||||||
|
const UnauthorizedFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class BarcodeFailure extends Failure {
|
||||||
|
const BarcodeFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncFailure extends Failure {
|
||||||
|
const SyncFailure({
|
||||||
|
required super.message,
|
||||||
|
super.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
273
app/lib/core/theme/app_theme.dart
Normal file
273
app/lib/core/theme/app_theme.dart
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class AppTheme {
|
||||||
|
// Brand Colors - HHA Corporate Colors
|
||||||
|
static const Color hhaRed = Color(0xFFE3001B);
|
||||||
|
static const Color hhaDarkRed = Color(0xFFB30015);
|
||||||
|
static const Color hhaLightRed = Color(0xFFFF4D5E);
|
||||||
|
|
||||||
|
// State Colors
|
||||||
|
static const Color stateDelivery = Color(0xFFB3B3B3);
|
||||||
|
static const Color stateStation = Color(0xFFFFDD00);
|
||||||
|
static const Color stateInFA = Color(0xFF9CDA7A);
|
||||||
|
static const Color stateRetFail = Color(0xFFFF9081);
|
||||||
|
static const Color stateRetDS = Color(0xFFAFE0ED);
|
||||||
|
static const Color stateRetGI = Color(0xFFAFE0ED);
|
||||||
|
static const Color stateFinDS = Color(0xFF29B7FB);
|
||||||
|
static const Color stateFinGI = Color(0xFF25BAFC);
|
||||||
|
static const Color stateInVS = Color(0xFFFAE14B);
|
||||||
|
|
||||||
|
// Functional Colors
|
||||||
|
static const Color success = Color(0xFF4CAF50);
|
||||||
|
static const Color warning = Color(0xFFFF9800);
|
||||||
|
static const Color error = Color(0xFFE3001B);
|
||||||
|
static const Color info = Color(0xFF2196F3);
|
||||||
|
|
||||||
|
// Neutral Colors
|
||||||
|
static const Color white = Color(0xFFFFFFFF);
|
||||||
|
static const Color background = Color(0xFFF5F5F5);
|
||||||
|
static const Color surface = Color(0xFFFFFFFF);
|
||||||
|
static const Color cardBackground = Color(0xFFFAFAFA);
|
||||||
|
static const Color divider = Color(0xFFE0E0E0);
|
||||||
|
|
||||||
|
// Text Colors
|
||||||
|
static const Color textPrimary = Color(0xFF212121);
|
||||||
|
static const Color textSecondary = Color(0xFF757575);
|
||||||
|
static const Color textTertiary = Color(0xFF9E9E9E);
|
||||||
|
static const Color textOnDark = Color(0xFFFFFFFF);
|
||||||
|
|
||||||
|
static ThemeData get lightTheme {
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: const ColorScheme.light(
|
||||||
|
primary: hhaRed,
|
||||||
|
primaryContainer: hhaLightRed,
|
||||||
|
onPrimaryContainer: white,
|
||||||
|
secondary: Color(0xFF2196F3),
|
||||||
|
onSurface: textPrimary,
|
||||||
|
surfaceContainerHighest: background,
|
||||||
|
error: error,
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: background,
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: hhaRed,
|
||||||
|
foregroundColor: white,
|
||||||
|
titleTextStyle: GoogleFonts.inter(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: white,
|
||||||
|
),
|
||||||
|
systemOverlayStyle: const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
statusBarIconBrightness: Brightness.light,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: Colors.black.withValues(alpha: 26),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
color: surface,
|
||||||
|
),
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: hhaRed,
|
||||||
|
foregroundColor: white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
textStyle: GoogleFonts.inter(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: hhaRed,
|
||||||
|
side: const BorderSide(color: hhaRed, width: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
textStyle: GoogleFonts.inter(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: hhaRed,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
textStyle: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||||
|
backgroundColor: hhaRed,
|
||||||
|
foregroundColor: white,
|
||||||
|
elevation: 4,
|
||||||
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: surface,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: divider),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: divider),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: hhaRed, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: error, width: 2),
|
||||||
|
),
|
||||||
|
labelStyle: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
color: textSecondary,
|
||||||
|
),
|
||||||
|
hintStyle: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
color: textTertiary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
chipTheme: ChipThemeData(
|
||||||
|
backgroundColor: background,
|
||||||
|
selectedColor: hhaRed.withValues(alpha: 26),
|
||||||
|
labelStyle: GoogleFonts.inter(fontSize: 12),
|
||||||
|
secondaryLabelStyle: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
color: hhaRed,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
listTileTheme: ListTileThemeData(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dividerTheme: const DividerThemeData(
|
||||||
|
color: divider,
|
||||||
|
thickness: 1,
|
||||||
|
space: 1,
|
||||||
|
),
|
||||||
|
textTheme: _textTheme,
|
||||||
|
fontFamily: GoogleFonts.inter().fontFamily,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TextTheme get _textTheme {
|
||||||
|
return TextTheme(
|
||||||
|
displayLarge: GoogleFonts.inter(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
displayMedium: GoogleFonts.inter(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
displaySmall: GoogleFonts.inter(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
headlineLarge: GoogleFonts.inter(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
headlineMedium: GoogleFonts.inter(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
headlineSmall: GoogleFonts.inter(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
titleLarge: GoogleFonts.inter(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
titleMedium: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
titleSmall: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: textSecondary,
|
||||||
|
),
|
||||||
|
bodyLarge: GoogleFonts.inter(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
bodyMedium: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
bodySmall: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
color: textSecondary,
|
||||||
|
),
|
||||||
|
labelLarge: GoogleFonts.inter(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
labelMedium: GoogleFonts.inter(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: textSecondary,
|
||||||
|
),
|
||||||
|
labelSmall: GoogleFonts.inter(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: textTertiary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ColorExtension on Color {
|
||||||
|
Color darken([double amount = .1]) {
|
||||||
|
assert(amount >= 0 && amount <= 1);
|
||||||
|
final hsl = HSLColor.fromColor(this);
|
||||||
|
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||||
|
return hslDark.toColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color lighten([double amount = .1]) {
|
||||||
|
assert(amount >= 0 && amount <= 1);
|
||||||
|
final hsl = HSLColor.fromColor(this);
|
||||||
|
final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
|
||||||
|
return hslLight.toColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
163
app/lib/data/models/logistic_object_model.dart
Normal file
163
app/lib/data/models/logistic_object_model.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import '../../domain/entities/logistic_object.dart';
|
||||||
|
|
||||||
|
class LogisticObjectModel extends LogisticObject {
|
||||||
|
const LogisticObjectModel({
|
||||||
|
required super.id,
|
||||||
|
required super.objectId,
|
||||||
|
required super.type,
|
||||||
|
required super.version,
|
||||||
|
super.locationId,
|
||||||
|
required super.code,
|
||||||
|
super.remark,
|
||||||
|
required super.state,
|
||||||
|
required super.subtype,
|
||||||
|
super.origin,
|
||||||
|
super.isManual,
|
||||||
|
super.lastModified,
|
||||||
|
super.typeName,
|
||||||
|
super.typeMnemonic,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LogisticObjectModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return LogisticObjectModel(
|
||||||
|
id: json['id'] ?? 0,
|
||||||
|
objectId: json['object_id'] ?? json['id'] ?? 0,
|
||||||
|
type: json['type'] ?? 0,
|
||||||
|
version: json['version'] ?? json['ver'] ?? 0,
|
||||||
|
locationId: json['loc_id'],
|
||||||
|
code: json['code'] ?? '',
|
||||||
|
remark: json['rem'],
|
||||||
|
state: json['state'] ?? 'unknown',
|
||||||
|
subtype: json['subtype'] ?? json['type']?.toString() ?? '',
|
||||||
|
origin: json['origin'],
|
||||||
|
isManual: json['manual'] == '1' || json['manual'] == true,
|
||||||
|
lastModified: json['last_modified'] != null
|
||||||
|
? DateTime.parse(json['last_modified'])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory LogisticObjectModel.fromMap(Map<String, dynamic> map) {
|
||||||
|
return LogisticObjectModel(
|
||||||
|
id: map['id'] ?? 0,
|
||||||
|
objectId: map['object_id'] ?? 0,
|
||||||
|
type: map['type'] ?? 0,
|
||||||
|
version: map['version'] ?? 0,
|
||||||
|
locationId: map['loc_id'],
|
||||||
|
code: map['code'] ?? '',
|
||||||
|
remark: map['rem'],
|
||||||
|
state: map['state'] ?? 'unknown',
|
||||||
|
subtype: map['subtype'] ?? '',
|
||||||
|
origin: map['origin'],
|
||||||
|
isManual: map['manual'] == '1' || map['manual'] == 1,
|
||||||
|
lastModified: map['last_modified'] != null
|
||||||
|
? DateTime.tryParse(map['last_modified'])
|
||||||
|
: null,
|
||||||
|
typeName: map['type_name'],
|
||||||
|
typeMnemonic: map['type_mnemonic'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'object_id': objectId,
|
||||||
|
'type': type,
|
||||||
|
'version': version,
|
||||||
|
'loc_id': locationId,
|
||||||
|
'code': code,
|
||||||
|
'rem': remark,
|
||||||
|
'state': state,
|
||||||
|
'subtype': subtype,
|
||||||
|
'origin': origin,
|
||||||
|
'manual': isManual ? 1 : 0,
|
||||||
|
'last_modified': lastModified?.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
LogisticObjectModel copyWithModel({
|
||||||
|
int? id,
|
||||||
|
int? objectId,
|
||||||
|
int? type,
|
||||||
|
int? version,
|
||||||
|
int? locationId,
|
||||||
|
String? code,
|
||||||
|
String? remark,
|
||||||
|
String? state,
|
||||||
|
String? subtype,
|
||||||
|
String? origin,
|
||||||
|
bool? isManual,
|
||||||
|
DateTime? lastModified,
|
||||||
|
String? typeName,
|
||||||
|
String? typeMnemonic,
|
||||||
|
}) {
|
||||||
|
return LogisticObjectModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
objectId: objectId ?? this.objectId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
version: version ?? this.version,
|
||||||
|
locationId: locationId ?? this.locationId,
|
||||||
|
code: code ?? this.code,
|
||||||
|
remark: remark ?? this.remark,
|
||||||
|
state: state ?? this.state,
|
||||||
|
subtype: subtype ?? this.subtype,
|
||||||
|
origin: origin ?? this.origin,
|
||||||
|
isManual: isManual ?? this.isManual,
|
||||||
|
lastModified: lastModified ?? this.lastModified,
|
||||||
|
typeName: typeName ?? this.typeName,
|
||||||
|
typeMnemonic: typeMnemonic ?? this.typeMnemonic,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectMetadataModel extends ObjectMetadata {
|
||||||
|
const ObjectMetadataModel({
|
||||||
|
required super.id,
|
||||||
|
required super.type,
|
||||||
|
required super.version,
|
||||||
|
required super.mnemonic,
|
||||||
|
required super.name,
|
||||||
|
required super.prefix,
|
||||||
|
required super.subtype,
|
||||||
|
required super.counterText,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ObjectMetadataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ObjectMetadataModel(
|
||||||
|
id: json['id'] ?? 0,
|
||||||
|
type: json['type'] ?? 0,
|
||||||
|
version: json['version'] ?? json['ver'] ?? 0,
|
||||||
|
mnemonic: json['mnemonic'] ?? '',
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
prefix: json['pre'] ?? '',
|
||||||
|
subtype: json['subtype'] ?? '',
|
||||||
|
counterText: json['counter_text'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ObjectMetadataModel.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ObjectMetadataModel(
|
||||||
|
id: map['id'] ?? 0,
|
||||||
|
type: map['type'] ?? 0,
|
||||||
|
version: map['version'] ?? 0,
|
||||||
|
mnemonic: map['mnemonic'] ?? '',
|
||||||
|
name: map['name'] ?? '',
|
||||||
|
prefix: map['prefix'] ?? '',
|
||||||
|
subtype: map['subtype'] ?? '',
|
||||||
|
counterText: map['counter_text'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'type': type,
|
||||||
|
'version': version,
|
||||||
|
'mnemonic': mnemonic,
|
||||||
|
'name': name,
|
||||||
|
'prefix': prefix,
|
||||||
|
'subtype': subtype,
|
||||||
|
'counter_text': counterText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
194
app/lib/data/models/tour_model.dart
Normal file
194
app/lib/data/models/tour_model.dart
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import '../../domain/entities/tour.dart';
|
||||||
|
|
||||||
|
class TourModel extends Tour {
|
||||||
|
const TourModel({
|
||||||
|
required super.id,
|
||||||
|
required super.jobId,
|
||||||
|
required super.tourId,
|
||||||
|
required super.version,
|
||||||
|
required super.state,
|
||||||
|
required super.type,
|
||||||
|
required super.sort,
|
||||||
|
required super.locationId,
|
||||||
|
required super.locationCode,
|
||||||
|
super.locationCode2,
|
||||||
|
super.remark,
|
||||||
|
super.menuText,
|
||||||
|
required super.modified,
|
||||||
|
super.deliveryCode,
|
||||||
|
super.locationName,
|
||||||
|
super.isCompleted,
|
||||||
|
super.pages,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TourModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return TourModel(
|
||||||
|
id: json['id'] ?? 0,
|
||||||
|
jobId: json['job_id'] ?? 0,
|
||||||
|
tourId: json['tour_id'] ?? 0,
|
||||||
|
version: json['version'] ?? 0,
|
||||||
|
state: json['state'] ?? 0,
|
||||||
|
type: json['type'] ?? '',
|
||||||
|
sort: json['sort'] ?? 0,
|
||||||
|
locationId: json['loc_id'] ?? 0,
|
||||||
|
locationCode: json['loc_code'] ?? '',
|
||||||
|
locationCode2: json['loc_code_2'],
|
||||||
|
remark: json['rem'],
|
||||||
|
menuText: json['menu'],
|
||||||
|
modified: json['modified'] ?? 0,
|
||||||
|
deliveryCode: json['del_code'],
|
||||||
|
locationName: json['location_name'],
|
||||||
|
isCompleted: json['state'] == 1,
|
||||||
|
pages: (json['pages'] as List?)
|
||||||
|
?.map((p) => TourPageModel.fromJson(p))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory TourModel.fromMap(Map<String, dynamic> map) {
|
||||||
|
return TourModel(
|
||||||
|
id: map['id'] ?? 0,
|
||||||
|
jobId: map['job_id'] ?? 0,
|
||||||
|
tourId: map['tour_id'] ?? 0,
|
||||||
|
version: map['version'] ?? 0,
|
||||||
|
state: map['state'] ?? 0,
|
||||||
|
type: map['type'] ?? '',
|
||||||
|
sort: map['sort'] ?? 0,
|
||||||
|
locationId: map['loc_id'] ?? 0,
|
||||||
|
locationCode: map['loc_code'] ?? '',
|
||||||
|
locationCode2: map['loc_code2'],
|
||||||
|
remark: map['rem'],
|
||||||
|
menuText: map['menu'],
|
||||||
|
modified: map['modified'] ?? 0,
|
||||||
|
deliveryCode: map['del_code'],
|
||||||
|
locationName: map['location_name'],
|
||||||
|
isCompleted: map['state'] == 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'job_id': jobId,
|
||||||
|
'tour_id': tourId,
|
||||||
|
'version': version,
|
||||||
|
'state': state,
|
||||||
|
'type': type,
|
||||||
|
'sort': sort,
|
||||||
|
'loc_id': locationId,
|
||||||
|
'loc_code': locationCode,
|
||||||
|
'loc_code2': locationCode2,
|
||||||
|
'rem': remark,
|
||||||
|
'menu': menuText,
|
||||||
|
'modified': modified,
|
||||||
|
'del_code': deliveryCode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TourModel copyWithModel({
|
||||||
|
int? id,
|
||||||
|
int? jobId,
|
||||||
|
int? tourId,
|
||||||
|
int? version,
|
||||||
|
int? state,
|
||||||
|
String? type,
|
||||||
|
int? sort,
|
||||||
|
int? locationId,
|
||||||
|
String? locationCode,
|
||||||
|
String? locationCode2,
|
||||||
|
String? remark,
|
||||||
|
String? menuText,
|
||||||
|
int? modified,
|
||||||
|
String? deliveryCode,
|
||||||
|
String? locationName,
|
||||||
|
bool? isCompleted,
|
||||||
|
List<TourPage>? pages,
|
||||||
|
}) {
|
||||||
|
return TourModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
jobId: jobId ?? this.jobId,
|
||||||
|
tourId: tourId ?? this.tourId,
|
||||||
|
version: version ?? this.version,
|
||||||
|
state: state ?? this.state,
|
||||||
|
type: type ?? this.type,
|
||||||
|
sort: sort ?? this.sort,
|
||||||
|
locationId: locationId ?? this.locationId,
|
||||||
|
locationCode: locationCode ?? this.locationCode,
|
||||||
|
locationCode2: locationCode2 ?? this.locationCode2,
|
||||||
|
remark: remark ?? this.remark,
|
||||||
|
menuText: menuText ?? this.menuText,
|
||||||
|
modified: modified ?? this.modified,
|
||||||
|
deliveryCode: deliveryCode ?? this.deliveryCode,
|
||||||
|
locationName: locationName ?? this.locationName,
|
||||||
|
isCompleted: isCompleted ?? this.isCompleted,
|
||||||
|
pages: pages ?? this.pages,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TourPageModel extends TourPage {
|
||||||
|
const TourPageModel({
|
||||||
|
required super.id,
|
||||||
|
required super.tourId,
|
||||||
|
required super.pageNumber,
|
||||||
|
required super.pageId,
|
||||||
|
required super.type,
|
||||||
|
super.code,
|
||||||
|
super.label,
|
||||||
|
super.pickupCounts,
|
||||||
|
super.swapCounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TourPageModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
Map<String, int> pickupCounts = {};
|
||||||
|
Map<String, int> swapCounts = {};
|
||||||
|
|
||||||
|
if (json['pickup'] != null && json['pickup']['cnt'] != null) {
|
||||||
|
final cnt = json['pickup']['cnt'] as Map<String, dynamic>;
|
||||||
|
pickupCounts = cnt.map((key, value) => MapEntry(key, value as int));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json['swap'] != null && json['swap']['cnt'] != null) {
|
||||||
|
final cnt = json['swap']['cnt'] as Map<String, dynamic>;
|
||||||
|
swapCounts = cnt.map((key, value) => MapEntry(key, value as int));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TourPageModel(
|
||||||
|
id: json['id'] ?? 0,
|
||||||
|
tourId: json['tour_id'] ?? 0,
|
||||||
|
pageNumber: json['page_number'] ?? 0,
|
||||||
|
pageId: json['page_id'] ?? '',
|
||||||
|
type: json['type'] ?? '',
|
||||||
|
code: json['code'],
|
||||||
|
label: json['lbl'],
|
||||||
|
pickupCounts: pickupCounts,
|
||||||
|
swapCounts: swapCounts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory TourPageModel.fromMap(Map<String, dynamic> map) {
|
||||||
|
return TourPageModel(
|
||||||
|
id: map['id'] ?? 0,
|
||||||
|
tourId: map['tour_id'] ?? 0,
|
||||||
|
pageNumber: map['page_number'] ?? 0,
|
||||||
|
pageId: map['page_id'] ?? '',
|
||||||
|
type: map['type'] ?? '',
|
||||||
|
code: map['code'],
|
||||||
|
label: map['label'],
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'tour_id': tourId,
|
||||||
|
'page_number': pageNumber,
|
||||||
|
'page_id': pageId,
|
||||||
|
'type': type,
|
||||||
|
'code': code,
|
||||||
|
'label': label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
199
app/lib/domain/entities/counter.dart
Normal file
199
app/lib/domain/entities/counter.dart
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Repräsentiert einen Zählerstand für einen Objekttyp
|
||||||
|
/// Entspricht der Lua-Logik in CreateLoadingStockStartView etc.
|
||||||
|
class ObjectCounter extends Equatable {
|
||||||
|
final String objectType; // z.B. 'meka', 'beka', 'hp1a', etc.
|
||||||
|
final String label; // z.B. 'MEK', 'BEK', 'H1'
|
||||||
|
final int currentCount; // Aktueller Bestand (z.B. im Fahrzeug)
|
||||||
|
final int targetCount; // Soll-Zahl (z.B. Beladezähler)
|
||||||
|
final int? alternateCount; // Alternative Zählung (z.B. HADAG, CR, SST)
|
||||||
|
|
||||||
|
const ObjectCounter({
|
||||||
|
required this.objectType,
|
||||||
|
required this.label,
|
||||||
|
required this.currentCount,
|
||||||
|
required this.targetCount,
|
||||||
|
this.alternateCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isComplete => currentCount >= targetCount;
|
||||||
|
bool get isOver => currentCount > targetCount;
|
||||||
|
int get difference => targetCount - currentCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [objectType, label, currentCount, targetCount, alternateCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gruppen von Zählern für verschiedene Ansichten
|
||||||
|
class CounterGroup extends Equatable {
|
||||||
|
final String title;
|
||||||
|
final List<ObjectCounter> counters;
|
||||||
|
|
||||||
|
const CounterGroup({
|
||||||
|
required this.title,
|
||||||
|
required this.counters,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [title, counters];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zähler-Übersicht für eine komplette Tour/Page
|
||||||
|
class CounterOverview extends Equatable {
|
||||||
|
final int tourId;
|
||||||
|
final String? pageId;
|
||||||
|
final List<CounterGroup> groups;
|
||||||
|
final DateTime? lastUpdated;
|
||||||
|
|
||||||
|
const CounterOverview({
|
||||||
|
required this.tourId,
|
||||||
|
this.pageId,
|
||||||
|
required this.groups,
|
||||||
|
this.lastUpdated,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Standard-Gruppen für StockStart (Lager Beladung)
|
||||||
|
/// Entspricht Lua: CreateLoadingStockStartView
|
||||||
|
factory CounterOverview.stockStart({
|
||||||
|
required int tourId,
|
||||||
|
required List<ObjectCounter> vehicleStock, // Bestand Fzg
|
||||||
|
required List<ObjectCounter> loadingCounters, // Beladezähler
|
||||||
|
required List<ObjectCounter> hadagCounters, // HADAG
|
||||||
|
required List<ObjectCounter> sstCounters, // SST
|
||||||
|
required List<ObjectCounter> crCounters, // CR
|
||||||
|
}) {
|
||||||
|
return CounterOverview(
|
||||||
|
tourId: tourId,
|
||||||
|
groups: [
|
||||||
|
CounterGroup(title: 'Bestand Fzg', counters: vehicleStock),
|
||||||
|
CounterGroup(title: 'Beladezähler', counters: loadingCounters),
|
||||||
|
CounterGroup(title: 'HADAG', counters: hadagCounters),
|
||||||
|
CounterGroup(title: 'SST', counters: sstCounters),
|
||||||
|
CounterGroup(title: 'CR', counters: crCounters),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard-Gruppen für VehStart
|
||||||
|
/// Entspricht Lua: CreateLoadingVehStartView
|
||||||
|
factory CounterOverview.vehStart({
|
||||||
|
required int tourId,
|
||||||
|
required List<ObjectCounter> loadingCounters,
|
||||||
|
}) {
|
||||||
|
return CounterOverview(
|
||||||
|
tourId: tourId,
|
||||||
|
groups: [
|
||||||
|
CounterGroup(title: 'Beladezähler', counters: loadingCounters),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard-Gruppen für Veh (Station)
|
||||||
|
/// Entspricht Lua: CreateLoadingVehView
|
||||||
|
factory CounterOverview.veh({
|
||||||
|
required int tourId,
|
||||||
|
String? pageId,
|
||||||
|
required List<ObjectCounter> swapCounters, // Wechselzähler
|
||||||
|
required List<ObjectCounter> pickupCounters, // Abholzähler
|
||||||
|
}) {
|
||||||
|
return CounterOverview(
|
||||||
|
tourId: tourId,
|
||||||
|
pageId: pageId,
|
||||||
|
groups: [
|
||||||
|
CounterGroup(title: 'Wechsel', counters: swapCounters),
|
||||||
|
CounterGroup(title: 'Abholung', counters: pickupCounters),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tourId, pageId, groups, lastUpdated];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pickup/Abhol-Zähler aus der Datenbank
|
||||||
|
/// Entspricht Lua: page_pickup_count Tabelle
|
||||||
|
class PickupCount extends Equatable {
|
||||||
|
final int tourId;
|
||||||
|
final String pageId;
|
||||||
|
final String objectType;
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
const PickupCount({
|
||||||
|
required this.tourId,
|
||||||
|
required this.pageId,
|
||||||
|
required this.objectType,
|
||||||
|
required this.count,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tourId, pageId, objectType, count];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap/Wechsel-Zähler aus der Datenbank
|
||||||
|
/// Entspricht Lua: page_swap_count Tabelle
|
||||||
|
class SwapCount extends Equatable {
|
||||||
|
final int tourId;
|
||||||
|
final String pageId;
|
||||||
|
final String objectType;
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
const SwapCount({
|
||||||
|
required this.tourId,
|
||||||
|
required this.pageId,
|
||||||
|
required this.objectType,
|
||||||
|
required this.count,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tourId, pageId, objectType, count];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Container-Information für VS/Verwahrungsstelle
|
||||||
|
/// Entspricht Lua: ContainerId, ContainerType in vsStateMachine
|
||||||
|
class ContainerInfo extends Equatable {
|
||||||
|
final String containerId;
|
||||||
|
final String containerType; // 'a' = Geldinstitut, 'b' = Dienststelle
|
||||||
|
final String? subtype; // 'cntra' oder 'cntrb'
|
||||||
|
final int? objectCount; // Anzahl Objekte im Container
|
||||||
|
|
||||||
|
const ContainerInfo({
|
||||||
|
required this.containerId,
|
||||||
|
required this.containerType,
|
||||||
|
this.subtype,
|
||||||
|
this.objectCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isForGI => containerType == 'a';
|
||||||
|
bool get isForDS => containerType == 'b';
|
||||||
|
|
||||||
|
String get displayName {
|
||||||
|
if (isForGI) return 'Container Geldinstitut';
|
||||||
|
if (isForDS) return 'Container Dienststelle';
|
||||||
|
return 'Container $containerId';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [containerId, containerType, subtype, objectCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Letzte gescannte Objekte für die Anzeige
|
||||||
|
/// Entspricht Lua: Die Liste in ShowStockStartScreen etc.
|
||||||
|
class RecentScan extends Equatable {
|
||||||
|
final String objectCode;
|
||||||
|
final String objectName;
|
||||||
|
final String state;
|
||||||
|
final DateTime scanTime;
|
||||||
|
final String? imageName;
|
||||||
|
|
||||||
|
const RecentScan({
|
||||||
|
required this.objectCode,
|
||||||
|
required this.objectName,
|
||||||
|
required this.state,
|
||||||
|
required this.scanTime,
|
||||||
|
this.imageName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [objectCode, objectName, state, scanTime, imageName];
|
||||||
|
}
|
||||||
89
app/lib/domain/entities/location.dart
Normal file
89
app/lib/domain/entities/location.dart
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class Location extends Equatable {
|
||||||
|
final int id;
|
||||||
|
final int locationId;
|
||||||
|
final int version;
|
||||||
|
final String name;
|
||||||
|
final String? street;
|
||||||
|
final String? number;
|
||||||
|
final String? zip;
|
||||||
|
final String? city;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final String? remark;
|
||||||
|
|
||||||
|
const Location({
|
||||||
|
required this.id,
|
||||||
|
required this.locationId,
|
||||||
|
required this.version,
|
||||||
|
required this.name,
|
||||||
|
this.street,
|
||||||
|
this.number,
|
||||||
|
this.zip,
|
||||||
|
this.city,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.remark,
|
||||||
|
});
|
||||||
|
|
||||||
|
Location copyWith({
|
||||||
|
int? id,
|
||||||
|
int? locationId,
|
||||||
|
int? version,
|
||||||
|
String? name,
|
||||||
|
String? street,
|
||||||
|
String? number,
|
||||||
|
String? zip,
|
||||||
|
String? city,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
|
String? remark,
|
||||||
|
}) {
|
||||||
|
return Location(
|
||||||
|
id: id ?? this.id,
|
||||||
|
locationId: locationId ?? this.locationId,
|
||||||
|
version: version ?? this.version,
|
||||||
|
name: name ?? this.name,
|
||||||
|
street: street ?? this.street,
|
||||||
|
number: number ?? this.number,
|
||||||
|
zip: zip ?? this.zip,
|
||||||
|
city: city ?? this.city,
|
||||||
|
latitude: latitude ?? this.latitude,
|
||||||
|
longitude: longitude ?? this.longitude,
|
||||||
|
remark: remark ?? this.remark,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get fullAddress {
|
||||||
|
final parts = <String>[];
|
||||||
|
if (street != null && street!.isNotEmpty) {
|
||||||
|
parts.add(street!);
|
||||||
|
if (number != null && number!.isNotEmpty) {
|
||||||
|
parts.add(number!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (zip != null && zip!.isNotEmpty) {
|
||||||
|
parts.add(zip!);
|
||||||
|
}
|
||||||
|
if (city != null && city!.isNotEmpty) {
|
||||||
|
parts.add(city!);
|
||||||
|
}
|
||||||
|
return parts.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
locationId,
|
||||||
|
version,
|
||||||
|
name,
|
||||||
|
street,
|
||||||
|
number,
|
||||||
|
zip,
|
||||||
|
city,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
remark,
|
||||||
|
];
|
||||||
|
}
|
||||||
250
app/lib/domain/entities/logistic_object.dart
Normal file
250
app/lib/domain/entities/logistic_object.dart
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LogisticObject extends Equatable {
|
||||||
|
final int id;
|
||||||
|
final int objectId;
|
||||||
|
final int type;
|
||||||
|
final int version;
|
||||||
|
final int? locationId;
|
||||||
|
final String code;
|
||||||
|
final String? remark;
|
||||||
|
final String state;
|
||||||
|
final String subtype;
|
||||||
|
final String? origin;
|
||||||
|
final bool isManual;
|
||||||
|
final DateTime? lastModified;
|
||||||
|
final String? typeName;
|
||||||
|
final String? typeMnemonic;
|
||||||
|
|
||||||
|
const LogisticObject({
|
||||||
|
required this.id,
|
||||||
|
required this.objectId,
|
||||||
|
required this.type,
|
||||||
|
required this.version,
|
||||||
|
this.locationId,
|
||||||
|
required this.code,
|
||||||
|
this.remark,
|
||||||
|
required this.state,
|
||||||
|
required this.subtype,
|
||||||
|
this.origin,
|
||||||
|
this.isManual = false,
|
||||||
|
this.lastModified,
|
||||||
|
this.typeName,
|
||||||
|
this.typeMnemonic,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogisticObject copyWith({
|
||||||
|
int? id,
|
||||||
|
int? objectId,
|
||||||
|
int? type,
|
||||||
|
int? version,
|
||||||
|
int? locationId,
|
||||||
|
String? code,
|
||||||
|
String? remark,
|
||||||
|
String? state,
|
||||||
|
String? subtype,
|
||||||
|
String? origin,
|
||||||
|
bool? isManual,
|
||||||
|
DateTime? lastModified,
|
||||||
|
String? typeName,
|
||||||
|
String? typeMnemonic,
|
||||||
|
}) {
|
||||||
|
return LogisticObject(
|
||||||
|
id: id ?? this.id,
|
||||||
|
objectId: objectId ?? this.objectId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
version: version ?? this.version,
|
||||||
|
locationId: locationId ?? this.locationId,
|
||||||
|
code: code ?? this.code,
|
||||||
|
remark: remark ?? this.remark,
|
||||||
|
state: state ?? this.state,
|
||||||
|
subtype: subtype ?? this.subtype,
|
||||||
|
origin: origin ?? this.origin,
|
||||||
|
isManual: isManual ?? this.isManual,
|
||||||
|
lastModified: lastModified ?? this.lastModified,
|
||||||
|
typeName: typeName ?? this.typeName,
|
||||||
|
typeMnemonic: typeMnemonic ?? this.typeMnemonic,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
objectId,
|
||||||
|
type,
|
||||||
|
version,
|
||||||
|
locationId,
|
||||||
|
code,
|
||||||
|
remark,
|
||||||
|
state,
|
||||||
|
subtype,
|
||||||
|
origin,
|
||||||
|
isManual,
|
||||||
|
lastModified,
|
||||||
|
typeName,
|
||||||
|
typeMnemonic,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectMetadata extends Equatable {
|
||||||
|
final int id;
|
||||||
|
final int type;
|
||||||
|
final int version;
|
||||||
|
final String mnemonic;
|
||||||
|
final String name;
|
||||||
|
final String prefix;
|
||||||
|
final String subtype;
|
||||||
|
final String counterText;
|
||||||
|
|
||||||
|
const ObjectMetadata({
|
||||||
|
required this.id,
|
||||||
|
required this.type,
|
||||||
|
required this.version,
|
||||||
|
required this.mnemonic,
|
||||||
|
required this.name,
|
||||||
|
required this.prefix,
|
||||||
|
required this.subtype,
|
||||||
|
required this.counterText,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
version,
|
||||||
|
mnemonic,
|
||||||
|
name,
|
||||||
|
prefix,
|
||||||
|
subtype,
|
||||||
|
counterText,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectStateInfo {
|
||||||
|
final String state;
|
||||||
|
final String displayName;
|
||||||
|
final int colorValue;
|
||||||
|
final String iconName;
|
||||||
|
|
||||||
|
const ObjectStateInfo({
|
||||||
|
required this.state,
|
||||||
|
required this.displayName,
|
||||||
|
required this.colorValue,
|
||||||
|
required this.iconName,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const Map<String, ObjectStateInfo> stateInfos = {
|
||||||
|
'unknown': ObjectStateInfo(
|
||||||
|
state: 'unknown',
|
||||||
|
displayName: 'Unbekannt',
|
||||||
|
colorValue: 0xFFFFFFFF,
|
||||||
|
iconName: 'help',
|
||||||
|
),
|
||||||
|
'delivery': ObjectStateInfo(
|
||||||
|
state: 'delivery',
|
||||||
|
displayName: 'Im Fahrzeug',
|
||||||
|
colorValue: 0xFFB3B3B3,
|
||||||
|
iconName: 'local_shipping',
|
||||||
|
),
|
||||||
|
'to_delivery': ObjectStateInfo(
|
||||||
|
state: 'to_delivery',
|
||||||
|
displayName: 'Zum Fahrzeug',
|
||||||
|
colorValue: 0xFFB3B3B3,
|
||||||
|
iconName: 'local_shipping_outlined',
|
||||||
|
),
|
||||||
|
'station': ObjectStateInfo(
|
||||||
|
state: 'station',
|
||||||
|
displayName: 'An Station',
|
||||||
|
colorValue: 0xFFFFDD00,
|
||||||
|
iconName: 'location_on',
|
||||||
|
),
|
||||||
|
'in_fa': ObjectStateInfo(
|
||||||
|
state: 'in_fa',
|
||||||
|
displayName: 'Im Fahrscheinautomat',
|
||||||
|
colorValue: 0xFF9CDA7A,
|
||||||
|
iconName: 'confirmation_number',
|
||||||
|
),
|
||||||
|
'in_vs': ObjectStateInfo(
|
||||||
|
state: 'in_vs',
|
||||||
|
displayName: 'In Versorgungsstelle',
|
||||||
|
colorValue: 0xFFFAE14B,
|
||||||
|
iconName: 'inventory',
|
||||||
|
),
|
||||||
|
'ret_fail': ObjectStateInfo(
|
||||||
|
state: 'ret_fail',
|
||||||
|
displayName: 'Fehler - zur Dienststelle',
|
||||||
|
colorValue: 0xFFFF9081,
|
||||||
|
iconName: 'error',
|
||||||
|
),
|
||||||
|
'ret_fail_fzg': ObjectStateInfo(
|
||||||
|
state: 'ret_fail_fzg',
|
||||||
|
displayName: 'Fehler - im Fahrzeug',
|
||||||
|
colorValue: 0xFFFF9081,
|
||||||
|
iconName: 'error_outline',
|
||||||
|
),
|
||||||
|
'ret_ds': ObjectStateInfo(
|
||||||
|
state: 'ret_ds',
|
||||||
|
displayName: 'Zur Dienststelle',
|
||||||
|
colorValue: 0xFFAFE0ED,
|
||||||
|
iconName: 'account_balance',
|
||||||
|
),
|
||||||
|
'ret_ds_fzg': ObjectStateInfo(
|
||||||
|
state: 'ret_ds_fzg',
|
||||||
|
displayName: 'Zur Dienststelle (Fzg)',
|
||||||
|
colorValue: 0xFFAFE0ED,
|
||||||
|
iconName: 'account_balance_outlined',
|
||||||
|
),
|
||||||
|
'ret_gi': ObjectStateInfo(
|
||||||
|
state: 'ret_gi',
|
||||||
|
displayName: 'Zum Geldinstitut',
|
||||||
|
colorValue: 0xFFAFE0ED,
|
||||||
|
iconName: 'account_balance_wallet',
|
||||||
|
),
|
||||||
|
'ret_gi_fzg': ObjectStateInfo(
|
||||||
|
state: 'ret_gi_fzg',
|
||||||
|
displayName: 'Zum Geldinstitut (Fzg)',
|
||||||
|
colorValue: 0xFFAFE0ED,
|
||||||
|
iconName: 'account_balance_wallet_outlined',
|
||||||
|
),
|
||||||
|
'fin_ds': ObjectStateInfo(
|
||||||
|
state: 'fin_ds',
|
||||||
|
displayName: 'In Dienststelle',
|
||||||
|
colorValue: 0xFF29B7FB,
|
||||||
|
iconName: 'check_circle',
|
||||||
|
),
|
||||||
|
'fin_gi': ObjectStateInfo(
|
||||||
|
state: 'fin_gi',
|
||||||
|
displayName: 'In Geldinstitut',
|
||||||
|
colorValue: 0xFF25BAFC,
|
||||||
|
iconName: 'check_circle_outline',
|
||||||
|
),
|
||||||
|
'hdl': ObjectStateInfo(
|
||||||
|
state: 'hdl',
|
||||||
|
displayName: 'Handel',
|
||||||
|
colorValue: 0xFF9E9E9E,
|
||||||
|
iconName: 'shopping_cart',
|
||||||
|
),
|
||||||
|
'ret_ds_empty': ObjectStateInfo(
|
||||||
|
state: 'ret_ds_empty',
|
||||||
|
displayName: 'Leer - zur Dienststelle',
|
||||||
|
colorValue: 0xFFAFE0ED,
|
||||||
|
iconName: 'remove_circle_outline',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
static Color getColorForState(String state) {
|
||||||
|
final info = stateInfos[state];
|
||||||
|
return info != null ? Color(info.colorValue) : const Color(0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getDisplayName(String state) {
|
||||||
|
final info = stateInfos[state];
|
||||||
|
return info?.displayName ?? 'Unbekannt';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getIconName(String state) {
|
||||||
|
final info = stateInfos[state];
|
||||||
|
return info?.iconName ?? 'help';
|
||||||
|
}
|
||||||
|
}
|
||||||
163
app/lib/domain/entities/tour.dart
Normal file
163
app/lib/domain/entities/tour.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class Tour extends Equatable {
|
||||||
|
final int id;
|
||||||
|
final int jobId;
|
||||||
|
final int tourId;
|
||||||
|
final int version;
|
||||||
|
final int state; // 0 = offen, 1 = erledigt, 2 = abgeschlossen
|
||||||
|
final String type;
|
||||||
|
final int sort;
|
||||||
|
final int locationId;
|
||||||
|
final String locationCode;
|
||||||
|
final String? locationCode2;
|
||||||
|
final String? remark;
|
||||||
|
final String? menuText;
|
||||||
|
final int modified;
|
||||||
|
final String? deliveryCode;
|
||||||
|
final String? locationName;
|
||||||
|
final bool isCompleted;
|
||||||
|
final List<TourPage> pages;
|
||||||
|
|
||||||
|
const Tour({
|
||||||
|
required this.id,
|
||||||
|
required this.jobId,
|
||||||
|
required this.tourId,
|
||||||
|
required this.version,
|
||||||
|
required this.state,
|
||||||
|
required this.type,
|
||||||
|
required this.sort,
|
||||||
|
required this.locationId,
|
||||||
|
required this.locationCode,
|
||||||
|
this.locationCode2,
|
||||||
|
this.remark,
|
||||||
|
this.menuText,
|
||||||
|
required this.modified,
|
||||||
|
this.deliveryCode,
|
||||||
|
this.locationName,
|
||||||
|
this.isCompleted = false,
|
||||||
|
this.pages = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
Tour copyWith({
|
||||||
|
int? id,
|
||||||
|
int? jobId,
|
||||||
|
int? tourId,
|
||||||
|
int? version,
|
||||||
|
int? state,
|
||||||
|
String? type,
|
||||||
|
int? sort,
|
||||||
|
int? locationId,
|
||||||
|
String? locationCode,
|
||||||
|
String? locationCode2,
|
||||||
|
String? remark,
|
||||||
|
String? menuText,
|
||||||
|
int? modified,
|
||||||
|
String? deliveryCode,
|
||||||
|
String? locationName,
|
||||||
|
bool? isCompleted,
|
||||||
|
List<TourPage>? pages,
|
||||||
|
}) {
|
||||||
|
return Tour(
|
||||||
|
id: id ?? this.id,
|
||||||
|
jobId: jobId ?? this.jobId,
|
||||||
|
tourId: tourId ?? this.tourId,
|
||||||
|
version: version ?? this.version,
|
||||||
|
state: state ?? this.state,
|
||||||
|
type: type ?? this.type,
|
||||||
|
sort: sort ?? this.sort,
|
||||||
|
locationId: locationId ?? this.locationId,
|
||||||
|
locationCode: locationCode ?? this.locationCode,
|
||||||
|
locationCode2: locationCode2 ?? this.locationCode2,
|
||||||
|
remark: remark ?? this.remark,
|
||||||
|
menuText: menuText ?? this.menuText,
|
||||||
|
modified: modified ?? this.modified,
|
||||||
|
deliveryCode: deliveryCode ?? this.deliveryCode,
|
||||||
|
locationName: locationName ?? this.locationName,
|
||||||
|
isCompleted: isCompleted ?? this.isCompleted,
|
||||||
|
pages: pages ?? this.pages,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
jobId,
|
||||||
|
tourId,
|
||||||
|
version,
|
||||||
|
state,
|
||||||
|
type,
|
||||||
|
sort,
|
||||||
|
locationId,
|
||||||
|
locationCode,
|
||||||
|
locationCode2,
|
||||||
|
remark,
|
||||||
|
menuText,
|
||||||
|
modified,
|
||||||
|
deliveryCode,
|
||||||
|
locationName,
|
||||||
|
isCompleted,
|
||||||
|
pages,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class TourPage extends Equatable {
|
||||||
|
final int id;
|
||||||
|
final int tourId;
|
||||||
|
final int pageNumber;
|
||||||
|
final String pageId;
|
||||||
|
final String type;
|
||||||
|
final String? code;
|
||||||
|
final String? label;
|
||||||
|
final Map<String, int> pickupCounts;
|
||||||
|
final Map<String, int> swapCounts;
|
||||||
|
|
||||||
|
const TourPage({
|
||||||
|
required this.id,
|
||||||
|
required this.tourId,
|
||||||
|
required this.pageNumber,
|
||||||
|
required this.pageId,
|
||||||
|
required this.type,
|
||||||
|
this.code,
|
||||||
|
this.label,
|
||||||
|
this.pickupCounts = const {},
|
||||||
|
this.swapCounts = const {},
|
||||||
|
});
|
||||||
|
|
||||||
|
TourPage copyWith({
|
||||||
|
int? id,
|
||||||
|
int? tourId,
|
||||||
|
int? pageNumber,
|
||||||
|
String? pageId,
|
||||||
|
String? type,
|
||||||
|
String? code,
|
||||||
|
String? label,
|
||||||
|
Map<String, int>? pickupCounts,
|
||||||
|
Map<String, int>? swapCounts,
|
||||||
|
}) {
|
||||||
|
return TourPage(
|
||||||
|
id: id ?? this.id,
|
||||||
|
tourId: tourId ?? this.tourId,
|
||||||
|
pageNumber: pageNumber ?? this.pageNumber,
|
||||||
|
pageId: pageId ?? this.pageId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
code: code ?? this.code,
|
||||||
|
label: label ?? this.label,
|
||||||
|
pickupCounts: pickupCounts ?? this.pickupCounts,
|
||||||
|
swapCounts: swapCounts ?? this.swapCounts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
tourId,
|
||||||
|
pageNumber,
|
||||||
|
pageId,
|
||||||
|
type,
|
||||||
|
code,
|
||||||
|
label,
|
||||||
|
pickupCounts,
|
||||||
|
swapCounts,
|
||||||
|
];
|
||||||
|
}
|
||||||
109
app/lib/domain/repositories/tour_repository.dart
Normal file
109
app/lib/domain/repositories/tour_repository.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import '../entities/tour.dart';
|
||||||
|
import '../entities/logistic_object.dart';
|
||||||
|
import '../entities/location.dart';
|
||||||
|
import '../entities/counter.dart';
|
||||||
|
import '../../core/errors/failures.dart';
|
||||||
|
|
||||||
|
abstract class TourRepository {
|
||||||
|
// Touren
|
||||||
|
Future<Either<Failure, List<Tour>>> getTours();
|
||||||
|
Future<Either<Failure, Tour>> getTourById(int tourId);
|
||||||
|
Future<Either<Failure, void>> updateTourState(int tourId, int state);
|
||||||
|
Future<Either<Failure, void>> completeTour(int tourId);
|
||||||
|
|
||||||
|
// Sync
|
||||||
|
Future<Either<Failure, void>> syncData();
|
||||||
|
Future<Either<Failure, bool>> checkForUpdates();
|
||||||
|
|
||||||
|
// Objects
|
||||||
|
Future<Either<Failure, List<LogisticObject>>> getObjectsByTour(int tourId);
|
||||||
|
Future<Either<Failure, List<LogisticObject>>> getObjectsByState(String state);
|
||||||
|
Future<Either<Failure, LogisticObject?>> getObjectByBarcode(String barcode);
|
||||||
|
Future<Either<Failure, void>> updateObjectState(
|
||||||
|
int objectId,
|
||||||
|
String newState, {
|
||||||
|
int? locationId,
|
||||||
|
int? refType,
|
||||||
|
int? refId,
|
||||||
|
String? containerCode,
|
||||||
|
});
|
||||||
|
Future<Either<Failure, void>> createObject({
|
||||||
|
required int type,
|
||||||
|
required String code,
|
||||||
|
bool isManual = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Locations
|
||||||
|
Future<Either<Failure, List<Location>>> getLocations();
|
||||||
|
Future<Either<Failure, Location>> getLocationById(int locationId);
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
Future<Either<Failure, List<ObjectMetadata>>> getObjectMetadata();
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
Future<Either<Failure, TourStatistics>> getTourStatistics(int tourId);
|
||||||
|
Future<Either<Failure, ObjectStatistics>> getObjectStatistics();
|
||||||
|
|
||||||
|
// Counter operations (spezifisch für Lua-kompatible Ansichten)
|
||||||
|
Future<Either<Failure, CounterOverview>> getCounterOverview(
|
||||||
|
int tourId,
|
||||||
|
String tourType, {
|
||||||
|
String? pageId,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Either<Failure, List<PickupCount>>> getPickupCounts(int tourId, String pageId);
|
||||||
|
Future<Either<Failure, List<SwapCount>>> getSwapCounts(int tourId, String pageId);
|
||||||
|
|
||||||
|
// Container operations
|
||||||
|
Future<Either<Failure, List<ContainerInfo>>> getOpenContainers();
|
||||||
|
Future<Either<Failure, void>> addObjectToContainer(
|
||||||
|
String containerId,
|
||||||
|
String containerType,
|
||||||
|
int objectId,
|
||||||
|
);
|
||||||
|
Future<Either<Failure, void>> closeContainer(
|
||||||
|
String containerId,
|
||||||
|
String containerType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Recent scans
|
||||||
|
Future<Either<Failure, List<RecentScan>>> getRecentScans(
|
||||||
|
int tourId, {
|
||||||
|
int limit = 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TourStatistics {
|
||||||
|
final int totalObjects;
|
||||||
|
final int completedObjects;
|
||||||
|
final int pendingObjects;
|
||||||
|
final Map<String, int> objectsByState;
|
||||||
|
final Map<String, int> objectsByType;
|
||||||
|
final double completionPercentage;
|
||||||
|
|
||||||
|
const TourStatistics({
|
||||||
|
required this.totalObjects,
|
||||||
|
required this.completedObjects,
|
||||||
|
required this.pendingObjects,
|
||||||
|
required this.objectsByState,
|
||||||
|
required this.objectsByType,
|
||||||
|
required this.completionPercentage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObjectStatistics {
|
||||||
|
final Map<String, int> byState;
|
||||||
|
final Map<String, int> byType;
|
||||||
|
final Map<String, int> byLocation;
|
||||||
|
final List<LogisticObject> recentObjects;
|
||||||
|
final int totalCount;
|
||||||
|
|
||||||
|
const ObjectStatistics({
|
||||||
|
required this.byState,
|
||||||
|
required this.byType,
|
||||||
|
required this.byLocation,
|
||||||
|
required this.recentObjects,
|
||||||
|
required this.totalCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
424
app/lib/main.dart
Normal file
424
app/lib/main.dart
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
import 'package:dartz/dartz.dart' hide State;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'core/theme/app_theme.dart';
|
||||||
|
import 'domain/entities/tour.dart';
|
||||||
|
import 'domain/entities/logistic_object.dart';
|
||||||
|
import 'domain/entities/location.dart';
|
||||||
|
import 'domain/entities/counter.dart';
|
||||||
|
import 'domain/repositories/tour_repository.dart';
|
||||||
|
import 'core/errors/failures.dart';
|
||||||
|
import 'presentation/blocs/tour/tour_bloc.dart';
|
||||||
|
import 'presentation/blocs/scan/scan_bloc.dart';
|
||||||
|
import 'presentation/pages/tours/tours_page.dart';
|
||||||
|
import 'presentation/pages/tours/dashboard_page.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Set preferred orientations
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
DeviceOrientation.portraitDown,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set system UI overlay style
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
const SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
statusBarIconBrightness: Brightness.light,
|
||||||
|
systemNavigationBarColor: Colors.white,
|
||||||
|
systemNavigationBarIconBrightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
runApp(const HHAApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class HHAApp extends StatelessWidget {
|
||||||
|
const HHAApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final repository = MockTourRepository();
|
||||||
|
|
||||||
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => TourBloc(
|
||||||
|
repository: repository,
|
||||||
|
)..add(const LoadTours()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => ScanBloc(
|
||||||
|
repository: repository,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
title: 'HHA Logistics',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: AppTheme.lightTheme,
|
||||||
|
home: const MainNavigationPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainNavigationPage extends StatefulWidget {
|
||||||
|
const MainNavigationPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MainNavigationPage> createState() => _MainNavigationPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MainNavigationPageState extends State<MainNavigationPage> {
|
||||||
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
final List<Widget> _pages = const [
|
||||||
|
DashboardPage(),
|
||||||
|
ToursPage(),
|
||||||
|
InventoryPage(),
|
||||||
|
SettingsPage(),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: IndexedStack(
|
||||||
|
index: _currentIndex,
|
||||||
|
children: _pages,
|
||||||
|
),
|
||||||
|
bottomNavigationBar: NavigationBar(
|
||||||
|
selectedIndex: _currentIndex,
|
||||||
|
onDestinationSelected: (index) {
|
||||||
|
setState(() {
|
||||||
|
_currentIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destinations: const [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.dashboard_outlined),
|
||||||
|
selectedIcon: Icon(Icons.dashboard),
|
||||||
|
label: 'Dashboard',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.map_outlined),
|
||||||
|
selectedIcon: Icon(Icons.map),
|
||||||
|
label: 'Touren',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.inventory_2_outlined),
|
||||||
|
selectedIcon: Icon(Icons.inventory_2),
|
||||||
|
label: 'Bestand',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.settings_outlined),
|
||||||
|
selectedIcon: Icon(Icons.settings),
|
||||||
|
label: 'Einstellungen',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock Repository for demonstration
|
||||||
|
class MockTourRepository implements TourRepository {
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<Tour>>> getTours() async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
return const Right([
|
||||||
|
Tour(
|
||||||
|
id: 1,
|
||||||
|
jobId: 1,
|
||||||
|
tourId: 1,
|
||||||
|
version: 1,
|
||||||
|
state: 0,
|
||||||
|
type: 'stock_start',
|
||||||
|
sort: 1,
|
||||||
|
locationId: 1,
|
||||||
|
locationCode: 'LAGER001',
|
||||||
|
locationName: 'Hauptlager Wandsbek',
|
||||||
|
modified: 0,
|
||||||
|
),
|
||||||
|
Tour(
|
||||||
|
id: 2,
|
||||||
|
jobId: 1,
|
||||||
|
tourId: 2,
|
||||||
|
version: 1,
|
||||||
|
state: 0,
|
||||||
|
type: 'veh_start',
|
||||||
|
sort: 2,
|
||||||
|
locationId: 2,
|
||||||
|
locationCode: 'DST001',
|
||||||
|
locationName: 'Dienststelle Hammerbrook',
|
||||||
|
modified: 0,
|
||||||
|
),
|
||||||
|
Tour(
|
||||||
|
id: 3,
|
||||||
|
jobId: 1,
|
||||||
|
tourId: 3,
|
||||||
|
version: 1,
|
||||||
|
state: 0,
|
||||||
|
type: 'st',
|
||||||
|
sort: 3,
|
||||||
|
locationId: 3,
|
||||||
|
locationCode: 'HALT001',
|
||||||
|
locationName: 'Hauptbahnhof Nord',
|
||||||
|
remark: '4 Fahrscheinautomaten',
|
||||||
|
modified: 0,
|
||||||
|
),
|
||||||
|
Tour(
|
||||||
|
id: 4,
|
||||||
|
jobId: 1,
|
||||||
|
tourId: 4,
|
||||||
|
version: 1,
|
||||||
|
state: 1,
|
||||||
|
type: 'st',
|
||||||
|
sort: 4,
|
||||||
|
locationId: 4,
|
||||||
|
locationCode: 'HALT002',
|
||||||
|
locationName: 'Jungfernstieg',
|
||||||
|
remark: '2 Fahrscheinautomaten',
|
||||||
|
modified: 0,
|
||||||
|
),
|
||||||
|
Tour(
|
||||||
|
id: 5,
|
||||||
|
jobId: 1,
|
||||||
|
tourId: 5,
|
||||||
|
version: 1,
|
||||||
|
state: 0,
|
||||||
|
type: 'gi',
|
||||||
|
sort: 5,
|
||||||
|
locationId: 5,
|
||||||
|
locationCode: 'BANK001',
|
||||||
|
locationName: 'Geldinstitut Mitte',
|
||||||
|
modified: 0,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, Tour>> getTourById(int tourId) async {
|
||||||
|
final tours = await getTours();
|
||||||
|
return tours.fold(
|
||||||
|
(failure) => Left(failure),
|
||||||
|
(tourList) {
|
||||||
|
final tour = tourList.firstWhere(
|
||||||
|
(t) => t.tourId == tourId,
|
||||||
|
orElse: () => throw Exception('Tour not found'),
|
||||||
|
);
|
||||||
|
return Right(tour);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> updateTourState(int tourId, int state) async {
|
||||||
|
return const Right(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> completeTour(int tourId) async {
|
||||||
|
return const Right(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> syncData() async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
return const Right(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, bool>> checkForUpdates() async => const Right(false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<LogisticObject>>> getObjectsByTour(int tourId) async => const Right([]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<LogisticObject>>> getObjectsByState(String state) async => const Right([]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, LogisticObject?>> getObjectByBarcode(String barcode) async => const Right(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> updateObjectState(
|
||||||
|
int objectId,
|
||||||
|
String newState, {
|
||||||
|
int? locationId,
|
||||||
|
int? refType,
|
||||||
|
int? refId,
|
||||||
|
String? containerCode,
|
||||||
|
}) async => const Right(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> createObject({
|
||||||
|
required int type,
|
||||||
|
required String code,
|
||||||
|
bool isManual = true,
|
||||||
|
}) async => const Right(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<Location>>> getLocations() async => const Right([]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, Location>> getLocationById(int locationId) async {
|
||||||
|
return const Left(NotFoundFailure(message: 'Location not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<ObjectMetadata>>> getObjectMetadata() async => const Right([]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, TourStatistics>> getTourStatistics(int tourId) async {
|
||||||
|
return const Right(TourStatistics(
|
||||||
|
totalObjects: 10,
|
||||||
|
completedObjects: 5,
|
||||||
|
pendingObjects: 5,
|
||||||
|
objectsByState: {'delivery': 3, 'station': 2},
|
||||||
|
objectsByType: {'meka': 3, 'beka': 2},
|
||||||
|
completionPercentage: 50.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, ObjectStatistics>> getObjectStatistics() async {
|
||||||
|
return const Right(ObjectStatistics(
|
||||||
|
byState: {},
|
||||||
|
byType: {},
|
||||||
|
byLocation: {},
|
||||||
|
recentObjects: [],
|
||||||
|
totalCount: 0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, CounterOverview>> getCounterOverview(
|
||||||
|
int tourId,
|
||||||
|
String tourType, {
|
||||||
|
String? pageId,
|
||||||
|
}) async {
|
||||||
|
return Right(CounterOverview(
|
||||||
|
tourId: tourId,
|
||||||
|
pageId: pageId,
|
||||||
|
groups: [],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<PickupCount>>> getPickupCounts(
|
||||||
|
int tourId,
|
||||||
|
String pageId,
|
||||||
|
) async => const Right([]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<SwapCount>>> getSwapCounts(
|
||||||
|
int tourId,
|
||||||
|
String pageId,
|
||||||
|
) async => const Right([]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<ContainerInfo>>> getOpenContainers() async => const Right([]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> addObjectToContainer(
|
||||||
|
String containerId,
|
||||||
|
String containerType,
|
||||||
|
int objectId,
|
||||||
|
) async => const Right(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> closeContainer(
|
||||||
|
String containerId,
|
||||||
|
String containerType,
|
||||||
|
) async => const Right(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, List<RecentScan>>> getRecentScans(
|
||||||
|
int tourId, {
|
||||||
|
int limit = 10,
|
||||||
|
}) async => const Right([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder pages
|
||||||
|
class InventoryPage extends StatelessWidget {
|
||||||
|
const InventoryPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Bestand'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.inventory_2,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Bestandsübersicht',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Hier wird der aktuelle Bestand angezeigt',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsPage extends StatelessWidget {
|
||||||
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Einstellungen'),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.sync),
|
||||||
|
title: const Text('Daten synchronisieren'),
|
||||||
|
subtitle: const Text('Letzte Synchronisation: Gerade eben'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
// Trigger sync
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.visibility),
|
||||||
|
title: const Text('Erledigte Stationen anzeigen'),
|
||||||
|
trailing: Switch(
|
||||||
|
value: true,
|
||||||
|
onChanged: (value) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.info),
|
||||||
|
title: const Text('Über'),
|
||||||
|
subtitle: const Text('Version 2.0.0'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
916
app/lib/presentation/blocs/scan/scan_bloc.dart
Normal file
916
app/lib/presentation/blocs/scan/scan_bloc.dart
Normal file
@@ -0,0 +1,916 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import '../../../domain/entities/logistic_object.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../../domain/repositories/tour_repository.dart';
|
||||||
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
|
||||||
|
part 'scan_event.dart';
|
||||||
|
part 'scan_state.dart';
|
||||||
|
|
||||||
|
class ScanBloc extends Bloc<ScanEvent, ScanState> {
|
||||||
|
final TourRepository repository;
|
||||||
|
Tour? currentTour;
|
||||||
|
String? currentPageId;
|
||||||
|
String? scannedFsaId;
|
||||||
|
|
||||||
|
// Container handling for VS state machine
|
||||||
|
String? containerId;
|
||||||
|
String? containerType; // 'a' or 'b'
|
||||||
|
|
||||||
|
ScanBloc({required this.repository}) : super(ScanInitial()) {
|
||||||
|
on<InitializeScan>(_onInitializeScan);
|
||||||
|
on<ProcessBarcode>(_onProcessBarcode);
|
||||||
|
on<ValidateBarcode>(_onValidateBarcode);
|
||||||
|
on<UpdateObjectState>(_onUpdateObjectState);
|
||||||
|
on<ResetScan>(_onResetScan);
|
||||||
|
on<CreateUnknownObject>(_onCreateUnknownObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onInitializeScan(InitializeScan event, Emitter<ScanState> emit) async {
|
||||||
|
currentTour = event.tour;
|
||||||
|
containerId = null;
|
||||||
|
containerType = null;
|
||||||
|
emit(ScanReady(tour: event.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onProcessBarcode(ProcessBarcode event, Emitter<ScanState> emit) async {
|
||||||
|
emit(ScanProcessing(barcode: event.barcode));
|
||||||
|
|
||||||
|
final barcode = event.barcode.trim();
|
||||||
|
|
||||||
|
// Prüfe auf spezielle Barcodes (Seiten-Codes)
|
||||||
|
if (currentTour != null) {
|
||||||
|
final pageInfo = _findPageForBarcode(barcode);
|
||||||
|
if (pageInfo != null) {
|
||||||
|
emit(ScanPageDetected(
|
||||||
|
pageId: pageInfo['pageId']!,
|
||||||
|
label: pageInfo['label']!,
|
||||||
|
tour: currentTour!,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suche Objekt nach Barcode
|
||||||
|
final result = await repository.getObjectByBarcode(barcode);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => emit(ScanError(message: _mapFailureToMessage(failure))),
|
||||||
|
(object) {
|
||||||
|
if (object != null) {
|
||||||
|
_processScannedObject(object, barcode, emit);
|
||||||
|
} else {
|
||||||
|
// Unbekanntes Objekt - prüfe auf gültiges Präfix
|
||||||
|
final prefix = barcode.length >= 3 ? barcode.substring(0, 3) : '';
|
||||||
|
if (_isValidPrefix(prefix)) {
|
||||||
|
emit(ScanUnknownObject(
|
||||||
|
barcode: barcode,
|
||||||
|
prefix: prefix,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(ScanError(message: 'Unbekannter Barcode: $barcode'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processScannedObject(LogisticObject object, String barcode, Emitter<ScanState> emit) {
|
||||||
|
if (currentTour == null) {
|
||||||
|
emit(const ScanError(message: 'Keine Tour ausgewählt'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tourType = currentTour!.type;
|
||||||
|
|
||||||
|
// Dispatch to appropriate state machine based on tour type
|
||||||
|
switch (tourType) {
|
||||||
|
case TourTypes.stockStart:
|
||||||
|
_stockStartStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.vehStart:
|
||||||
|
_vehStartStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.vehBulk:
|
||||||
|
_vehBulkStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.veh:
|
||||||
|
_vehStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.fsa:
|
||||||
|
_fsaStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.vs:
|
||||||
|
_vsStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.vehVs:
|
||||||
|
_vehVsStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.gi:
|
||||||
|
_giStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.vehEnd:
|
||||||
|
_vehEndStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.stockEnd:
|
||||||
|
_stockEndStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
case TourTypes.stock:
|
||||||
|
_stockStateMachine(object, emit);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Fallback to simple state machine for unknown tour types
|
||||||
|
final nextState = _determineNextState(object);
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: nextState,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: stockStart (Lager Beladung)
|
||||||
|
// ============================================================================
|
||||||
|
void _stockStartStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final currentState = object.state;
|
||||||
|
|
||||||
|
if (currentState == ObjectStates.unknown) {
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.toDelivery,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else if (currentState == ObjectStates.finGITmp) {
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retGI,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(ScanError(
|
||||||
|
message: 'Fehler: Ungültiger Barcode für Lager Beladung',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: vehStart (Fahrzeug Start)
|
||||||
|
// ============================================================================
|
||||||
|
void _vehStartStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final currentState = object.state;
|
||||||
|
|
||||||
|
if (currentState == ObjectStates.toDelivery) {
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.delivery,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else if (currentState == ObjectStates.retGI) {
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retGIFzg,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Ungültiger Barcode'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: vehBulk (Fahrzeug Bulk)
|
||||||
|
// ============================================================================
|
||||||
|
void _vehBulkStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final objectType = object.type;
|
||||||
|
final currentState = object.state;
|
||||||
|
|
||||||
|
// Type 9 = special bulk handling
|
||||||
|
if (objectType == 9) {
|
||||||
|
// Bulk update all objects with state to_delivery
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.delivery,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
if (currentState == ObjectStates.toDelivery) {
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.delivery,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else if (currentState == ObjectStates.retGI) {
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retGIFzg,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Ungültiger Barcode'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: veh (Fahrzeug - Stationen)
|
||||||
|
// ============================================================================
|
||||||
|
void _vehStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final currentState = object.state;
|
||||||
|
|
||||||
|
switch (currentState) {
|
||||||
|
case ObjectStates.delivery:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.station,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retFail:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retFailFzg,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retGI:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retGIFzg,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retDS:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retDSFzg,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.station:
|
||||||
|
// Reverse transition: back to delivery
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.delivery,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.inFA:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.hdl,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: fsa (Fahrscheinautomat)
|
||||||
|
// ============================================================================
|
||||||
|
void _fsaStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final objectType = object.type;
|
||||||
|
final currentState = object.state;
|
||||||
|
final subtype = object.subtype.toLowerCase();
|
||||||
|
|
||||||
|
// GK = Geldkassette (type 1)
|
||||||
|
if (_isTypeGK(objectType, subtype)) {
|
||||||
|
_fsaGKStateMachine(object, currentState, emit);
|
||||||
|
}
|
||||||
|
// HP = Hauptkasse/Druckerpatronen (type 2)
|
||||||
|
else if (_isTypeHP(objectType, subtype)) {
|
||||||
|
_fsaHPStateMachine(object, currentState, emit);
|
||||||
|
}
|
||||||
|
// FR = Fahrkartenrolle (type 5)
|
||||||
|
else if (_isTypeFR(objectType, subtype)) {
|
||||||
|
_fsaFRStateMachine(object, currentState, emit);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Typ für FSA'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fsaGKStateMachine(LogisticObject object, String currentState, Emitter<ScanState> emit) {
|
||||||
|
switch (currentState) {
|
||||||
|
case ObjectStates.station:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.inFA,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.inFA:
|
||||||
|
// Special handling: Fehlkassette logic
|
||||||
|
emit(ScanFehlKassetteDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retFail,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.unknown:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retGI,
|
||||||
|
tour: currentTour,
|
||||||
|
originBarcode: object.code,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fsaHPStateMachine(LogisticObject object, String currentState, Emitter<ScanState> emit) {
|
||||||
|
switch (currentState) {
|
||||||
|
case ObjectStates.station:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.inFA,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.inFA:
|
||||||
|
// Bidirectional: back to station
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.station,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.unknown:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retDS,
|
||||||
|
tour: currentTour,
|
||||||
|
originBarcode: object.code,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fsaFRStateMachine(LogisticObject object, String currentState, Emitter<ScanState> emit) {
|
||||||
|
switch (currentState) {
|
||||||
|
case ObjectStates.station:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.inFA,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.inFA:
|
||||||
|
// Bidirectional: back to station
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.station,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: vs (Versorgungsstelle)
|
||||||
|
// ============================================================================
|
||||||
|
void _vsStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final objectType = object.type;
|
||||||
|
final currentState = object.state;
|
||||||
|
final subtype = object.subtype.toLowerCase();
|
||||||
|
|
||||||
|
// SB = Safebag
|
||||||
|
if (_isTypeSB(objectType, subtype)) {
|
||||||
|
_vsSBStateMachine(object, currentState, emit);
|
||||||
|
}
|
||||||
|
// ABS = Abfallbehälter
|
||||||
|
else if (_isTypeABS(objectType, subtype)) {
|
||||||
|
_vsABSStateMachine(object, currentState, emit);
|
||||||
|
}
|
||||||
|
// CNTR = Container
|
||||||
|
else if (_isTypeCNTR(objectType, subtype)) {
|
||||||
|
_vsCNTRStateMachine(object, currentState, subtype, emit);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _vsSBStateMachine(LogisticObject object, String currentState, Emitter<ScanState> emit) {
|
||||||
|
if (containerId == null) {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Zustand - Container nicht ausgewählt'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerType == 'a') {
|
||||||
|
if (currentState == ObjectStates.inVS) {
|
||||||
|
emit(ScanContainerObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retGI,
|
||||||
|
tour: currentTour,
|
||||||
|
containerId: containerId!,
|
||||||
|
containerType: containerType!,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Zustand'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Zustand'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _vsABSStateMachine(LogisticObject object, String currentState, Emitter<ScanState> emit) {
|
||||||
|
if (containerId == null) {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Zustand - Container nicht ausgewählt'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containerType == 'a') {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Zustand'));
|
||||||
|
} else {
|
||||||
|
if (currentState == ObjectStates.inVS) {
|
||||||
|
emit(ScanContainerObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retDS,
|
||||||
|
tour: currentTour,
|
||||||
|
containerId: containerId!,
|
||||||
|
containerType: containerType!,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Zustand'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _vsCNTRStateMachine(LogisticObject object, String currentState, String subtype, Emitter<ScanState> emit) {
|
||||||
|
containerId = object.code;
|
||||||
|
|
||||||
|
if (subtype == 'cntra') {
|
||||||
|
containerType = 'a';
|
||||||
|
emit(ScanContainerDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retcGI,
|
||||||
|
tour: currentTour,
|
||||||
|
containerId: containerId!,
|
||||||
|
containerType: containerType!,
|
||||||
|
));
|
||||||
|
} else if (subtype == 'cntrb') {
|
||||||
|
containerType = 'b';
|
||||||
|
emit(ScanContainerDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retcDS,
|
||||||
|
tour: currentTour,
|
||||||
|
containerId: containerId!,
|
||||||
|
containerType: containerType!,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Unbekannter Container-Typ'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: vehVs (Fahrzeug VS)
|
||||||
|
// ============================================================================
|
||||||
|
void _vehVsStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final objectType = object.type;
|
||||||
|
final currentState = object.state;
|
||||||
|
final subtype = object.subtype.toLowerCase();
|
||||||
|
|
||||||
|
// SB and ABS not allowed in vehVs
|
||||||
|
if (_isTypeSB(objectType, subtype) || _isTypeABS(objectType, subtype)) {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CNTR = Container
|
||||||
|
if (_isTypeCNTR(objectType, subtype)) {
|
||||||
|
_vehVsCNTRStateMachine(object, currentState, subtype, emit);
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Typ'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _vehVsCNTRStateMachine(LogisticObject object, String currentState, String subtype, Emitter<ScanState> emit) {
|
||||||
|
if (subtype == 'cntra') {
|
||||||
|
if (currentState == ObjectStates.retcGI) {
|
||||||
|
// Update all container objects and clear container
|
||||||
|
emit(ScanContainerCloseDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.unknown,
|
||||||
|
targetStateForObjects: ObjectStates.retGIFzg,
|
||||||
|
tour: currentTour,
|
||||||
|
containerType: 'a',
|
||||||
|
));
|
||||||
|
containerId = null;
|
||||||
|
containerType = null;
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
} else if (subtype == 'cntrb') {
|
||||||
|
if (currentState == ObjectStates.retcDS) {
|
||||||
|
// Update all container objects and clear container
|
||||||
|
emit(ScanContainerCloseDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.unknown,
|
||||||
|
targetStateForObjects: ObjectStates.retDSFzg,
|
||||||
|
tour: currentTour,
|
||||||
|
containerType: 'b',
|
||||||
|
));
|
||||||
|
containerId = null;
|
||||||
|
containerType = null;
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: gi (Geldinstitut)
|
||||||
|
// ============================================================================
|
||||||
|
void _giStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final objectType = object.type;
|
||||||
|
final currentState = object.state;
|
||||||
|
final subtype = object.subtype.toLowerCase();
|
||||||
|
|
||||||
|
// GK = Geldkassette
|
||||||
|
if (_isTypeGK(objectType, subtype)) {
|
||||||
|
_giGKStateMachine(object, currentState, emit);
|
||||||
|
}
|
||||||
|
// SB = Safebag
|
||||||
|
else if (_isTypeSB(objectType, subtype)) {
|
||||||
|
_giSBStateMachine(object, currentState, emit);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Typ'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _giGKStateMachine(LogisticObject object, String currentState, Emitter<ScanState> emit) {
|
||||||
|
switch (currentState) {
|
||||||
|
case ObjectStates.retGIFzg:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.finGI,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.unknown:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retDSEmpty,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.delivery:
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _giSBStateMachine(LogisticObject object, String currentState, Emitter<ScanState> emit) {
|
||||||
|
if (currentState == ObjectStates.retGIFzg) {
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.finGI,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: vehEnd (Fahrzeug Ende)
|
||||||
|
// ============================================================================
|
||||||
|
void _vehEndStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final currentState = object.state;
|
||||||
|
|
||||||
|
switch (currentState) {
|
||||||
|
case ObjectStates.retFailFzg:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retFailStk,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retDSFzg:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retDSStk,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.delivery:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retDSStk,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retDSEmpty:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retDSStk,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retGIFzg:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retGIStk,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.retDSErr,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: stockEnd (Lager Ende)
|
||||||
|
// ============================================================================
|
||||||
|
void _stockEndStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final currentState = object.state;
|
||||||
|
|
||||||
|
switch (currentState) {
|
||||||
|
case ObjectStates.retFailStk:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.finDSFail,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retDSStk:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.finDS,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retDSErr:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.finDSErr,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.retGIStk:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.finGITmp,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Status'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STATE MACHINE: stock (Lager - HADAG)
|
||||||
|
// ============================================================================
|
||||||
|
void _stockStateMachine(LogisticObject object, Emitter<ScanState> emit) {
|
||||||
|
final objectType = object.type;
|
||||||
|
final currentState = object.state;
|
||||||
|
final subtype = object.subtype.toLowerCase();
|
||||||
|
|
||||||
|
// Only HP and GK allowed
|
||||||
|
if (!_isTypeHP(objectType, subtype) && !_isTypeGK(objectType, subtype)) {
|
||||||
|
emit(const ScanError(message: 'Fehler: Falscher Typ'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (currentState) {
|
||||||
|
case ObjectStates.unknown:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.station,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.stkHadag:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.station,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case ObjectStates.station:
|
||||||
|
emit(ScanObjectDetected(
|
||||||
|
object: object,
|
||||||
|
suggestedState: ObjectStates.stkHadag,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
emit(ScanUnknownObject(
|
||||||
|
barcode: object.code,
|
||||||
|
prefix: object.code.length >= 3 ? object.code.substring(0, 3) : '',
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helper methods for type checking
|
||||||
|
// ============================================================================
|
||||||
|
bool _isTypeGK(int type, String subtype) {
|
||||||
|
// GK = Geldkassette (type 1, subtypes meka, mekb, mekc, mekd, beka, bekb, bekc, bekd)
|
||||||
|
return type == 1 || subtype.startsWith('mek') || subtype.startsWith('bek');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTypeHP(int type, String subtype) {
|
||||||
|
// HP = Hauptkasse/Druckerpatronen (type 2, subtypes hp1a, hp1b, etc.)
|
||||||
|
return type == 2 || subtype.startsWith('hp');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTypeFR(int type, String subtype) {
|
||||||
|
// FR = Fahrkartenrolle (type 5, subtype fra)
|
||||||
|
return type == 5 || subtype.startsWith('fr');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTypeSB(int type, String subtype) {
|
||||||
|
// SB = Safebag (type 6)
|
||||||
|
return type == 6 || subtype == 'sb';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTypeABS(int type, String subtype) {
|
||||||
|
// ABS = Abfallbehälter (type 7)
|
||||||
|
return type == 7 || subtype == 'abs';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTypeCNTR(int type, String subtype) {
|
||||||
|
// CNTR = Container (type 8)
|
||||||
|
return type == 8 || subtype.startsWith('cntr');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Legacy simple state machine (fallback)
|
||||||
|
// ============================================================================
|
||||||
|
String _determineNextState(LogisticObject object) {
|
||||||
|
switch (object.state) {
|
||||||
|
case ObjectStates.unknown:
|
||||||
|
return ObjectStates.toDelivery;
|
||||||
|
case ObjectStates.toDelivery:
|
||||||
|
return ObjectStates.delivery;
|
||||||
|
case ObjectStates.delivery:
|
||||||
|
return ObjectStates.station;
|
||||||
|
case ObjectStates.station:
|
||||||
|
return ObjectStates.inFA;
|
||||||
|
case ObjectStates.inFA:
|
||||||
|
return ObjectStates.retGI;
|
||||||
|
case ObjectStates.retGI:
|
||||||
|
return ObjectStates.retGIFzg;
|
||||||
|
case ObjectStates.retGIFzg:
|
||||||
|
return ObjectStates.finGI;
|
||||||
|
case ObjectStates.retFail:
|
||||||
|
return ObjectStates.retFailFzg;
|
||||||
|
case ObjectStates.retFailFzg:
|
||||||
|
return ObjectStates.retFailStk;
|
||||||
|
case ObjectStates.retDS:
|
||||||
|
return ObjectStates.retDSFzg;
|
||||||
|
case ObjectStates.retDSFzg:
|
||||||
|
return ObjectStates.finDS;
|
||||||
|
default:
|
||||||
|
return object.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Event handlers
|
||||||
|
// ============================================================================
|
||||||
|
Future<void> _onValidateBarcode(ValidateBarcode event, Emitter<ScanState> emit) async {
|
||||||
|
if (event.barcode.isEmpty) {
|
||||||
|
emit(const ScanValidationError(message: 'Barcode darf nicht leer sein'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.barcode.length < 6) {
|
||||||
|
emit(const ScanValidationError(message: 'Barcode zu kurz'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(ProcessBarcode(barcode: event.barcode));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onUpdateObjectState(UpdateObjectState event, Emitter<ScanState> emit) async {
|
||||||
|
emit(ScanProcessing(barcode: event.object.code));
|
||||||
|
|
||||||
|
final result = await repository.updateObjectState(
|
||||||
|
event.object.objectId,
|
||||||
|
event.newState,
|
||||||
|
locationId: currentTour?.locationId,
|
||||||
|
refType: currentTour != null ? _getTourTypeCode(currentTour!.type) : null,
|
||||||
|
refId: currentTour?.tourId,
|
||||||
|
containerCode: event.containerCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => emit(ScanError(message: _mapFailureToMessage(failure))),
|
||||||
|
(_) {
|
||||||
|
emit(ScanObjectUpdated(
|
||||||
|
object: event.object.copyWith(state: event.newState),
|
||||||
|
previousState: event.object.state,
|
||||||
|
newState: event.newState,
|
||||||
|
tour: currentTour,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onCreateUnknownObject(CreateUnknownObject event, Emitter<ScanState> emit) async {
|
||||||
|
emit(ScanProcessing(barcode: event.barcode));
|
||||||
|
|
||||||
|
final result = await repository.createObject(
|
||||||
|
type: event.type,
|
||||||
|
code: event.barcode,
|
||||||
|
isManual: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => emit(ScanError(message: _mapFailureToMessage(failure))),
|
||||||
|
(_) {
|
||||||
|
emit(ScanObjectCreated(barcode: event.barcode));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onResetScan(ResetScan event, Emitter<ScanState> emit) {
|
||||||
|
if (currentTour != null) {
|
||||||
|
emit(ScanReady(tour: currentTour!));
|
||||||
|
} else {
|
||||||
|
emit(ScanInitial());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helper methods
|
||||||
|
// ============================================================================
|
||||||
|
Map<String, String>? _findPageForBarcode(String barcode) {
|
||||||
|
if (currentTour == null) return null;
|
||||||
|
|
||||||
|
for (final page in currentTour!.pages) {
|
||||||
|
if (page.code == barcode) {
|
||||||
|
return {
|
||||||
|
'pageId': page.pageId,
|
||||||
|
'label': page.label ?? page.pageId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.pageId.toLowerCase().startsWith('fsa') && page.type.isNotEmpty) {
|
||||||
|
// FSA page detected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isValidPrefix(String prefix) {
|
||||||
|
final validPrefixes = ['MEK', 'BEK', 'HOP', 'H1P', 'H2P', 'H3P', 'FR', 'SB', 'ABS', 'FZG'];
|
||||||
|
return validPrefixes.any((p) => prefix.toUpperCase().startsWith(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _getTourTypeCode(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case TourTypes.stockStart:
|
||||||
|
return 1;
|
||||||
|
case TourTypes.vehStart:
|
||||||
|
return 2;
|
||||||
|
case TourTypes.veh:
|
||||||
|
return 3;
|
||||||
|
case TourTypes.fsa:
|
||||||
|
return 4;
|
||||||
|
case TourTypes.vs:
|
||||||
|
return 5;
|
||||||
|
case TourTypes.gi:
|
||||||
|
return 6;
|
||||||
|
case TourTypes.vehEnd:
|
||||||
|
return 7;
|
||||||
|
case TourTypes.stockEnd:
|
||||||
|
return 8;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _mapFailureToMessage(Failure failure) {
|
||||||
|
return switch (failure) {
|
||||||
|
ServerFailure _ => 'Serverfehler: ${failure.message}',
|
||||||
|
NetworkFailure _ => 'Netzwerkfehler. Bitte überprüfen Sie Ihre Internetverbindung.',
|
||||||
|
NotFoundFailure _ => 'Objekt nicht gefunden',
|
||||||
|
BarcodeFailure _ => 'Barcode-Fehler: ${failure.message}',
|
||||||
|
_ => 'Ein Fehler ist aufgetreten: ${failure.message}',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/lib/presentation/blocs/scan/scan_event.dart
Normal file
69
app/lib/presentation/blocs/scan/scan_event.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
part of 'scan_bloc.dart';
|
||||||
|
|
||||||
|
abstract class ScanEvent extends Equatable {
|
||||||
|
const ScanEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class InitializeScan extends ScanEvent {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const InitializeScan(this.tour);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tour];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProcessBarcode extends ScanEvent {
|
||||||
|
final String barcode;
|
||||||
|
|
||||||
|
const ProcessBarcode({required this.barcode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [barcode];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValidateBarcode extends ScanEvent {
|
||||||
|
final String barcode;
|
||||||
|
|
||||||
|
const ValidateBarcode({required this.barcode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [barcode];
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateObjectState extends ScanEvent {
|
||||||
|
final LogisticObject object;
|
||||||
|
final String newState;
|
||||||
|
final int? locationId;
|
||||||
|
final String? containerCode;
|
||||||
|
|
||||||
|
const UpdateObjectState({
|
||||||
|
required this.object,
|
||||||
|
required this.newState,
|
||||||
|
this.locationId,
|
||||||
|
this.containerCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [object, newState, locationId, containerCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResetScan extends ScanEvent {
|
||||||
|
const ResetScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateUnknownObject extends ScanEvent {
|
||||||
|
final String barcode;
|
||||||
|
final int type;
|
||||||
|
|
||||||
|
const CreateUnknownObject({
|
||||||
|
required this.barcode,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [barcode, type];
|
||||||
|
}
|
||||||
195
app/lib/presentation/blocs/scan/scan_state.dart
Normal file
195
app/lib/presentation/blocs/scan/scan_state.dart
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
part of 'scan_bloc.dart';
|
||||||
|
|
||||||
|
abstract class ScanState extends Equatable {
|
||||||
|
const ScanState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanInitial extends ScanState {}
|
||||||
|
|
||||||
|
class ScanReady extends ScanState {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const ScanReady({required this.tour});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tour];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanProcessing extends ScanState {
|
||||||
|
final String barcode;
|
||||||
|
|
||||||
|
const ScanProcessing({required this.barcode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [barcode];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanObjectDetected extends ScanState {
|
||||||
|
final LogisticObject object;
|
||||||
|
final String suggestedState;
|
||||||
|
final Tour? tour;
|
||||||
|
final String? originBarcode;
|
||||||
|
|
||||||
|
const ScanObjectDetected({
|
||||||
|
required this.object,
|
||||||
|
required this.suggestedState,
|
||||||
|
this.tour,
|
||||||
|
this.originBarcode,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [object, suggestedState, tour, originBarcode];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special state for Fehlkassette (error cassette) detection
|
||||||
|
class ScanFehlKassetteDetected extends ScanState {
|
||||||
|
final LogisticObject object;
|
||||||
|
final String suggestedState;
|
||||||
|
final Tour? tour;
|
||||||
|
|
||||||
|
const ScanFehlKassetteDetected({
|
||||||
|
required this.object,
|
||||||
|
required this.suggestedState,
|
||||||
|
this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [object, suggestedState, tour];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special state for container object detection (VS state machine)
|
||||||
|
class ScanContainerObjectDetected extends ScanState {
|
||||||
|
final LogisticObject object;
|
||||||
|
final String suggestedState;
|
||||||
|
final Tour? tour;
|
||||||
|
final String containerId;
|
||||||
|
final String containerType;
|
||||||
|
|
||||||
|
const ScanContainerObjectDetected({
|
||||||
|
required this.object,
|
||||||
|
required this.suggestedState,
|
||||||
|
this.tour,
|
||||||
|
required this.containerId,
|
||||||
|
required this.containerType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [object, suggestedState, tour, containerId, containerType];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special state for container detection
|
||||||
|
class ScanContainerDetected extends ScanState {
|
||||||
|
final LogisticObject object;
|
||||||
|
final String suggestedState;
|
||||||
|
final Tour? tour;
|
||||||
|
final String containerId;
|
||||||
|
final String containerType;
|
||||||
|
|
||||||
|
const ScanContainerDetected({
|
||||||
|
required this.object,
|
||||||
|
required this.suggestedState,
|
||||||
|
this.tour,
|
||||||
|
required this.containerId,
|
||||||
|
required this.containerType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [object, suggestedState, tour, containerId, containerType];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special state for container close detection (vehVs state machine)
|
||||||
|
class ScanContainerCloseDetected extends ScanState {
|
||||||
|
final LogisticObject object;
|
||||||
|
final String suggestedState;
|
||||||
|
final String targetStateForObjects;
|
||||||
|
final Tour? tour;
|
||||||
|
final String containerType;
|
||||||
|
|
||||||
|
const ScanContainerCloseDetected({
|
||||||
|
required this.object,
|
||||||
|
required this.suggestedState,
|
||||||
|
required this.targetStateForObjects,
|
||||||
|
this.tour,
|
||||||
|
required this.containerType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [object, suggestedState, targetStateForObjects, tour, containerType];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanUnknownObject extends ScanState {
|
||||||
|
final String barcode;
|
||||||
|
final String prefix;
|
||||||
|
final Tour? tour;
|
||||||
|
|
||||||
|
const ScanUnknownObject({
|
||||||
|
required this.barcode,
|
||||||
|
required this.prefix,
|
||||||
|
this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [barcode, prefix, tour];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanPageDetected extends ScanState {
|
||||||
|
final String pageId;
|
||||||
|
final String label;
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const ScanPageDetected({
|
||||||
|
required this.pageId,
|
||||||
|
required this.label,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [pageId, label, tour];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanObjectUpdated extends ScanState {
|
||||||
|
final LogisticObject object;
|
||||||
|
final String previousState;
|
||||||
|
final String newState;
|
||||||
|
final Tour? tour;
|
||||||
|
|
||||||
|
const ScanObjectUpdated({
|
||||||
|
required this.object,
|
||||||
|
required this.previousState,
|
||||||
|
required this.newState,
|
||||||
|
this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [object, previousState, newState, tour];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanObjectCreated extends ScanState {
|
||||||
|
final String barcode;
|
||||||
|
|
||||||
|
const ScanObjectCreated({required this.barcode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [barcode];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanError extends ScanState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const ScanError({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanValidationError extends ScanState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const ScanValidationError({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message];
|
||||||
|
}
|
||||||
124
app/lib/presentation/blocs/tour/tour_bloc.dart
Normal file
124
app/lib/presentation/blocs/tour/tour_bloc.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../../domain/entities/logistic_object.dart';
|
||||||
|
import '../../../domain/repositories/tour_repository.dart';
|
||||||
|
import '../../../core/errors/failures.dart';
|
||||||
|
|
||||||
|
part 'tour_event.dart';
|
||||||
|
part 'tour_state.dart';
|
||||||
|
|
||||||
|
class TourBloc extends Bloc<TourEvent, TourState> {
|
||||||
|
final TourRepository repository;
|
||||||
|
|
||||||
|
TourBloc({required this.repository}) : super(TourInitial()) {
|
||||||
|
on<LoadTours>(_onLoadTours);
|
||||||
|
on<SelectTour>(_onSelectTour);
|
||||||
|
on<CompleteTour>(_onCompleteTour);
|
||||||
|
on<SyncData>(_onSyncData);
|
||||||
|
on<RefreshTours>(_onRefreshTours);
|
||||||
|
on<LoadTourDetails>(_onLoadTourDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadTours(LoadTours event, Emitter<TourState> emit) async {
|
||||||
|
emit(TourLoading());
|
||||||
|
|
||||||
|
final result = await repository.getTours();
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => emit(TourError(message: _mapFailureToMessage(failure))),
|
||||||
|
(tours) {
|
||||||
|
final openTours = tours.where((t) => t.state < 2).toList();
|
||||||
|
final completedTours = tours.where((t) => t.state == 1).toList();
|
||||||
|
|
||||||
|
emit(ToursLoaded(
|
||||||
|
tours: openTours,
|
||||||
|
completedTours: completedTours,
|
||||||
|
allTours: tours,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSelectTour(SelectTour event, Emitter<TourState> emit) async {
|
||||||
|
final currentState = state;
|
||||||
|
if (currentState is ToursLoaded) {
|
||||||
|
emit(currentState.copyWith(selectedTour: event.tour));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onCompleteTour(CompleteTour event, Emitter<TourState> emit) async {
|
||||||
|
final currentState = state;
|
||||||
|
if (currentState is ToursLoaded) {
|
||||||
|
emit(TourLoading());
|
||||||
|
|
||||||
|
final result = await repository.completeTour(event.tourId);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => emit(TourError(message: _mapFailureToMessage(failure))),
|
||||||
|
(_) => add(const LoadTours()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSyncData(SyncData event, Emitter<TourState> emit) async {
|
||||||
|
final currentState = state;
|
||||||
|
if (currentState is ToursLoaded) {
|
||||||
|
emit(currentState.copyWith(isSyncing: true));
|
||||||
|
|
||||||
|
final result = await repository.syncData();
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
emit(currentState.copyWith(isSyncing: false));
|
||||||
|
emit(SyncError(message: _mapFailureToMessage(failure)));
|
||||||
|
},
|
||||||
|
(_) {
|
||||||
|
emit(currentState.copyWith(isSyncing: false));
|
||||||
|
add(const LoadTours());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRefreshTours(RefreshTours event, Emitter<TourState> emit) async {
|
||||||
|
add(const LoadTours());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadTourDetails(LoadTourDetails event, Emitter<TourState> emit) async {
|
||||||
|
final currentState = state;
|
||||||
|
if (currentState is ToursLoaded) {
|
||||||
|
emit(currentState.copyWith(isLoadingDetails: true));
|
||||||
|
|
||||||
|
final objectsResult = await repository.getObjectsByTour(event.tourId);
|
||||||
|
final statsResult = await repository.getTourStatistics(event.tourId);
|
||||||
|
|
||||||
|
objectsResult.fold(
|
||||||
|
(failure) => emit(TourError(message: _mapFailureToMessage(failure))),
|
||||||
|
(objects) {
|
||||||
|
statsResult.fold(
|
||||||
|
(failure) => emit(TourError(message: _mapFailureToMessage(failure))),
|
||||||
|
(statistics) {
|
||||||
|
emit(currentState.copyWith(
|
||||||
|
selectedTourObjects: objects,
|
||||||
|
selectedTourStats: statistics,
|
||||||
|
isLoadingDetails: false,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _mapFailureToMessage(Failure failure) {
|
||||||
|
return switch (failure) {
|
||||||
|
ServerFailure _ => 'Serverfehler: ${failure.message}',
|
||||||
|
NetworkFailure _ => 'Netzwerkfehler. Bitte überprüfen Sie Ihre Internetverbindung.',
|
||||||
|
CacheFailure _ => 'Cachefehler: ${failure.message}',
|
||||||
|
NotFoundFailure _ => 'Daten nicht gefunden',
|
||||||
|
UnauthorizedFailure _ => 'Nicht autorisiert. Bitte melden Sie sich erneut an.',
|
||||||
|
_ => 'Ein unerwarteter Fehler ist aufgetreten',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/lib/presentation/blocs/tour/tour_event.dart
Normal file
56
app/lib/presentation/blocs/tour/tour_event.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
part of 'tour_bloc.dart';
|
||||||
|
|
||||||
|
abstract class TourEvent extends Equatable {
|
||||||
|
const TourEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTours extends TourEvent {
|
||||||
|
const LoadTours();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectTour extends TourEvent {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const SelectTour(this.tour);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tour];
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompleteTour extends TourEvent {
|
||||||
|
final int tourId;
|
||||||
|
|
||||||
|
const CompleteTour(this.tourId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tourId];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncData extends TourEvent {
|
||||||
|
const SyncData();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshTours extends TourEvent {
|
||||||
|
const RefreshTours();
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadTourDetails extends TourEvent {
|
||||||
|
final int tourId;
|
||||||
|
|
||||||
|
const LoadTourDetails(this.tourId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [tourId];
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateShowCompleted extends TourEvent {
|
||||||
|
final bool showCompleted;
|
||||||
|
|
||||||
|
const UpdateShowCompleted(this.showCompleted);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [showCompleted];
|
||||||
|
}
|
||||||
95
app/lib/presentation/blocs/tour/tour_state.dart
Normal file
95
app/lib/presentation/blocs/tour/tour_state.dart
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
part of 'tour_bloc.dart';
|
||||||
|
|
||||||
|
abstract class TourState extends Equatable {
|
||||||
|
const TourState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class TourInitial extends TourState {}
|
||||||
|
|
||||||
|
class TourLoading extends TourState {}
|
||||||
|
|
||||||
|
class ToursLoaded extends TourState {
|
||||||
|
final List<Tour> tours;
|
||||||
|
final List<Tour> completedTours;
|
||||||
|
final List<Tour> allTours;
|
||||||
|
final Tour? selectedTour;
|
||||||
|
final List<LogisticObject>? selectedTourObjects;
|
||||||
|
final TourStatistics? selectedTourStats;
|
||||||
|
final bool isSyncing;
|
||||||
|
final bool isLoadingDetails;
|
||||||
|
final bool showCompleted;
|
||||||
|
|
||||||
|
const ToursLoaded({
|
||||||
|
required this.tours,
|
||||||
|
this.completedTours = const [],
|
||||||
|
required this.allTours,
|
||||||
|
this.selectedTour,
|
||||||
|
this.selectedTourObjects,
|
||||||
|
this.selectedTourStats,
|
||||||
|
this.isSyncing = false,
|
||||||
|
this.isLoadingDetails = false,
|
||||||
|
this.showCompleted = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
ToursLoaded copyWith({
|
||||||
|
List<Tour>? tours,
|
||||||
|
List<Tour>? completedTours,
|
||||||
|
List<Tour>? allTours,
|
||||||
|
Tour? selectedTour,
|
||||||
|
List<LogisticObject>? selectedTourObjects,
|
||||||
|
TourStatistics? selectedTourStats,
|
||||||
|
bool? isSyncing,
|
||||||
|
bool? isLoadingDetails,
|
||||||
|
bool? showCompleted,
|
||||||
|
}) {
|
||||||
|
return ToursLoaded(
|
||||||
|
tours: tours ?? this.tours,
|
||||||
|
completedTours: completedTours ?? this.completedTours,
|
||||||
|
allTours: allTours ?? this.allTours,
|
||||||
|
selectedTour: selectedTour ?? this.selectedTour,
|
||||||
|
selectedTourObjects: selectedTourObjects ?? this.selectedTourObjects,
|
||||||
|
selectedTourStats: selectedTourStats ?? this.selectedTourStats,
|
||||||
|
isSyncing: isSyncing ?? this.isSyncing,
|
||||||
|
isLoadingDetails: isLoadingDetails ?? this.isLoadingDetails,
|
||||||
|
showCompleted: showCompleted ?? this.showCompleted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int get completedCount => completedTours.length;
|
||||||
|
int get totalCount => allTours.length;
|
||||||
|
double get completionPercentage => totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
tours,
|
||||||
|
completedTours,
|
||||||
|
allTours,
|
||||||
|
selectedTour,
|
||||||
|
selectedTourObjects,
|
||||||
|
selectedTourStats,
|
||||||
|
isSyncing,
|
||||||
|
isLoadingDetails,
|
||||||
|
showCompleted,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class TourError extends TourState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const TourError({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncError extends TourState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const SyncError({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message];
|
||||||
|
}
|
||||||
626
app/lib/presentation/pages/scan/scan_page.dart
Normal file
626
app/lib/presentation/pages/scan/scan_page.dart
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import 'scan_result_sheet.dart';
|
||||||
|
|
||||||
|
class ScanPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const ScanPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ScanPage> createState() => _ScanPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin {
|
||||||
|
late MobileScannerController controller;
|
||||||
|
bool isFlashOn = false;
|
||||||
|
bool isManualEntry = false;
|
||||||
|
final TextEditingController barcodeController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
controller = MobileScannerController();
|
||||||
|
|
||||||
|
// Initialize scan bloc with tour
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
barcodeController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black.withValues(alpha: 128),
|
||||||
|
elevation: 0,
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'Station',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_getTypeLabel(widget.tour.type),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
isFlashOn = !isFlashOn;
|
||||||
|
controller.toggleTorch();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
isFlashOn
|
||||||
|
? Icons.flashlight_on
|
||||||
|
: Icons.flashlight_off,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.switchCamera();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.flip_camera_android,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: BlocListener<ScanBloc, ScanState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is ScanObjectDetected) {
|
||||||
|
_showScanResult(context, state);
|
||||||
|
} else if (state is ScanFehlKassetteDetected) {
|
||||||
|
_showFehlKassetteDialog(context, state);
|
||||||
|
} else if (state is ScanContainerObjectDetected) {
|
||||||
|
_showContainerObjectResult(context, state);
|
||||||
|
} else if (state is ScanContainerDetected) {
|
||||||
|
_showContainerDetectedSnackBar(context, state);
|
||||||
|
} else if (state is ScanContainerCloseDetected) {
|
||||||
|
_showContainerCloseDialog(context, state);
|
||||||
|
} else if (state is ScanUnknownObject) {
|
||||||
|
_showUnknownObjectDialog(context, state);
|
||||||
|
} else if (state is ScanPageDetected) {
|
||||||
|
_showPageDetectedSnackBar(context, state);
|
||||||
|
} else if (state is ScanObjectUpdated) {
|
||||||
|
_showSuccessSnackBar(context, state);
|
||||||
|
} else if (state is ScanObjectCreated) {
|
||||||
|
_showObjectCreatedSnackBar(context, state);
|
||||||
|
} else if (state is ScanError) {
|
||||||
|
_showErrorSnackBar(context, state.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
// Camera Preview
|
||||||
|
MobileScanner(
|
||||||
|
controller: controller,
|
||||||
|
onDetect: (capture) {
|
||||||
|
final barcodes = capture.barcodes;
|
||||||
|
if (barcodes.isNotEmpty && barcodes.first.rawValue != null) {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
context.read<ScanBloc>().add(
|
||||||
|
ProcessBarcode(barcode: barcodes.first.rawValue!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scan Overlay
|
||||||
|
CustomPaint(
|
||||||
|
size: Size.infinite,
|
||||||
|
painter: ScanOverlayPainter(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom Controls
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.black.withValues(alpha: 230),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Barcode in den Rahmen halten',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Manual Entry Button
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _showManualEntryDialog,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.keyboard),
|
||||||
|
label: const Text('Manuelle Eingabe'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Loading Overlay
|
||||||
|
BlocBuilder<ScanBloc, ScanState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is ScanProcessing) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.black54,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showScanResult(BuildContext context, ScanObjectDetected state) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (context) => ScanResultSheet(
|
||||||
|
object: state.object,
|
||||||
|
suggestedState: state.suggestedState,
|
||||||
|
tour: state.tour,
|
||||||
|
onConfirm: () {
|
||||||
|
context.read<ScanBloc>().add(UpdateObjectState(
|
||||||
|
object: state.object,
|
||||||
|
newState: state.suggestedState,
|
||||||
|
));
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showUnknownObjectDialog(BuildContext context, ScanUnknownObject state) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Unbekanntes Objekt'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Barcode: ${state.barcode}'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Dieses Objekt ist nicht im System vorhanden. '
|
||||||
|
'Möchten Sie es neu anlegen?',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Abbrechen'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
// TODO: Navigate to create object page
|
||||||
|
},
|
||||||
|
child: const Text('Anlegen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPageDetectedSnackBar(BuildContext context, ScanPageDetected state) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Seite erkannt: ${state.label}'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'OK',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSuccessSnackBar(BuildContext context, ScanObjectUpdated state) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Status aktualisiert: ${state.object.code}'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showFehlKassetteDialog(BuildContext context, ScanFehlKassetteDetected state) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Fehlkassette'),
|
||||||
|
content: Text(
|
||||||
|
'Die Kassette ${state.object.code} wird als Fehlkassette markiert. '
|
||||||
|
'Der Status wird auf "Fehler - zur Dienststelle" geändert.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Abbrechen'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<ScanBloc>().add(UpdateObjectState(
|
||||||
|
object: state.object,
|
||||||
|
newState: state.suggestedState,
|
||||||
|
));
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Bestätigen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showContainerObjectResult(BuildContext context, ScanContainerObjectDetected state) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (context) => ScanResultSheet(
|
||||||
|
object: state.object,
|
||||||
|
suggestedState: state.suggestedState,
|
||||||
|
tour: state.tour,
|
||||||
|
containerInfo: 'Container: ${state.containerId} (${state.containerType == 'a' ? 'Geldinstitut' : 'Dienststelle'})',
|
||||||
|
onConfirm: () {
|
||||||
|
context.read<ScanBloc>().add(UpdateObjectState(
|
||||||
|
object: state.object,
|
||||||
|
newState: state.suggestedState,
|
||||||
|
containerCode: state.containerId,
|
||||||
|
));
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showContainerDetectedSnackBar(BuildContext context, ScanContainerDetected state) {
|
||||||
|
final containerTypeLabel = state.containerType == 'a' ? 'Geldinstitut' : 'Dienststelle';
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Container erkannt: ${state.containerId} ($containerTypeLabel)'),
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'OK',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
context.read<ScanBloc>().add(UpdateObjectState(
|
||||||
|
object: state.object,
|
||||||
|
newState: state.suggestedState,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showContainerCloseDialog(BuildContext context, ScanContainerCloseDetected state) {
|
||||||
|
final containerTypeLabel = state.containerType == 'a' ? 'Geldinstitut' : 'Dienststelle';
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Container schließen'),
|
||||||
|
content: Text(
|
||||||
|
'Container ${state.object.code} ($containerTypeLabel) wird geschlossen. '
|
||||||
|
'Alle enthaltenen Objekte werden auf den entsprechenden Status aktualisiert.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Abbrechen'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<ScanBloc>().add(UpdateObjectState(
|
||||||
|
object: state.object,
|
||||||
|
newState: state.suggestedState,
|
||||||
|
));
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Schließen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showObjectCreatedSnackBar(BuildContext context, ScanObjectCreated state) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Objekt erstellt: ${state.barcode}'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorSnackBar(BuildContext context, String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showManualEntryDialog() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Barcode manuell eingeben'),
|
||||||
|
content: TextField(
|
||||||
|
controller: barcodeController,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Barcode eingeben',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
barcodeController.clear();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text('Abbrechen'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (barcodeController.text.isNotEmpty) {
|
||||||
|
context.read<ScanBloc>().add(
|
||||||
|
ProcessBarcode(barcode: barcodeController.text),
|
||||||
|
);
|
||||||
|
barcodeController.clear();
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Suchen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getTypeLabel(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'stock_start':
|
||||||
|
return 'Lager - Beladung';
|
||||||
|
case 'stock_end':
|
||||||
|
return 'Lager - Rückgabe';
|
||||||
|
case 'start':
|
||||||
|
return 'Dienststelle';
|
||||||
|
case 'st':
|
||||||
|
return 'Haltestelle';
|
||||||
|
case 'hls':
|
||||||
|
return 'Hochbahnstation';
|
||||||
|
case 'fsa':
|
||||||
|
return 'Fahrscheinautomat';
|
||||||
|
case 'vs':
|
||||||
|
return 'Versorgungsstelle';
|
||||||
|
case 'gi':
|
||||||
|
return 'Geldinstitut';
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanOverlayPainter extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.black.withValues(alpha: 128)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final scanAreaSize = size.width * 0.7;
|
||||||
|
final scanAreaLeft = (size.width - scanAreaSize) / 2;
|
||||||
|
final scanAreaTop = (size.height - scanAreaSize) / 2;
|
||||||
|
|
||||||
|
// Draw dark overlay
|
||||||
|
final path = Path()
|
||||||
|
..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||||
|
|
||||||
|
final cutout = Path()
|
||||||
|
..addRRect(RRect.fromRectAndRadius(
|
||||||
|
Rect.fromLTWH(scanAreaLeft, scanAreaTop, scanAreaSize, scanAreaSize),
|
||||||
|
const Radius.circular(20),
|
||||||
|
));
|
||||||
|
|
||||||
|
final overlayPath = Path.combine(
|
||||||
|
PathOperation.difference,
|
||||||
|
path,
|
||||||
|
cutout,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawPath(overlayPath, paint);
|
||||||
|
|
||||||
|
// Draw corner markers
|
||||||
|
final markerPaint = Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 4;
|
||||||
|
|
||||||
|
final cornerLength = scanAreaSize * 0.15;
|
||||||
|
const cornerRadius = 20.0;
|
||||||
|
|
||||||
|
// Top-left corner
|
||||||
|
_drawCorner(
|
||||||
|
canvas,
|
||||||
|
Offset(scanAreaLeft, scanAreaTop),
|
||||||
|
cornerLength,
|
||||||
|
markerPaint,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
cornerRadius,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Top-right corner
|
||||||
|
_drawCorner(
|
||||||
|
canvas,
|
||||||
|
Offset(scanAreaLeft + scanAreaSize, scanAreaTop),
|
||||||
|
cornerLength,
|
||||||
|
markerPaint,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
cornerRadius,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bottom-left corner
|
||||||
|
_drawCorner(
|
||||||
|
canvas,
|
||||||
|
Offset(scanAreaLeft, scanAreaTop + scanAreaSize),
|
||||||
|
cornerLength,
|
||||||
|
markerPaint,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
cornerRadius,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bottom-right corner
|
||||||
|
_drawCorner(
|
||||||
|
canvas,
|
||||||
|
Offset(scanAreaLeft + scanAreaSize, scanAreaTop + scanAreaSize),
|
||||||
|
cornerLength,
|
||||||
|
markerPaint,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
cornerRadius,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawCorner(
|
||||||
|
Canvas canvas,
|
||||||
|
Offset position,
|
||||||
|
double length,
|
||||||
|
Paint paint,
|
||||||
|
bool isLeft,
|
||||||
|
bool isTop,
|
||||||
|
double radius,
|
||||||
|
) {
|
||||||
|
final path = Path();
|
||||||
|
|
||||||
|
if (isLeft && isTop) {
|
||||||
|
path.moveTo(position.dx + length, position.dy);
|
||||||
|
path.lineTo(position.dx + radius, position.dy);
|
||||||
|
path.arcToPoint(
|
||||||
|
Offset(position.dx, position.dy + radius),
|
||||||
|
radius: Radius.circular(radius),
|
||||||
|
clockwise: false,
|
||||||
|
);
|
||||||
|
path.lineTo(position.dx, position.dy + length);
|
||||||
|
} else if (!isLeft && isTop) {
|
||||||
|
path.moveTo(position.dx - length, position.dy);
|
||||||
|
path.lineTo(position.dx - radius, position.dy);
|
||||||
|
path.arcToPoint(
|
||||||
|
Offset(position.dx, position.dy + radius),
|
||||||
|
radius: Radius.circular(radius),
|
||||||
|
);
|
||||||
|
path.lineTo(position.dx, position.dy + length);
|
||||||
|
} else if (isLeft && !isTop) {
|
||||||
|
path.moveTo(position.dx + length, position.dy);
|
||||||
|
path.lineTo(position.dx + radius, position.dy);
|
||||||
|
path.arcToPoint(
|
||||||
|
Offset(position.dx, position.dy - radius),
|
||||||
|
radius: Radius.circular(radius),
|
||||||
|
);
|
||||||
|
path.lineTo(position.dx, position.dy - length);
|
||||||
|
} else {
|
||||||
|
path.moveTo(position.dx - length, position.dy);
|
||||||
|
path.lineTo(position.dx - radius, position.dy);
|
||||||
|
path.arcToPoint(
|
||||||
|
Offset(position.dx, position.dy - radius),
|
||||||
|
radius: Radius.circular(radius),
|
||||||
|
clockwise: false,
|
||||||
|
);
|
||||||
|
path.lineTo(position.dx, position.dy - length);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
317
app/lib/presentation/pages/scan/scan_result_sheet.dart
Normal file
317
app/lib/presentation/pages/scan/scan_result_sheet.dart
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import '../../../domain/entities/logistic_object.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
|
||||||
|
class ScanResultSheet extends StatelessWidget {
|
||||||
|
final LogisticObject object;
|
||||||
|
final String suggestedState;
|
||||||
|
final Tour? tour;
|
||||||
|
final String? containerInfo;
|
||||||
|
final VoidCallback onConfirm;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
|
||||||
|
const ScanResultSheet({
|
||||||
|
super.key,
|
||||||
|
required this.object,
|
||||||
|
required this.suggestedState,
|
||||||
|
this.tour,
|
||||||
|
this.containerInfo,
|
||||||
|
required this.onConfirm,
|
||||||
|
required this.onCancel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final currentStateColor = ObjectStateInfo.getColorForState(object.state);
|
||||||
|
final suggestedStateColor = ObjectStateInfo.getColorForState(suggestedState);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Handle
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Success Icon
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.green.shade400,
|
||||||
|
Colors.green.shade600,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.green.withValues(alpha: 77),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
).animate().scale(duration: 300.ms, curve: Curves.elasticOut),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
'Objekt gefunden',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Object Info Card
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: BorderSide(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Code',
|
||||||
|
object.code,
|
||||||
|
Icons.qr_code,
|
||||||
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Typ',
|
||||||
|
object.typeName ?? object.subtype.toUpperCase(),
|
||||||
|
Icons.inventory_2,
|
||||||
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildStateRow(
|
||||||
|
context,
|
||||||
|
'Aktueller Status',
|
||||||
|
object.state,
|
||||||
|
currentStateColor,
|
||||||
|
),
|
||||||
|
if (containerInfo != null) ...[
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildInfoRow(
|
||||||
|
context,
|
||||||
|
'Container',
|
||||||
|
containerInfo!,
|
||||||
|
Icons.inventory_2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// State Transition
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
suggestedStateColor.withValues(alpha: 26),
|
||||||
|
suggestedStateColor.withValues(alpha: 13),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: suggestedStateColor.withValues(alpha: 77)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Status wird geändert zu:',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: suggestedStateColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
ObjectStateInfo.getDisplayName(suggestedState),
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: suggestedStateColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: onCancel,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
child: const Text('Abbrechen'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onConfirm,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
child: const Text('Bestätigen'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(BuildContext context, String label, String value, IconData icon) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
size: 20,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStateRow(BuildContext context, String label, String state, Color color) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.info,
|
||||||
|
size: 20,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
ObjectStateInfo.getDisplayName(state),
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
252
app/lib/presentation/pages/tour_types/fsa_page.dart
Normal file
252
app/lib/presentation/pages/tour_types/fsa_page.dart
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
/// FSA Page - Fahrscheinautomat
|
||||||
|
/// Entspricht Lua: ShowFsaScreen + CreateLoadingFsaView
|
||||||
|
class FsaPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const FsaPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FsaPage> createState() => _FsaPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FsaPageState extends State<FsaPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.tour.locationName ?? 'Fahrscheinautomat'),
|
||||||
|
const Text(
|
||||||
|
'Objekt-Einbuchung',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
_buildHeader(context),
|
||||||
|
|
||||||
|
// FSA-spezifische Info
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildFsaInfo(context),
|
||||||
|
_buildObjectTypes(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scan Button
|
||||||
|
_buildScanButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: const Color(0xFFA4D4F0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.confirmation_number,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Fahrscheinautomat',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'FSA',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
if (widget.tour.remark != null)
|
||||||
|
Text(
|
||||||
|
widget.tour.remark!,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFsaInfo(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.blue.shade200),
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Gültige Objekte:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'• GK (Geldkassette): station → in_fa → ret_fail\n'
|
||||||
|
'• HP (Hauptkasse): station ↔ in_fa (Wechsel)\n'
|
||||||
|
'• FR (Fahrkartenrolle): station ↔ in_fa',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildObjectTypes(BuildContext context) {
|
||||||
|
final objectTypes = [
|
||||||
|
_ObjectTypeInfo('Geldkassette (GK)', 'MEK, BEK', Icons.money, Colors.green),
|
||||||
|
_ObjectTypeInfo('Hauptkasse (HP)', 'H1, H2, H3', Icons.print, Colors.blue),
|
||||||
|
_ObjectTypeInfo('Fahrkartenrolle (FR)', 'P', Icons.receipt, Colors.orange),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Objekttypen',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...objectTypes.map((type) => _buildObjectTypeCard(context, type)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildObjectTypeCard(BuildContext context, _ObjectTypeInfo type) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: type.color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(type.icon, color: type.color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
type.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
type.subtypes,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanButton(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text('Barcode scannen'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openScanner(BuildContext context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ScanPage(tour: widget.tour),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ObjectTypeInfo {
|
||||||
|
final String name;
|
||||||
|
final String subtypes;
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
_ObjectTypeInfo(this.name, this.subtypes, this.icon, this.color);
|
||||||
|
}
|
||||||
264
app/lib/presentation/pages/tour_types/gi_page.dart
Normal file
264
app/lib/presentation/pages/tour_types/gi_page.dart
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
/// GI Page - Geldinstitut
|
||||||
|
/// Entspricht Lua: ShowGiScreen + CreateLoadingGiView
|
||||||
|
class GiPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const GiPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GiPage> createState() => _GiPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GiPageState extends State<GiPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.tour.locationName ?? 'Geldinstitut'),
|
||||||
|
const Text(
|
||||||
|
'Übergabe',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
_buildHeader(context),
|
||||||
|
|
||||||
|
// GI-spezifische Info
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildGiInfo(context),
|
||||||
|
_buildExpectedObjects(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scan Button
|
||||||
|
_buildScanButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: const Color(0xFFA4D4F0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.account_balance,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Geldinstitut',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'Bank',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGiInfo(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.green.shade200),
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Erwartete Objekte:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'• GK mit Status "ret_gi_fzg" → "fin_gi"\n'
|
||||||
|
'• SB (Safebag) mit Status "ret_gi_fzg" → "fin_gi"\n'
|
||||||
|
'• Leere Kassetten: "unknown" → "ret_ds_empty"',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildExpectedObjects(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Zu übergebende Objekte',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Geldkassetten
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Geldkassetten (GK)',
|
||||||
|
'ret_gi_fzg → fin_gi',
|
||||||
|
Icons.money,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Safebags
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Safebags (SB)',
|
||||||
|
'ret_gi_fzg → fin_gi',
|
||||||
|
Icons.shopping_bag,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Leere Kassetten
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Leere Kassetten',
|
||||||
|
'unknown → ret_ds_empty',
|
||||||
|
Icons.remove_circle_outline,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildObjectStatusCard(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
String transition,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
transition,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanButton(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text('Barcode scannen'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openScanner(BuildContext context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ScanPage(tour: widget.tour),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
274
app/lib/presentation/pages/tour_types/stock_end_page.dart
Normal file
274
app/lib/presentation/pages/tour_types/stock_end_page.dart
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
/// Stock End Page - Lager Rückgabe
|
||||||
|
/// Entspricht Lua: ShowStockEndScreen + CreateLoadingStockEndView
|
||||||
|
class StockEndPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const StockEndPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StockEndPage> createState() => _StockEndPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StockEndPageState extends State<StockEndPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.tour.locationName ?? 'Lager'),
|
||||||
|
const Text(
|
||||||
|
'Rückgabe',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
_buildHeader(context),
|
||||||
|
|
||||||
|
// Rückgabe-Übersicht
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildReturnInfo(context),
|
||||||
|
_buildObjectSummary(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scan Button
|
||||||
|
_buildScanButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: const Color(0xFFA4D4F0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.warehouse,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Lager Rückgabe',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'Hauptlager',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReturnInfo(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.green.shade200),
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Rückgabe-Status:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Abschluss der Tour - Objekte werden finalisiert:\n'
|
||||||
|
'• ret_fail_stk → fin_ds_fail\n'
|
||||||
|
'• ret_ds_stk → fin_ds\n'
|
||||||
|
'• ret_gi_stk → fin_gi_tmp',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildObjectSummary(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Objekte zur Rückgabe',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Fehlkassetten
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Fehlkassetten',
|
||||||
|
'ret_fail_stk → fin_ds_fail',
|
||||||
|
Icons.error,
|
||||||
|
Colors.red,
|
||||||
|
),
|
||||||
|
|
||||||
|
// DS-Normal
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Zur Dienststelle',
|
||||||
|
'ret_ds_stk → fin_ds',
|
||||||
|
Icons.account_balance,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
|
||||||
|
// DS-Fehler
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Fehlerhafte DS',
|
||||||
|
'ret_ds_err → fin_ds_err',
|
||||||
|
Icons.warning,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
|
||||||
|
// GI
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Zum Geldinstitut',
|
||||||
|
'ret_gi_stk → fin_gi_tmp',
|
||||||
|
Icons.account_balance_wallet,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildObjectStatusCard(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
String transition,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
transition,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanButton(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text('Barcode scannen'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openScanner(BuildContext context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ScanPage(tour: widget.tour),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
323
app/lib/presentation/pages/tour_types/stock_start_page.dart
Normal file
323
app/lib/presentation/pages/tour_types/stock_start_page.dart
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../../domain/entities/counter.dart';
|
||||||
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import '../../widgets/counter_grid.dart';
|
||||||
|
import '../../widgets/recent_scans_list.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
/// Stock Start Page - Lager Beladung
|
||||||
|
/// Entspricht Lua: ShowStockStartScreen + CreateLoadingStockStartView
|
||||||
|
class StockStartPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const StockStartPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StockStartPage> createState() => _StockStartPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StockStartPageState extends State<StockStartPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Initialize scan bloc with tour
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.tour.locationName ?? 'Lager'),
|
||||||
|
Text(
|
||||||
|
'Beladung',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Header mit Lager-Icon
|
||||||
|
_buildHeader(context),
|
||||||
|
|
||||||
|
// Zähler-Übersicht (wie in Lua CreateLoadingStockStartView)
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Bestand Fzg (aktueller Bestand im Fahrzeug)
|
||||||
|
_buildCounterSection(
|
||||||
|
context,
|
||||||
|
title: 'Bestand Fzg',
|
||||||
|
counters: const [
|
||||||
|
CounterItem('MEK', 0),
|
||||||
|
CounterItem('BEK', 0),
|
||||||
|
CounterItem('H1', 0),
|
||||||
|
CounterItem('H2', 0),
|
||||||
|
CounterItem('H3', 0),
|
||||||
|
CounterItem('P', 0),
|
||||||
|
],
|
||||||
|
backgroundColor: const Color(0xFFA4D4F0), // Lua-Farbe
|
||||||
|
),
|
||||||
|
|
||||||
|
// Beladezähler (Soll-Zahlen)
|
||||||
|
_buildCounterSection(
|
||||||
|
context,
|
||||||
|
title: 'Beladezähler',
|
||||||
|
counters: const [
|
||||||
|
CounterItem('MEK', 0),
|
||||||
|
CounterItem('BEK', 0),
|
||||||
|
CounterItem('H1', 0),
|
||||||
|
CounterItem('H2', 0),
|
||||||
|
CounterItem('H3', 0),
|
||||||
|
CounterItem('P', 0),
|
||||||
|
],
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
),
|
||||||
|
|
||||||
|
// HADAG
|
||||||
|
_buildCounterSection(
|
||||||
|
context,
|
||||||
|
title: 'HADAG',
|
||||||
|
counters: const [
|
||||||
|
CounterItem('', null),
|
||||||
|
CounterItem('BEK-B', 0),
|
||||||
|
CounterItem('H1-B', 0),
|
||||||
|
CounterItem('H2-B', 0),
|
||||||
|
CounterItem('', null),
|
||||||
|
CounterItem('', null),
|
||||||
|
],
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
),
|
||||||
|
|
||||||
|
// SST (Schnellbahn)
|
||||||
|
_buildCounterSection(
|
||||||
|
context,
|
||||||
|
title: 'SST',
|
||||||
|
counters: const [
|
||||||
|
CounterItem('MEK-SST', 0),
|
||||||
|
CounterItem('BEK-SST', 0),
|
||||||
|
CounterItem('H1-SST', 0),
|
||||||
|
CounterItem('H2-SST', 0),
|
||||||
|
CounterItem('', null),
|
||||||
|
CounterItem('', null),
|
||||||
|
],
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
),
|
||||||
|
|
||||||
|
// CR (CityRail)
|
||||||
|
_buildCounterSection(
|
||||||
|
context,
|
||||||
|
title: 'CR',
|
||||||
|
counters: const [
|
||||||
|
CounterItem('MEK-CR', 0),
|
||||||
|
CounterItem('BEK-CR', 0),
|
||||||
|
CounterItem('', null),
|
||||||
|
CounterItem('', null),
|
||||||
|
CounterItem('', null),
|
||||||
|
CounterItem('', null),
|
||||||
|
],
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Zuletzt gescannte Objekte
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: RecentScansList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scan Button
|
||||||
|
_buildScanButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: const Color(0xFFA4D4F0), // Hellblau wie in Lua
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.warehouse,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Lager Beladung',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'Hauptlager',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCounterSection(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required List<CounterItem> counters,
|
||||||
|
required Color backgroundColor,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Titel
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Zähler-Grid
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Row(
|
||||||
|
children: counters.map((counter) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: counter.value == null ? Colors.transparent : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
counter.label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: counter.value == null ? Colors.transparent : Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (counter.value != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${counter.value}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanButton(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 10),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text(
|
||||||
|
'Barcode scannen',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openScanner(BuildContext context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ScanPage(tour: widget.tour),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hilfsklasse für Zähler-Darstellung
|
||||||
|
class CounterItem {
|
||||||
|
final String label;
|
||||||
|
final int? value;
|
||||||
|
|
||||||
|
const CounterItem(this.label, this.value);
|
||||||
|
}
|
||||||
274
app/lib/presentation/pages/tour_types/veh_end_page.dart
Normal file
274
app/lib/presentation/pages/tour_types/veh_end_page.dart
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
/// Veh End Page - Fahrzeug Rückgabe
|
||||||
|
/// Entspricht Lua: ShowVehEndScreen + CreateLoadingVehEndView
|
||||||
|
class VehEndPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const VehEndPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VehEndPage> createState() => _VehEndPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VehEndPageState extends State<VehEndPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.tour.locationName ?? 'Fahrzeugende'),
|
||||||
|
const Text(
|
||||||
|
'Rückgabe',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
_buildHeader(context),
|
||||||
|
|
||||||
|
// Rückgabe-Übersicht
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildReturnInfo(context),
|
||||||
|
_buildObjectSummary(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scan Button
|
||||||
|
_buildScanButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: const Color(0xFFA4D4F0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.local_shipping,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Fahrzeug Rückgabe',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'Dienststelle',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReturnInfo(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.orange.shade200),
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Rückgabe-Status:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Alle Objekte im Fahrzeug werden zurückgebucht:\n'
|
||||||
|
'• ret_fail_fzg → ret_fail_stk\n'
|
||||||
|
'• ret_ds_fzg → ret_ds_stk\n'
|
||||||
|
'• ret_gi_fzg → ret_gi_stk',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildObjectSummary(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Objekte im Fahrzeug',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Fehlkassetten
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Fehlkassetten',
|
||||||
|
'ret_fail_fzg → ret_fail_stk',
|
||||||
|
Icons.error,
|
||||||
|
Colors.red,
|
||||||
|
),
|
||||||
|
|
||||||
|
// DS-Objekte
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Zur Dienststelle',
|
||||||
|
'ret_ds_fzg → ret_ds_stk',
|
||||||
|
Icons.account_balance,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
|
||||||
|
// GI-Objekte
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Zum Geldinstitut',
|
||||||
|
'ret_gi_fzg → ret_gi_stk',
|
||||||
|
Icons.account_balance_wallet,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Noch im Fahrzeug
|
||||||
|
_buildObjectStatusCard(
|
||||||
|
context,
|
||||||
|
'Noch im Fahrzeug (Rest)',
|
||||||
|
'delivery → ret_ds_stk',
|
||||||
|
Icons.local_shipping,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildObjectStatusCard(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
String transition,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
transition,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanButton(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text('Barcode scannen'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openScanner(BuildContext context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ScanPage(tour: widget.tour),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
256
app/lib/presentation/pages/tour_types/veh_page.dart
Normal file
256
app/lib/presentation/pages/tour_types/veh_page.dart
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
/// Veh Page - Station (Haltestelle)
|
||||||
|
/// Entspricht Lua: ShowVehScreen + CreateLoadingVehView
|
||||||
|
class VehPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const VehPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VehPage> createState() => _VehPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VehPageState extends State<VehPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.tour.locationName ?? 'Station'),
|
||||||
|
const Text(
|
||||||
|
'Objekt-Wechsel',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Header mit Stations-Info
|
||||||
|
_buildHeader(context),
|
||||||
|
|
||||||
|
// Zähler-Bereiche
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Wechselzähler (Leer/Voll)
|
||||||
|
_buildSwapCounters(context),
|
||||||
|
|
||||||
|
// Abholzähler
|
||||||
|
_buildPickupCounters(context),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scan Button
|
||||||
|
_buildScanButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: const Color(0xFFA4D4F0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.train,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'Haltestelle',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.tour.remark != null)
|
||||||
|
Text(
|
||||||
|
widget.tour.remark!,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwapCounters(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.shade100,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Wechselzähler',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Leer/Voll/HADAG Reihen
|
||||||
|
_buildCounterRow('Leer', [0, 0, 0, 0, 0, 0]),
|
||||||
|
const Divider(height: 1),
|
||||||
|
_buildCounterRow('Voll', [0, 0, 0, 0, 0, 0]),
|
||||||
|
const Divider(height: 1),
|
||||||
|
_buildCounterRow('HADAG', [null, 0, 0, 0, null, null]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPickupCounters(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.shade100,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Abholzähler',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildCounterRow('Abholung', [0, 0, 0, 0, 0, 0]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCounterRow(String label, List<int?> values) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: values.map((value) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: value == null ? Colors.transparent : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: value != null
|
||||||
|
? Text(
|
||||||
|
'$value',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanButton(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text('Barcode scannen'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openScanner(BuildContext context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ScanPage(tour: widget.tour),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
233
app/lib/presentation/pages/tour_types/veh_start_page.dart
Normal file
233
app/lib/presentation/pages/tour_types/veh_start_page.dart
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
/// Veh Start Page - Fahrzeug Beladung
|
||||||
|
/// Entspricht Lua: ShowVehStartScreen + CreateLoadingVehStartView
|
||||||
|
class VehStartPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const VehStartPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VehStartPage> createState() => _VehStartPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VehStartPageState extends State<VehStartPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.tour.locationName ?? 'Dienststelle'),
|
||||||
|
const Text(
|
||||||
|
'Fahrzeug Beladung',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
_buildHeader(context),
|
||||||
|
|
||||||
|
// Beladezähler (wie in Lua CreateLoadingVehStartView)
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildCounterSection(
|
||||||
|
context,
|
||||||
|
title: 'Beladezähler',
|
||||||
|
counters: const [
|
||||||
|
CounterItem('MEK', 0),
|
||||||
|
CounterItem('BEK', 0),
|
||||||
|
CounterItem('H1', 0),
|
||||||
|
CounterItem('H2', 0),
|
||||||
|
CounterItem('H3', 0),
|
||||||
|
CounterItem('P', 0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Zuletzt gescannt
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
'Scanne Objekte zum Beladen',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scan Button
|
||||||
|
_buildScanButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: const Color(0xFFA4D4F0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.local_shipping,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Fahrzeug Beladung',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'Dienststelle',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCounterSection(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required List<CounterItem> counters,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Row(
|
||||||
|
children: counters.map((counter) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
counter.label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${counter.value}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanButton(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text('Barcode scannen'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openScanner(BuildContext context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ScanPage(tour: widget.tour),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CounterItem {
|
||||||
|
final String label;
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
const CounterItem(this.label, this.value);
|
||||||
|
}
|
||||||
309
app/lib/presentation/pages/tour_types/vs_page.dart
Normal file
309
app/lib/presentation/pages/tour_types/vs_page.dart
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../blocs/scan/scan_bloc.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
/// VS Page - Verwahrungsstelle
|
||||||
|
/// Entspricht Lua: ShowVsScreen + CreateLoadingVsView
|
||||||
|
/// Spezial: Container-Handling für SB (Safebag) und ABS (Abfallbehälter)
|
||||||
|
class VsPage extends StatefulWidget {
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
const VsPage({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VsPage> createState() => _VsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VsPageState extends State<VsPage> {
|
||||||
|
String? selectedContainerId;
|
||||||
|
String? selectedContainerType; // 'a' = GI, 'b' = DS
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<ScanBloc>().add(InitializeScan(widget.tour));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.tour.locationName ?? 'Verwahrungsstelle'),
|
||||||
|
const Text(
|
||||||
|
'Container-Annahme',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
onPressed: () => _openScanner(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
_buildHeader(context),
|
||||||
|
|
||||||
|
// Container-Auswahl
|
||||||
|
_buildContainerSelection(context),
|
||||||
|
|
||||||
|
// Aktueller Container Status
|
||||||
|
if (selectedContainerId != null)
|
||||||
|
_buildContainerStatus(context),
|
||||||
|
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
|
// Scan Button
|
||||||
|
_buildScanButton(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: const Color(0xFFA4D4F0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.inventory_2,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Verwahrungsstelle',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.tour.locationName ?? 'VS',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContainerSelection(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Container auswählen',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Container A - Geldinstitut
|
||||||
|
_buildContainerOption(
|
||||||
|
context,
|
||||||
|
id: 'CONT_A',
|
||||||
|
type: 'a',
|
||||||
|
title: 'Container A',
|
||||||
|
subtitle: 'Für Geldinstitut (GI)',
|
||||||
|
icon: Icons.account_balance,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Container B - Dienststelle
|
||||||
|
_buildContainerOption(
|
||||||
|
context,
|
||||||
|
id: 'CONT_B',
|
||||||
|
type: 'b',
|
||||||
|
title: 'Container B',
|
||||||
|
subtitle: 'Für Dienststelle (DS)',
|
||||||
|
icon: Icons.business,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContainerOption(
|
||||||
|
BuildContext context, {
|
||||||
|
required String id,
|
||||||
|
required String type,
|
||||||
|
required String title,
|
||||||
|
required String subtitle,
|
||||||
|
required IconData icon,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
final isSelected = selectedContainerId == id;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
selectedContainerId = id;
|
||||||
|
selectedContainerType = type;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? color.withValues(alpha: 20) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? color : Colors.grey.shade300,
|
||||||
|
width: isSelected ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
Icon(Icons.check_circle, color: color),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContainerStatus(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selectedContainerType == 'a'
|
||||||
|
? Colors.blue.shade50
|
||||||
|
: Colors.orange.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: selectedContainerType == 'a'
|
||||||
|
? Colors.blue
|
||||||
|
: Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Ausgewählt: ${selectedContainerType == 'a' ? 'Container A (GI)' : 'Container B (DS)'}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: selectedContainerType == 'a'
|
||||||
|
? Colors.blue
|
||||||
|
: Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Scannen Sie jetzt SB (Safebag) oder ABS (Abfallbehälter)',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanButton(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (selectedContainerId == null)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 8),
|
||||||
|
child: Text(
|
||||||
|
'Bitte zuerst einen Container auswählen',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.orange,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: selectedContainerId != null
|
||||||
|
? () => _openScanner(context)
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text('Barcode scannen'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openScanner(BuildContext context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ScanPage(tour: widget.tour),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
423
app/lib/presentation/pages/tours/dashboard_page.dart
Normal file
423
app/lib/presentation/pages/tours/dashboard_page.dart
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../blocs/tour/tour_bloc.dart';
|
||||||
|
import '../../widgets/loading_indicator.dart';
|
||||||
|
|
||||||
|
class DashboardPage extends StatelessWidget {
|
||||||
|
const DashboardPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: BlocBuilder<TourBloc, TourState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is TourLoading) {
|
||||||
|
return const LoadingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is ToursLoaded) {
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
// App Bar
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Guten Morgen,',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Fahrer',
|
||||||
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.person,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Tour vom ${DateTime.now().day}.${DateTime.now().month}.${DateTime.now().year}',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Quick Stats
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: _buildQuickStats(context, state),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Section Title
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Schnellzugriff',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Navigate to full tours list
|
||||||
|
},
|
||||||
|
child: const Text('Alle anzeigen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Quick Actions Grid
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
sliver: SliverGrid.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
children: [
|
||||||
|
_buildQuickActionCard(
|
||||||
|
context,
|
||||||
|
'Nächste Station',
|
||||||
|
Icons.location_on,
|
||||||
|
Colors.orange,
|
||||||
|
'${state.tours.where((t) => t.state == 0).length} offen',
|
||||||
|
() {},
|
||||||
|
),
|
||||||
|
_buildQuickActionCard(
|
||||||
|
context,
|
||||||
|
'Scan',
|
||||||
|
Icons.qr_code_scanner,
|
||||||
|
Colors.green,
|
||||||
|
'Barcode scannen',
|
||||||
|
() {},
|
||||||
|
),
|
||||||
|
_buildQuickActionCard(
|
||||||
|
context,
|
||||||
|
'Bestand',
|
||||||
|
Icons.warehouse,
|
||||||
|
Colors.blue,
|
||||||
|
'Objekte anzeigen',
|
||||||
|
() {},
|
||||||
|
),
|
||||||
|
_buildQuickActionCard(
|
||||||
|
context,
|
||||||
|
'Sync',
|
||||||
|
Icons.sync,
|
||||||
|
Colors.purple,
|
||||||
|
state.isSyncing ? 'Synchronisiert...' : 'Daten aktualisieren',
|
||||||
|
() {
|
||||||
|
context.read<TourBloc>().add(const SyncData());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Recent Activity
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||||
|
child: Text(
|
||||||
|
'Letzte Aktivitäten',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
return _buildActivityItem(
|
||||||
|
context,
|
||||||
|
'Geldkassette gescannt',
|
||||||
|
'Station: Hauptbahnhof Nord',
|
||||||
|
'10:23 Uhr',
|
||||||
|
Icons.qr_code_scanner,
|
||||||
|
Colors.green,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Center(child: Text('Willkommen bei HHA Logistics'));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQuickStats(BuildContext context, ToursLoaded state) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final completionPercentage = state.completionPercentage;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 204),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 77),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 51),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.route,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Tour-Fortschritt',
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 204),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${state.completedCount} / ${state.totalCount} Stationen',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: completionPercentage / 100,
|
||||||
|
backgroundColor: Colors.white.withValues(alpha: 51),
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
minHeight: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${completionPercentage.toStringAsFixed(0)}% abgeschlossen',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 230),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state.isSyncing)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.white.withValues(alpha: 204),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Sync...',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 230),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQuickActionCard(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
|
String subtitle,
|
||||||
|
VoidCallback onTap,
|
||||||
|
) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
side: BorderSide(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: color,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActivityItem(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
String subtitle,
|
||||||
|
String time,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
|
) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: BorderSide(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
leading: Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: color,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
time,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
327
app/lib/presentation/pages/tours/tours_page.dart
Normal file
327
app/lib/presentation/pages/tours/tours_page.dart
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../domain/entities/tour.dart';
|
||||||
|
import '../../../core/constants/app_constants.dart';
|
||||||
|
import '../../blocs/tour/tour_bloc.dart';
|
||||||
|
import '../../widgets/tour_list_item.dart';
|
||||||
|
import '../../widgets/loading_indicator.dart';
|
||||||
|
import '../../widgets/error_view.dart';
|
||||||
|
import '../tour_types/stock_start_page.dart';
|
||||||
|
import '../tour_types/veh_start_page.dart';
|
||||||
|
import '../tour_types/veh_page.dart';
|
||||||
|
import '../tour_types/fsa_page.dart';
|
||||||
|
import '../tour_types/vs_page.dart';
|
||||||
|
import '../tour_types/gi_page.dart';
|
||||||
|
import '../tour_types/veh_end_page.dart';
|
||||||
|
import '../tour_types/stock_end_page.dart';
|
||||||
|
import '../scan/scan_page.dart';
|
||||||
|
|
||||||
|
class ToursPage extends StatelessWidget {
|
||||||
|
const ToursPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: BlocBuilder<TourBloc, TourState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is TourLoading) {
|
||||||
|
return const LoadingIndicator(message: 'Touren werden geladen...');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is TourError) {
|
||||||
|
return ErrorView(
|
||||||
|
message: state.message,
|
||||||
|
onRetry: () => context.read<TourBloc>().add(const RefreshTours()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is ToursLoaded) {
|
||||||
|
return _ToursListView(state: state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const LoadingIndicator();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToursListView extends StatelessWidget {
|
||||||
|
final ToursLoaded state;
|
||||||
|
|
||||||
|
const _ToursListView({required this.state});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
// Header mit Fortschritt
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: _buildHeader(context),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Offene Touren
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Offene Stationen',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${state.tours.length}',
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final tour = state.tours[index];
|
||||||
|
return TourListItem(
|
||||||
|
tour: tour,
|
||||||
|
onTap: () => _onTourSelected(context, tour),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: state.tours.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Erledigte Touren (falls aktiviert)
|
||||||
|
if (state.showCompleted && state.completedTours.isNotEmpty) ...[
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Erledigte Stationen',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.withValues(alpha: 26),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${state.completedTours.length}',
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: Colors.green,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final tour = state.completedTours[index];
|
||||||
|
return TourListItem(
|
||||||
|
tour: tour,
|
||||||
|
onTap: () => _onTourSelected(context, tour),
|
||||||
|
isCompleted: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: state.completedTours.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(bottom: 100)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final completionPercentage = state.completionPercentage;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
theme.colorScheme.primary.withValues(alpha: 204),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 77),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 51),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.route,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Tagesübersicht',
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 204),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${state.completedCount} / ${state.totalCount} Stationen',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: completionPercentage / 100,
|
||||||
|
backgroundColor: Colors.white.withValues(alpha: 51),
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
minHeight: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${completionPercentage.toStringAsFixed(0)}% abgeschlossen',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 230),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state.isSyncing)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.white.withValues(alpha: 204),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Sync...',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 230),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTourSelected(BuildContext context, Tour tour) {
|
||||||
|
context.read<TourBloc>().add(SelectTour(tour));
|
||||||
|
|
||||||
|
// Navigation zur tour-spezifischen Page
|
||||||
|
final page = _getPageForTourType(tour);
|
||||||
|
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => page),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getPageForTourType(Tour tour) {
|
||||||
|
switch (tour.type) {
|
||||||
|
case TourTypes.stockStart:
|
||||||
|
return StockStartPage(tour: tour);
|
||||||
|
case TourTypes.vehStart:
|
||||||
|
return VehStartPage(tour: tour);
|
||||||
|
case TourTypes.veh:
|
||||||
|
return VehPage(tour: tour);
|
||||||
|
case TourTypes.fsa:
|
||||||
|
return FsaPage(tour: tour);
|
||||||
|
case TourTypes.vs:
|
||||||
|
return VsPage(tour: tour);
|
||||||
|
case TourTypes.gi:
|
||||||
|
return GiPage(tour: tour);
|
||||||
|
case TourTypes.vehEnd:
|
||||||
|
return VehEndPage(tour: tour);
|
||||||
|
case TourTypes.stockEnd:
|
||||||
|
return StockEndPage(tour: tour);
|
||||||
|
case TourTypes.stock:
|
||||||
|
// Stock (HADAG) uses similar UI to stock_start
|
||||||
|
return StockStartPage(tour: tour);
|
||||||
|
default:
|
||||||
|
// Fallback to generic scan page for unknown types
|
||||||
|
return ScanPage(tour: tour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
app/lib/presentation/widgets/counter_grid.dart
Normal file
186
app/lib/presentation/widgets/counter_grid.dart
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../domain/entities/counter.dart';
|
||||||
|
|
||||||
|
/// Zähler-Grid wie in Lua CreateLoadingStockStartView etc.
|
||||||
|
class CounterGrid extends StatelessWidget {
|
||||||
|
final List<CounterGroup> groups;
|
||||||
|
final bool showDifferences;
|
||||||
|
|
||||||
|
const CounterGrid({
|
||||||
|
super.key,
|
||||||
|
required this.groups,
|
||||||
|
this.showDifferences = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: groups.map((group) => _buildGroup(context, group)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGroup(BuildContext context, CounterGroup group) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getGroupColor(group.title),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Titel
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
group.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Zähler
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Row(
|
||||||
|
children: group.counters.map((counter) {
|
||||||
|
return Expanded(
|
||||||
|
child: _buildCounterCell(context, counter),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCounterCell(BuildContext context, ObjectCounter counter) {
|
||||||
|
final color = counter.isOver
|
||||||
|
? Colors.red.shade100
|
||||||
|
: counter.isComplete
|
||||||
|
? Colors.green.shade100
|
||||||
|
: Colors.white;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
counter.label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${counter.currentCount}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: counter.isOver ? Colors.red : Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showDifferences && counter.targetCount > 0) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'${counter.difference > 0 ? "+" : ""}${counter.difference}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: counter.difference == 0
|
||||||
|
? Colors.green
|
||||||
|
: counter.difference > 0
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getGroupColor(String title) {
|
||||||
|
switch (title.toLowerCase()) {
|
||||||
|
case 'bestand fzg':
|
||||||
|
return const Color(0xFFA4D4F0); // Hellblau wie in Lua
|
||||||
|
case 'beladezähler':
|
||||||
|
case 'hadag':
|
||||||
|
case 'sst':
|
||||||
|
case 'cr':
|
||||||
|
return Colors.grey.shade200;
|
||||||
|
default:
|
||||||
|
return Colors.grey.shade200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vereinfachte Zähler-Anzeige für eine Zeile
|
||||||
|
class CounterRow extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final int count;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const CounterRow({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.count,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor ?? Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$count',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
141
app/lib/presentation/widgets/error_view.dart
Normal file
141
app/lib/presentation/widgets/error_view.dart
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ErrorView extends StatelessWidget {
|
||||||
|
final String message;
|
||||||
|
final VoidCallback? onRetry;
|
||||||
|
final String? retryText;
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
|
const ErrorView({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
this.onRetry,
|
||||||
|
this.retryText,
|
||||||
|
this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.error.withValues(alpha: 26),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon ?? Icons.error_outline,
|
||||||
|
size: 48,
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Oops!',
|
||||||
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 179),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
if (onRetry != null) ...[
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: onRetry,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: Text(retryText ?? 'Erneut versuchen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmptyStateView extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String? subtitle;
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback? onAction;
|
||||||
|
final String? actionText;
|
||||||
|
|
||||||
|
const EmptyStateView({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
this.icon = Icons.inbox,
|
||||||
|
this.onAction,
|
||||||
|
this.actionText,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
if (subtitle != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
subtitle!,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 153),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (onAction != null && actionText != null) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: onAction,
|
||||||
|
child: Text(actionText!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
134
app/lib/presentation/widgets/loading_indicator.dart
Normal file
134
app/lib/presentation/widgets/loading_indicator.dart
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LoadingIndicator extends StatelessWidget {
|
||||||
|
final String? message;
|
||||||
|
final bool showAnimation;
|
||||||
|
|
||||||
|
const LoadingIndicator({
|
||||||
|
super.key,
|
||||||
|
this.message,
|
||||||
|
this.showAnimation = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (showAnimation) ...[
|
||||||
|
// Optional: Lottie Animation für Loading
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 3,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
SizedBox(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 3,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (message != null) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
message!,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 179),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SkeletonLoading extends StatelessWidget {
|
||||||
|
final int itemCount;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
const SkeletonLoading({
|
||||||
|
super.key,
|
||||||
|
this.itemCount = 5,
|
||||||
|
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: padding,
|
||||||
|
itemCount: itemCount,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: _SkeletonCard(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SkeletonCard extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
width: 120,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
173
app/lib/presentation/widgets/recent_scans_list.dart
Normal file
173
app/lib/presentation/widgets/recent_scans_list.dart
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../domain/entities/counter.dart';
|
||||||
|
|
||||||
|
/// Zeigt zuletzt gescannte Objekte an
|
||||||
|
/// Entspricht Lua: Die Liste in ShowStockStartScreen etc.
|
||||||
|
class RecentScansList extends StatelessWidget {
|
||||||
|
final List<RecentScan>? scans;
|
||||||
|
|
||||||
|
const RecentScansList({
|
||||||
|
super.key,
|
||||||
|
this.scans,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Demo-Daten falls keine vorhanden
|
||||||
|
final demoScans = scans ?? const [
|
||||||
|
// RecentScan(
|
||||||
|
// objectCode: 'MEKA123456',
|
||||||
|
// objectName: 'MEK A',
|
||||||
|
// state: 'to_delivery',
|
||||||
|
// scanTime: '',
|
||||||
|
// ),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (demoScans.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Noch keine Objekte gescannt',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Zuletzt gescannt',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...demoScans.map((scan) => _buildScanItem(context, scan)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanItem(BuildContext context, RecentScan scan) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Objekt-Icon
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_getIconForObject(scan.objectName),
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Objekt-Info
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
scan.objectName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'#${scan.objectCode}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getColorForState(scan.state),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_getShortStateName(scan.state),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getIconForObject(String objectName) {
|
||||||
|
final name = objectName.toLowerCase();
|
||||||
|
if (name.contains('mek')) return Icons.money;
|
||||||
|
if (name.contains('bek')) return Icons.money_off;
|
||||||
|
if (name.contains('hp')) return Icons.print;
|
||||||
|
if (name.contains('fr')) return Icons.receipt;
|
||||||
|
if (name.contains('sb')) return Icons.shopping_bag;
|
||||||
|
if (name.contains('abs')) return Icons.delete;
|
||||||
|
return Icons.inventory_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getColorForState(String state) {
|
||||||
|
switch (state) {
|
||||||
|
case 'to_delivery':
|
||||||
|
return Colors.grey.shade300;
|
||||||
|
case 'delivery':
|
||||||
|
return Colors.grey.shade400;
|
||||||
|
case 'station':
|
||||||
|
return const Color(0xFFFFDD00);
|
||||||
|
case 'in_fa':
|
||||||
|
return const Color(0xFF9CDA7A);
|
||||||
|
case 'ret_fail':
|
||||||
|
return const Color(0xFFFF9081);
|
||||||
|
default:
|
||||||
|
return Colors.grey.shade200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getShortStateName(String state) {
|
||||||
|
switch (state) {
|
||||||
|
case 'to_delivery':
|
||||||
|
return 'Zum Fzg';
|
||||||
|
case 'delivery':
|
||||||
|
return 'Im Fzg';
|
||||||
|
case 'station':
|
||||||
|
return 'Station';
|
||||||
|
case 'in_fa':
|
||||||
|
return 'Im FA';
|
||||||
|
case 'ret_fail':
|
||||||
|
return 'Fehler';
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
231
app/lib/presentation/widgets/tour_list_item.dart
Normal file
231
app/lib/presentation/widgets/tour_list_item.dart
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import '../../domain/entities/tour.dart';
|
||||||
|
import '../../core/constants/app_constants.dart';
|
||||||
|
|
||||||
|
class TourListItem extends StatelessWidget {
|
||||||
|
final Tour tour;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool isCompleted;
|
||||||
|
|
||||||
|
const TourListItem({
|
||||||
|
super.key,
|
||||||
|
required this.tour,
|
||||||
|
required this.onTap,
|
||||||
|
this.isCompleted = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final iconData = _getIconForTourType(tour.type);
|
||||||
|
final color = _getColorForTourType(tour.type);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
|
child: Card(
|
||||||
|
elevation: isCompleted ? 0 : 2,
|
||||||
|
shadowColor: Colors.black.withValues(alpha: 26),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: isCompleted
|
||||||
|
? BorderSide(color: Colors.grey.shade300)
|
||||||
|
: BorderSide.none,
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: isCompleted ? null : onTap,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Icon Container
|
||||||
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
color,
|
||||||
|
color.withValues(alpha: 204),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: color.withValues(alpha: 77),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
iconData,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
tour.locationName ?? 'Station ${tour.locationId}',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isCompleted ? Colors.grey : null,
|
||||||
|
decoration: isCompleted ? TextDecoration.lineThrough : null,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_getTypeLabel(tour.type),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (tour.remark != null && tour.remark!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
tour.remark!,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status Indicator
|
||||||
|
if (isCompleted)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.withValues(alpha: 26),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).animate().fadeIn(duration: 300.ms).slideX(begin: 0.1, end: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getIconForTourType(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case TourTypes.stockStart:
|
||||||
|
return Icons.warehouse;
|
||||||
|
case TourTypes.stockEnd:
|
||||||
|
return Icons.archive;
|
||||||
|
case TourTypes.start:
|
||||||
|
return Icons.business;
|
||||||
|
case TourTypes.end:
|
||||||
|
return Icons.flag;
|
||||||
|
case TourTypes.station:
|
||||||
|
return Icons.directions_bus;
|
||||||
|
case TourTypes.hls:
|
||||||
|
return Icons.train;
|
||||||
|
case TourTypes.fsa:
|
||||||
|
return Icons.confirmation_number;
|
||||||
|
case TourTypes.vs:
|
||||||
|
return Icons.store;
|
||||||
|
case TourTypes.gi:
|
||||||
|
return Icons.account_balance;
|
||||||
|
case TourTypes.veh:
|
||||||
|
case TourTypes.vehStart:
|
||||||
|
case TourTypes.vehEnd:
|
||||||
|
return Icons.local_shipping;
|
||||||
|
default:
|
||||||
|
return Icons.location_on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getColorForTourType(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case TourTypes.stockStart:
|
||||||
|
case TourTypes.stockEnd:
|
||||||
|
return const Color(0xFF2196F3);
|
||||||
|
case TourTypes.start:
|
||||||
|
case TourTypes.end:
|
||||||
|
return const Color(0xFF4CAF50);
|
||||||
|
case TourTypes.station:
|
||||||
|
return const Color(0xFFFF9800);
|
||||||
|
case TourTypes.hls:
|
||||||
|
return const Color(0xFF9C27B0);
|
||||||
|
case TourTypes.fsa:
|
||||||
|
return const Color(0xFFE91E63);
|
||||||
|
case TourTypes.vs:
|
||||||
|
return const Color(0xFF00BCD4);
|
||||||
|
case TourTypes.gi:
|
||||||
|
return const Color(0xFF3F51B5);
|
||||||
|
case TourTypes.veh:
|
||||||
|
case TourTypes.vehStart:
|
||||||
|
case TourTypes.vehEnd:
|
||||||
|
return const Color(0xFF795548);
|
||||||
|
default:
|
||||||
|
return const Color(0xFF607D8B);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getTypeLabel(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case TourTypes.stockStart:
|
||||||
|
return 'Lager - Beladung';
|
||||||
|
case TourTypes.stockEnd:
|
||||||
|
return 'Lager - Rückgabe';
|
||||||
|
case TourTypes.start:
|
||||||
|
return 'Dienststelle';
|
||||||
|
case TourTypes.end:
|
||||||
|
return 'Tour Ende';
|
||||||
|
case TourTypes.station:
|
||||||
|
return 'Haltestelle';
|
||||||
|
case TourTypes.hls:
|
||||||
|
return 'Hochbahnstation';
|
||||||
|
case TourTypes.fsa:
|
||||||
|
return 'Fahrscheinautomat';
|
||||||
|
case TourTypes.vs:
|
||||||
|
return 'Versorgungsstelle';
|
||||||
|
case TourTypes.gi:
|
||||||
|
return 'Geldinstitut';
|
||||||
|
case TourTypes.veh:
|
||||||
|
return 'Fahrzeug';
|
||||||
|
case TourTypes.vehStart:
|
||||||
|
return 'Fahrzeug - Beladung';
|
||||||
|
case TourTypes.vehEnd:
|
||||||
|
return 'Fahrzeug - Entladung';
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1111
app/pubspec.lock
Normal file
1111
app/pubspec.lock
Normal file
File diff suppressed because it is too large
Load Diff
75
app/pubspec.yaml
Normal file
75
app/pubspec.yaml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
name: hha_logistics
|
||||||
|
version: 1.0.0+1
|
||||||
|
publish_to: none
|
||||||
|
description: HHA Logistics - Moderne Flutter App für die Hamburger Hochbahn
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# State Management
|
||||||
|
flutter_bloc: ^8.1.3
|
||||||
|
equatable: ^2.0.5
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
go_router: ^12.1.1
|
||||||
|
|
||||||
|
# Local Storage
|
||||||
|
sqflite: ^2.3.0
|
||||||
|
path_provider: ^2.1.1
|
||||||
|
shared_preferences: ^2.2.2
|
||||||
|
|
||||||
|
# Networking
|
||||||
|
dio: ^5.4.0
|
||||||
|
connectivity_plus: ^5.0.2
|
||||||
|
|
||||||
|
# Barcode Scanning
|
||||||
|
mobile_scanner: ^3.5.5
|
||||||
|
|
||||||
|
# UI Components
|
||||||
|
shimmer: ^3.0.0
|
||||||
|
flutter_slidable: ^3.0.1
|
||||||
|
badges: ^3.1.2
|
||||||
|
flutter_animate: ^4.3.0
|
||||||
|
lottie: ^2.7.0
|
||||||
|
|
||||||
|
# Icons & Fonts
|
||||||
|
cupertino_icons: ^1.0.6
|
||||||
|
google_fonts: ^6.1.0
|
||||||
|
flutter_svg: ^2.0.9
|
||||||
|
phosphor_flutter: ^2.0.1
|
||||||
|
|
||||||
|
# Utils
|
||||||
|
intl: ^0.20.2
|
||||||
|
uuid: ^4.2.1
|
||||||
|
logger: ^2.0.2
|
||||||
|
freezed_annotation: ^2.4.1
|
||||||
|
json_annotation: ^4.8.1
|
||||||
|
dartz: ^0.10.1
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^3.0.1
|
||||||
|
build_runner: ^2.4.7
|
||||||
|
freezed: ^2.4.5
|
||||||
|
json_serializable: ^6.7.1
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
# assets:
|
||||||
|
# - assets/icons/
|
||||||
|
# - assets/animations/
|
||||||
|
# - assets/images/
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: "launcher_icon"
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/icons/app_icon.png"
|
||||||
|
min_sdk_android: 21
|
||||||
91
app/run_app.sh
Executable file
91
app/run_app.sh
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Flutter App Start-Skript mit sichtbarem Build-Status
|
||||||
|
# Usage: ./run_app.sh [device_id]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Farben für Output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo -e "${BLUE} HHA Logistics - Flutter App Start ${NC}"
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Device ID aus Parameter oder Default
|
||||||
|
DEVICE_ID=${1:-"emulator-5554"}
|
||||||
|
|
||||||
|
# Prüfe ob Flutter installiert ist
|
||||||
|
if ! command -v flutter &> /dev/null; then
|
||||||
|
echo -e "${RED}❌ Flutter wurde nicht gefunden!${NC}"
|
||||||
|
echo "Bitte Flutter installieren: https://flutter.dev/docs/get-started/install"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}📱 Verfügbare Geräte:${NC}"
|
||||||
|
flutter devices
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob gewähltes Gerät existiert
|
||||||
|
echo -e "${BLUE}🔍 Prüfe Gerät: $DEVICE_ID${NC}"
|
||||||
|
if ! flutter devices | grep -q "$DEVICE_ID"; then
|
||||||
|
echo -e "${YELLOW}⚠️ Gerät '$DEVICE_ID' nicht gefunden!${NC}"
|
||||||
|
echo -e "${YELLOW} Versuche verfügbare Geräte zu finden...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Suche nach Android Emulator
|
||||||
|
AVAILABLE_EMULATOR=$(flutter devices | grep "emulator" | head -1 | awk '{print $2}')
|
||||||
|
|
||||||
|
if [ -n "$AVAILABLE_EMULATOR" ]; then
|
||||||
|
echo -e "${GREEN}✅ Emulator gefunden: $AVAILABLE_EMULATOR${NC}"
|
||||||
|
DEVICE_ID=$AVAILABLE_EMULATOR
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Kein Emulator gefunden!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Verfügbare Emulatoren:"
|
||||||
|
flutter emulators
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Starte Emulator...${NC}"
|
||||||
|
flutter emulators --launch Pixel_8_Pro_API_29 2>/dev/null || true
|
||||||
|
sleep 5
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}📦 Installiere Dependencies...${NC}"
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}🔨 Starte Build...${NC}"
|
||||||
|
echo -e "${YELLOW} Das kann einige Minuten dauern beim ersten Mal!${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Zeige Build-Fortschritt mit verbose
|
||||||
|
flutter run -d "$DEVICE_ID" --debug --verbose 2>&1 | while IFS= read -r line; do
|
||||||
|
# Filtere wichtige Build-Schritte
|
||||||
|
if [[ $line == *"Building APK"* ]] || [[ $line == *"Compiling"* ]] || [[ $line == *"Installing"* ]] || [[ $line == *"Launching"* ]]; then
|
||||||
|
echo -e "${GREEN}🔄 $line${NC}"
|
||||||
|
elif [[ $line == *"error"* ]] || [[ $line == *"Error"* ]] || [[ $line == *"FAILED"* ]]; then
|
||||||
|
echo -e "${RED}❌ $line${NC}"
|
||||||
|
elif [[ $line == *"Syncing"* ]] || [[ $line == *"Reloaded"* ]]; then
|
||||||
|
echo -e "${BLUE}💫 $line${NC}"
|
||||||
|
else
|
||||||
|
# Normale Ausgabe nur bei verbose-Modus relevant
|
||||||
|
if [[ $line == *"[+"* ]] || [[ $line == *"lib/"* ]]; then
|
||||||
|
echo "$line"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}========================================${NC}"
|
||||||
|
echo -e "${GREEN} App beendet ${NC}"
|
||||||
|
echo -e "${GREEN}========================================${NC}"
|
||||||
34
backend/.gitignore
vendored
Normal file
34
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
node_modules
|
||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
wrapperVersion=3.3.4
|
||||||
|
distributionType=only-script
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
|
||||||
295
backend/mvnw
vendored
Executable file
295
backend/mvnw
vendored
Executable file
@@ -0,0 +1,295 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
#
|
||||||
|
# Optional ENV vars
|
||||||
|
# -----------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||||
|
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euf
|
||||||
|
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||||
|
|
||||||
|
# OS specific support.
|
||||||
|
native_path() { printf %s\\n "$1"; }
|
||||||
|
case "$(uname)" in
|
||||||
|
CYGWIN* | MINGW*)
|
||||||
|
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||||
|
native_path() { cygpath --path --windows "$1"; }
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# set JAVACMD and JAVACCMD
|
||||||
|
set_java_home() {
|
||||||
|
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||||
|
if [ -n "${JAVA_HOME-}" ]; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||||
|
|
||||||
|
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||||
|
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||||
|
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v java
|
||||||
|
)" || :
|
||||||
|
JAVACCMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v javac
|
||||||
|
)" || :
|
||||||
|
|
||||||
|
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||||
|
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# hash string like Java String::hashCode
|
||||||
|
hash_string() {
|
||||||
|
str="${1:-}" h=0
|
||||||
|
while [ -n "$str" ]; do
|
||||||
|
char="${str%"${str#?}"}"
|
||||||
|
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||||
|
str="${str#?}"
|
||||||
|
done
|
||||||
|
printf %x\\n $h
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose() { :; }
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf %s\\n "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
# MWRAPPER-139:
|
||||||
|
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||||
|
# Needed for removing poorly interpreted newline sequences when running in more
|
||||||
|
# exotic environments such as mingw bash on Windows.
|
||||||
|
printf "%s" "${1}" | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptDir="$(dirname "$0")"
|
||||||
|
scriptName="$(basename "$0")"
|
||||||
|
|
||||||
|
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
case "${key-}" in
|
||||||
|
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||||
|
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||||
|
esac
|
||||||
|
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
|
||||||
|
case "${distributionUrl##*/}" in
|
||||||
|
maven-mvnd-*bin.*)
|
||||||
|
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||||
|
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||||
|
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||||
|
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||||
|
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||||
|
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||||
|
*)
|
||||||
|
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||||
|
distributionPlatform=linux-amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||||
|
;;
|
||||||
|
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||||
|
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||||
|
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||||
|
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||||
|
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||||
|
|
||||||
|
exec_maven() {
|
||||||
|
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||||
|
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -d "$MAVEN_HOME" ]; then
|
||||||
|
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
exec_maven "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${distributionUrl-}" in
|
||||||
|
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||||
|
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||||
|
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||||
|
trap clean HUP INT TERM EXIT
|
||||||
|
else
|
||||||
|
die "cannot create temp dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
verbose "Downloading from: $distributionUrl"
|
||||||
|
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
# select .zip or .tar.gz
|
||||||
|
if ! command -v unzip >/dev/null; then
|
||||||
|
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# verbose opt
|
||||||
|
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||||
|
|
||||||
|
# normalize http auth
|
||||||
|
case "${MVNW_PASSWORD:+has-password}" in
|
||||||
|
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||||
|
verbose "Found wget ... using wget"
|
||||||
|
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||||
|
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||||
|
verbose "Found curl ... using curl"
|
||||||
|
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||||
|
elif set_java_home; then
|
||||||
|
verbose "Falling back to use Java to download"
|
||||||
|
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||||
|
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
cat >"$javaSource" <<-END
|
||||||
|
public class Downloader extends java.net.Authenticator
|
||||||
|
{
|
||||||
|
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||||
|
{
|
||||||
|
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||||
|
}
|
||||||
|
public static void main( String[] args ) throws Exception
|
||||||
|
{
|
||||||
|
setDefault( new Downloader() );
|
||||||
|
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END
|
||||||
|
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||||
|
verbose " - Compiling Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||||
|
verbose " - Running Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
if [ -n "${distributionSha256Sum-}" ]; then
|
||||||
|
distributionSha256Result=false
|
||||||
|
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||||
|
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||||
|
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
elif command -v sha256sum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
elif command -v shasum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||||
|
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $distributionSha256Result = false ]; then
|
||||||
|
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||||
|
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
if command -v unzip >/dev/null; then
|
||||||
|
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||||
|
else
|
||||||
|
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
actualDistributionDir=""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||||
|
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$distributionUrlNameMain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
# enable globbing to iterate over items
|
||||||
|
set +f
|
||||||
|
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$(basename "$dir")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
set -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||||
|
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||||
|
die "Could not find Maven distribution directory in extracted archive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||||
|
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||||
|
|
||||||
|
clean || :
|
||||||
|
exec_maven "$@"
|
||||||
189
backend/mvnw.cmd
vendored
Normal file
189
backend/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<# : batch portion
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
|
@REM distributed with this work for additional information
|
||||||
|
@REM regarding copyright ownership. The ASF licenses this file
|
||||||
|
@REM to you under the Apache License, Version 2.0 (the
|
||||||
|
@REM "License"); you may not use this file except in compliance
|
||||||
|
@REM with the License. You may obtain a copy of the License at
|
||||||
|
@REM
|
||||||
|
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@REM
|
||||||
|
@REM Unless required by applicable law or agreed to in writing,
|
||||||
|
@REM software distributed under the License is distributed on an
|
||||||
|
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
@REM KIND, either express or implied. See the License for the
|
||||||
|
@REM specific language governing permissions and limitations
|
||||||
|
@REM under the License.
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
@REM
|
||||||
|
@REM Optional ENV vars
|
||||||
|
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||||
|
@SET __MVNW_CMD__=
|
||||||
|
@SET __MVNW_ERROR__=
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||||
|
@SET PSModulePath=
|
||||||
|
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||||
|
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||||
|
)
|
||||||
|
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=
|
||||||
|
@SET __MVNW_ARG0_NAME__=
|
||||||
|
@SET MVNW_USERNAME=
|
||||||
|
@SET MVNW_PASSWORD=
|
||||||
|
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||||
|
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||||
|
@GOTO :EOF
|
||||||
|
: end batch / begin powershell #>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if ($env:MVNW_VERBOSE -eq "true") {
|
||||||
|
$VerbosePreference = "Continue"
|
||||||
|
}
|
||||||
|
|
||||||
|
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||||
|
if (!$distributionUrl) {
|
||||||
|
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||||
|
"maven-mvnd-*" {
|
||||||
|
$USE_MVND = $true
|
||||||
|
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||||
|
$MVN_CMD = "mvnd.cmd"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$USE_MVND = $false
|
||||||
|
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
if ($env:MVNW_REPOURL) {
|
||||||
|
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||||
|
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||||
|
}
|
||||||
|
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||||
|
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||||
|
|
||||||
|
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||||
|
if ($env:MAVEN_USER_HOME) {
|
||||||
|
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||||
|
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_WRAPPER_DISTS = $null
|
||||||
|
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||||
|
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||||
|
} else {
|
||||||
|
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||||
|
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||||
|
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||||
|
|
||||||
|
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||||
|
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
exit $?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||||
|
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||||
|
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||||
|
trap {
|
||||||
|
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
Write-Verbose "Downloading from: $distributionUrl"
|
||||||
|
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
$webclient = New-Object System.Net.WebClient
|
||||||
|
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||||
|
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||||
|
}
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||||
|
if ($distributionSha256Sum) {
|
||||||
|
if ($USE_MVND) {
|
||||||
|
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||||
|
}
|
||||||
|
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||||
|
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||||
|
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
$actualDistributionDir = ""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||||
|
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||||
|
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||||
|
$actualDistributionDir = $distributionUrlNameMain
|
||||||
|
}
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||||
|
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||||
|
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||||
|
$actualDistributionDir = $_.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||||
|
try {
|
||||||
|
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||||
|
} catch {
|
||||||
|
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||||
|
Write-Error "fail to move MAVEN_HOME"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
88
backend/pom.xml
Normal file
88
backend/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>4.0.3</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>de.assecutor.hha</groupId>
|
||||||
|
<artifactId>hha-backend</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>HHA Backend</name>
|
||||||
|
<description>Spring Boot and Vaadin backoffice module for the HHA repository.</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<vaadin.version>25.0.7</vaadin.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.vaadin</groupId>
|
||||||
|
<artifactId>vaadin-bom</artifactId>
|
||||||
|
<version>${vaadin.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.vaadin</groupId>
|
||||||
|
<artifactId>vaadin-dev</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.vaadin</groupId>
|
||||||
|
<artifactId>vaadin-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.vaadin</groupId>
|
||||||
|
<artifactId>vaadin-maven-plugin</artifactId>
|
||||||
|
<version>${vaadin.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>build-frontend</id>
|
||||||
|
<goals>
|
||||||
|
<goal>build-frontend</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
1
backend/src/main/frontend/generated/app-shell-imports.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/app-shell-imports.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {}
|
||||||
1
backend/src/main/frontend/generated/css.generated.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/css.generated.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export declare const applyCss: (target: Node) => void;
|
||||||
706
backend/src/main/frontend/generated/flow/Flow.tsx
Normal file
706
backend/src/main/frontend/generated/flow/Flow.tsx
Normal file
@@ -0,0 +1,706 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2000-2026 Vaadin Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
/// <reference lib="es2018" />
|
||||||
|
import { Flow as _Flow } from 'Frontend/generated/jar-resources/Flow.js';
|
||||||
|
import React, { useCallback, useEffect, useReducer, useRef, useState, type ReactNode } from 'react';
|
||||||
|
import { matchRoutes, useBlocker, useLocation, useNavigate, type NavigateOptions, useHref } from 'react-router';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
const flow = new _Flow({
|
||||||
|
imports: () => import('Frontend/generated/flow/generated-flow-imports.js')
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = {
|
||||||
|
render() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const flowReact : { active: boolean } = {
|
||||||
|
active: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClickHandler for vaadin-router-go event is copied from vaadin/router click.js
|
||||||
|
// @ts-ignore
|
||||||
|
function getAnchorOrigin(anchor) {
|
||||||
|
// IE11: on HTTP and HTTPS the default port is not included into
|
||||||
|
// window.location.origin, so won't include it here either.
|
||||||
|
const port = anchor.port;
|
||||||
|
const protocol = anchor.protocol;
|
||||||
|
const defaultHttp = protocol === 'http:' && port === '80';
|
||||||
|
const defaultHttps = protocol === 'https:' && port === '443';
|
||||||
|
const host =
|
||||||
|
defaultHttp || defaultHttps
|
||||||
|
? anchor.hostname // does not include the port number (e.g. www.example.org)
|
||||||
|
: anchor.host; // does include the port number (e.g. www.example.org:80)
|
||||||
|
return `${protocol}//${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeURL(url: URL): void | string {
|
||||||
|
// ignore click if baseURI does not match the document (external)
|
||||||
|
if (!url.href.startsWith(document.baseURI)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize path against baseURI
|
||||||
|
return '/' + url.href.slice(document.baseURI.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractURL(event: MouseEvent): void | URL {
|
||||||
|
// ignore the click if the default action is prevented
|
||||||
|
if (event.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the click if not with the primary mouse button
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the click if a modifier key is pressed
|
||||||
|
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the <a> element that the click is at (or within)
|
||||||
|
let maybeAnchor = event.target;
|
||||||
|
const path = event.composedPath
|
||||||
|
? event.composedPath()
|
||||||
|
: // @ts-ignore
|
||||||
|
event.path || [];
|
||||||
|
|
||||||
|
// example to check: `for...of` loop here throws the "Not yet implemented" error
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
const target = path[i];
|
||||||
|
if (target.nodeName && target.nodeName.toLowerCase() === 'a') {
|
||||||
|
maybeAnchor = target;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
while (maybeAnchor && maybeAnchor.nodeName.toLowerCase() !== 'a') {
|
||||||
|
// @ts-ignore
|
||||||
|
maybeAnchor = maybeAnchor.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the click if not at an <a> element
|
||||||
|
// @ts-ignore
|
||||||
|
if (!maybeAnchor || maybeAnchor.nodeName.toLowerCase() !== 'a') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchor = maybeAnchor as HTMLAnchorElement;
|
||||||
|
|
||||||
|
// ignore the click if the <a> element has a non-default target
|
||||||
|
if (anchor.target && anchor.target.toLowerCase() !== '_self') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the click if the <a> element has the 'download' attribute
|
||||||
|
if (anchor.hasAttribute('download')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the click if the <a> element has the 'router-ignore' attribute
|
||||||
|
if (anchor.hasAttribute('router-ignore')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the click if the target URL is a fragment on the current page
|
||||||
|
if (anchor.pathname === window.location.pathname && anchor.hash !== '') {
|
||||||
|
// @ts-ignore
|
||||||
|
window.location.hash = anchor.hash;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore the click if the target is external to the app
|
||||||
|
// In IE11 HTMLAnchorElement does not have the `origin` property
|
||||||
|
// @ts-ignore
|
||||||
|
const origin = anchor.origin || getAnchorOrigin(anchor);
|
||||||
|
if (origin !== window.location.origin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(anchor.href, anchor.baseURI);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPath(event: MouseEvent): void | string {
|
||||||
|
const url = extractURL(event);
|
||||||
|
if (!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return normalizeURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const registerGlobalClickHandler = () => {
|
||||||
|
window.addEventListener('click', (event: MouseEvent) => {
|
||||||
|
if (flowReact.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = extractURL(event);
|
||||||
|
if (!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ignore click if baseURI does not match the document (external)
|
||||||
|
if (!url.href.startsWith(document.baseURI)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event && event.preventDefault) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize path against baseURI
|
||||||
|
const path = url.pathname + url.search + url.hash;
|
||||||
|
const state = {...window.history.state}
|
||||||
|
if (state.idx !== undefined) {
|
||||||
|
state.idx = state.idx + 1;
|
||||||
|
}
|
||||||
|
window.history.pushState(state, '', path);
|
||||||
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||||
|
}, { capture: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire 'vaadin-navigated' event to inform components of navigation.
|
||||||
|
* @param pathname pathname of navigation
|
||||||
|
* @param search search of navigation
|
||||||
|
*/
|
||||||
|
function fireNavigated(pathname: string, search: string) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('vaadin-navigated', {
|
||||||
|
detail: {
|
||||||
|
pathname,
|
||||||
|
search
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// @ts-ignore
|
||||||
|
delete window.Vaadin.Flow.navigation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function postpone() {}
|
||||||
|
|
||||||
|
const prevent = () => postpone;
|
||||||
|
|
||||||
|
type RouterContainer = Awaited<ReturnType<(typeof flow.serverSideRoutes)[0]['action']>>;
|
||||||
|
|
||||||
|
type PortalEntry = {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
readonly domNode: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FlowPortalProps = React.PropsWithChildren<
|
||||||
|
Readonly<{
|
||||||
|
domNode: HTMLElement;
|
||||||
|
onRemove(): void;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
|
||||||
|
function FlowPortal({ children, domNode, onRemove }: FlowPortalProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
domNode.addEventListener(
|
||||||
|
'flow-portal-remove',
|
||||||
|
(event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onRemove();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return createPortal(children, domNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ADD_FLOW_PORTAL = 'ADD_FLOW_PORTAL';
|
||||||
|
|
||||||
|
type AddFlowPortalAction = Readonly<{
|
||||||
|
type: typeof ADD_FLOW_PORTAL;
|
||||||
|
portal: React.ReactElement<FlowPortalProps>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function addFlowPortal(portal: React.ReactElement<FlowPortalProps>): AddFlowPortalAction {
|
||||||
|
return {
|
||||||
|
type: ADD_FLOW_PORTAL,
|
||||||
|
portal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const REMOVE_FLOW_PORTAL = 'REMOVE_FLOW_PORTAL';
|
||||||
|
|
||||||
|
type RemoveFlowPortalAction = Readonly<{
|
||||||
|
type: typeof REMOVE_FLOW_PORTAL;
|
||||||
|
key: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function removeFlowPortal(key: string): RemoveFlowPortalAction {
|
||||||
|
return {
|
||||||
|
type: REMOVE_FLOW_PORTAL,
|
||||||
|
key
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function flowPortalsReducer(
|
||||||
|
portals: readonly React.ReactElement<FlowPortalProps>[],
|
||||||
|
action: AddFlowPortalAction | RemoveFlowPortalAction
|
||||||
|
) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_FLOW_PORTAL:
|
||||||
|
return [...portals, action.portal];
|
||||||
|
case REMOVE_FLOW_PORTAL:
|
||||||
|
return portals.filter(({ key }) => key !== action.key);
|
||||||
|
default:
|
||||||
|
return portals;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigateOpts = {
|
||||||
|
to: string;
|
||||||
|
callback: boolean;
|
||||||
|
opts?: NavigateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavigateFn = (to: string, callback: boolean, opts?: NavigateOptions) => void;
|
||||||
|
|
||||||
|
let navigateInProgress = false;
|
||||||
|
/**
|
||||||
|
* A hook providing the `navigate(path: string, opts?: NavigateOptions)` function
|
||||||
|
* with React Router API that has more consistent history updates. Uses internal
|
||||||
|
* queue for processing navigate calls.
|
||||||
|
*/
|
||||||
|
function useQueuedNavigate(
|
||||||
|
waitReference: React.MutableRefObject<Promise<void> | undefined>,
|
||||||
|
navigated: React.MutableRefObject<boolean>
|
||||||
|
): NavigateFn {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const navigateQueue = useRef<NavigateOpts[]>([]).current;
|
||||||
|
const [navigateQueueLength, setNavigateQueueLength] = useState(0);
|
||||||
|
|
||||||
|
const dequeueNavigation = useCallback(() => {
|
||||||
|
if (navigateInProgress) {
|
||||||
|
dequeueNavigationAfterCurrentTask();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateArgs = navigateQueue.shift();
|
||||||
|
if (navigateArgs === undefined) {
|
||||||
|
// Empty queue, do nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockingNavigate = async () => {
|
||||||
|
if (waitReference.current) {
|
||||||
|
await waitReference.current;
|
||||||
|
waitReference.current = undefined;
|
||||||
|
}
|
||||||
|
navigated.current = !navigateArgs.callback;
|
||||||
|
navigateInProgress = true;
|
||||||
|
navigate(navigateArgs.to, navigateArgs.opts);
|
||||||
|
setNavigateQueueLength(navigateQueue.length);
|
||||||
|
};
|
||||||
|
blockingNavigate();
|
||||||
|
}, [navigate, setNavigateQueueLength]);
|
||||||
|
|
||||||
|
const dequeueNavigationAfterCurrentTask = useCallback(() => {
|
||||||
|
setTimeout(dequeueNavigation, 0);
|
||||||
|
}, [dequeueNavigation]);
|
||||||
|
|
||||||
|
const enqueueNavigation = useCallback(
|
||||||
|
(to: string, callback: boolean, opts?: NavigateOptions) => {
|
||||||
|
navigateQueue.push({ to: to, callback: callback, opts: opts });
|
||||||
|
setNavigateQueueLength(navigateQueue.length);
|
||||||
|
if (navigateQueue.length === 1) {
|
||||||
|
// The first navigation can be started right after any pending sync
|
||||||
|
// jobs, which could add more navigations to the queue.
|
||||||
|
dequeueNavigationAfterCurrentTask();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setNavigateQueueLength, dequeueNavigationAfterCurrentTask]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
// The Flow component has rendered, but history might not be
|
||||||
|
// updated yet, as React Router does it asynchronously.
|
||||||
|
// Use microtask callback for history consistency.
|
||||||
|
dequeueNavigationAfterCurrentTask();
|
||||||
|
},
|
||||||
|
[navigateQueueLength, dequeueNavigationAfterCurrentTask]
|
||||||
|
);
|
||||||
|
|
||||||
|
return enqueueNavigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowNavigation = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.Vaadin.Flow.navigation = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Flow() {
|
||||||
|
const ref = useRef<HTMLOutputElement>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
|
||||||
|
navigated.current =
|
||||||
|
navigated.current ||
|
||||||
|
(nextLocation.pathname === currentLocation.pathname &&
|
||||||
|
nextLocation.search === currentLocation.search &&
|
||||||
|
nextLocation.hash === currentLocation.hash);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const location = useLocation();
|
||||||
|
const navigated = useRef<boolean>(false);
|
||||||
|
const blockerHandled = useRef<boolean>(false);
|
||||||
|
const fromAnchor = useRef<boolean>(false);
|
||||||
|
const containerRef = useRef<RouterContainer | undefined>(undefined);
|
||||||
|
const roundTrip = useRef<Promise<void> | undefined>(undefined);
|
||||||
|
const queuedNavigate = useQueuedNavigate(roundTrip, navigated);
|
||||||
|
const basename = useHref('/');
|
||||||
|
|
||||||
|
// portalsReducer function is used as state outside the Flow component.
|
||||||
|
const [portals, dispatchPortalAction] = useReducer(flowPortalsReducer, []);
|
||||||
|
|
||||||
|
const addPortalEventHandler = useCallback(
|
||||||
|
(event: CustomEvent<PortalEntry>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const key = Math.random().toString(36).slice(2);
|
||||||
|
dispatchPortalAction(
|
||||||
|
addFlowPortal(
|
||||||
|
<FlowPortal
|
||||||
|
key={key}
|
||||||
|
domNode={event.detail.domNode}
|
||||||
|
onRemove={() => dispatchPortalAction(removeFlowPortal(key))}
|
||||||
|
>
|
||||||
|
{event.detail.children}
|
||||||
|
</FlowPortal>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatchPortalAction]
|
||||||
|
);
|
||||||
|
|
||||||
|
const navigateEventHandler = useCallback(
|
||||||
|
(event: MouseEvent) => {
|
||||||
|
const path = extractPath(event);
|
||||||
|
if (!path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event && event.preventDefault) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
navigated.current = false;
|
||||||
|
// When navigation is triggered by click on a link, fromAnchor is set to true
|
||||||
|
// in order to get a server round-trip even when navigating to the same URL again
|
||||||
|
fromAnchor.current = true;
|
||||||
|
// @ts-ignore
|
||||||
|
window.Vaadin.Flow.navigation = true;
|
||||||
|
navigate(path);
|
||||||
|
// Dispatch close event for overlay drawer on click navigation.
|
||||||
|
window.dispatchEvent(new CustomEvent('close-overlay-drawer'));
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const vaadinRouterGoEventHandler = useCallback(
|
||||||
|
(event: CustomEvent<URL>) => {
|
||||||
|
const url = event.detail;
|
||||||
|
const path = normalizeURL(url);
|
||||||
|
if (!path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(path);
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const vaadinNavigateEventHandler = useCallback(
|
||||||
|
(event: CustomEvent<{ state: unknown; url: string; replace?: boolean; callback: boolean }>) => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.Vaadin.Flow.navigation = true;
|
||||||
|
// clean base uri away if for instance redirected to http://localhost/path/user?id=10
|
||||||
|
// else the whole http... will be appended to the url see #19580
|
||||||
|
const path = event.detail.url.startsWith(document.baseURI)
|
||||||
|
? '/' + event.detail.url.slice(document.baseURI.length)
|
||||||
|
: '/' + event.detail.url;
|
||||||
|
fromAnchor.current = false;
|
||||||
|
queuedNavigate(path, event.detail.callback, { state: event.detail.state, replace: event.detail.replace });
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const redirect = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
return () => {
|
||||||
|
navigate(path, { replace: true });
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.addEventListener('vaadin-router-go', vaadinRouterGoEventHandler);
|
||||||
|
// @ts-ignore
|
||||||
|
window.addEventListener('vaadin-navigate', vaadinNavigateEventHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.removeEventListener('vaadin-router-go', vaadinRouterGoEventHandler);
|
||||||
|
// @ts-ignore
|
||||||
|
window.removeEventListener('vaadin-navigate', vaadinNavigateEventHandler);
|
||||||
|
};
|
||||||
|
}, [vaadinRouterGoEventHandler, vaadinNavigateEventHandler]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.addEventListener("popstate", flowNavigation);
|
||||||
|
window.addEventListener('click', navigateEventHandler);
|
||||||
|
flowReact.active = true;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
containerRef.current?.parentNode?.removeChild(containerRef.current);
|
||||||
|
containerRef.current?.removeEventListener('flow-portal-add', addPortalEventHandler as EventListener);
|
||||||
|
containerRef.current = undefined;
|
||||||
|
// @ts-ignore
|
||||||
|
window.removeEventListener("popstate", flowNavigation);
|
||||||
|
window.removeEventListener('click', navigateEventHandler);
|
||||||
|
flowReact.active = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (blocker.state === 'blocked') {
|
||||||
|
if (blockerHandled.current) {
|
||||||
|
// Blocker is handled and the new navigation
|
||||||
|
// gets queued to be executed after the current handling ends.
|
||||||
|
const { pathname, state } = blocker.location;
|
||||||
|
// Clear base name to not get /baseName/basename/path
|
||||||
|
const pathNoBase = pathname.substring(basename.length);
|
||||||
|
// path should always start with / else react-router will append to current url
|
||||||
|
queuedNavigate(pathNoBase.startsWith('/') ? pathNoBase : '/' + pathNoBase, true, {
|
||||||
|
state: state,
|
||||||
|
replace: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
blockerHandled.current = true;
|
||||||
|
let blockingPromise: any;
|
||||||
|
roundTrip.current = new Promise<void>(
|
||||||
|
(resolve, reject) => (blockingPromise = { resolve: resolve, reject: reject })
|
||||||
|
);
|
||||||
|
// Release blocker handling after promise is fulfilled
|
||||||
|
roundTrip.current.then(
|
||||||
|
() => (blockerHandled.current = false),
|
||||||
|
() => (blockerHandled.current = false)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Proceed to the blocked location, unless the navigation originates from a click on a link.
|
||||||
|
// In that case continue with function execution and perform a server round-trip
|
||||||
|
if (navigated.current && !fromAnchor.current) {
|
||||||
|
blocker.proceed();
|
||||||
|
blockingPromise.resolve();
|
||||||
|
navigateInProgress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fromAnchor.current = false;
|
||||||
|
const { pathname, search } = blocker.location;
|
||||||
|
const routes = ((window as any)?.Vaadin?.routesConfig || []) as any[];
|
||||||
|
let matched = matchRoutes(Array.from(routes), pathname);
|
||||||
|
|
||||||
|
// Navigation between server routes
|
||||||
|
// @ts-ignore
|
||||||
|
if (matched && matched.filter((path) => path.route?.element?.type?.name === Flow.name).length != 0) {
|
||||||
|
containerRef.current?.onBeforeEnter?.call(
|
||||||
|
containerRef?.current,
|
||||||
|
{ pathname, search },
|
||||||
|
{
|
||||||
|
prevent() {
|
||||||
|
blocker.reset();
|
||||||
|
blockingPromise.resolve();
|
||||||
|
navigateInProgress = false;
|
||||||
|
navigated.current = false;
|
||||||
|
},
|
||||||
|
redirect,
|
||||||
|
continue() {
|
||||||
|
blocker.proceed();
|
||||||
|
blockingPromise.resolve();
|
||||||
|
navigateInProgress = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
router
|
||||||
|
);
|
||||||
|
navigated.current = true;
|
||||||
|
} else {
|
||||||
|
// For covering the 'server -> client' use case
|
||||||
|
Promise.resolve(
|
||||||
|
containerRef.current?.onBeforeLeave?.call(
|
||||||
|
containerRef?.current,
|
||||||
|
{
|
||||||
|
pathname,
|
||||||
|
search
|
||||||
|
},
|
||||||
|
{ prevent },
|
||||||
|
router
|
||||||
|
)
|
||||||
|
).then((cmd: unknown) => {
|
||||||
|
if (cmd === postpone && containerRef.current) {
|
||||||
|
// postponed navigation: expose existing blocker to Flow
|
||||||
|
containerRef.current.serverConnected = (cancel) => {
|
||||||
|
if (cancel) {
|
||||||
|
blocker.reset();
|
||||||
|
} else {
|
||||||
|
blocker.proceed();
|
||||||
|
}
|
||||||
|
blockingPromise.resolve();
|
||||||
|
navigateInProgress = false;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// permitted navigation: proceed with the blocker
|
||||||
|
blocker.proceed();
|
||||||
|
blockingPromise.resolve();
|
||||||
|
navigateInProgress = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [blocker.state, blocker.location]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (blocker.state === 'blocked') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (navigated.current) {
|
||||||
|
navigated.current = false;
|
||||||
|
fireNavigated(location.pathname, location.search);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flow.serverSideRoutes[0]
|
||||||
|
.action({ pathname: location.pathname, search: location.search })
|
||||||
|
.then((container) => {
|
||||||
|
const outlet = ref.current?.parentNode;
|
||||||
|
if (outlet && outlet !== container.parentNode) {
|
||||||
|
outlet.append(container);
|
||||||
|
container.addEventListener('flow-portal-add', addPortalEventHandler as EventListener);
|
||||||
|
containerRef.current = container;
|
||||||
|
}
|
||||||
|
return container.onBeforeEnter?.call(
|
||||||
|
container,
|
||||||
|
{ pathname: location.pathname, search: location.search },
|
||||||
|
{
|
||||||
|
prevent,
|
||||||
|
redirect,
|
||||||
|
continue() {
|
||||||
|
fireNavigated(location.pathname, location.search);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
router
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then((result: unknown) => {
|
||||||
|
if (typeof result === 'function') {
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<output ref={ref} style={{ display: 'none' }} />
|
||||||
|
{portals}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Flow.type = 'FlowContainer'; // This is for copilot to recognize this
|
||||||
|
|
||||||
|
export const serverSideRoutes = [{ path: '/*', element: <Flow /> }];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the script for an exported WebComponent with the given tag
|
||||||
|
*
|
||||||
|
* @param tag name of the exported web-component to load
|
||||||
|
*
|
||||||
|
* @returns Promise(resolve, reject) that is fulfilled on script load
|
||||||
|
*/
|
||||||
|
export const loadComponentScript = (tag: String): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = `/web-component/${tag}.js`;
|
||||||
|
script.onload = function () {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
script.onerror = function (err) {
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.head.removeChild(script);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Properties {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load WebComponent script and create a React element for the WebComponent.
|
||||||
|
*
|
||||||
|
* @param tag custom web-component tag name.
|
||||||
|
* @param props optional Properties object to create element attributes with
|
||||||
|
* @param onload optional callback to be called for script onload
|
||||||
|
* @param onerror optional callback for error loading the script
|
||||||
|
*/
|
||||||
|
export const reactElement = (tag: string, props?: Properties, onload?: () => void, onerror?: (err: any) => void) => {
|
||||||
|
loadComponentScript(tag).then(
|
||||||
|
() => onload?.(),
|
||||||
|
(err) => {
|
||||||
|
if (onerror) {
|
||||||
|
onerror(err);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to load script for ${tag}.`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (props) {
|
||||||
|
return React.createElement(tag, props);
|
||||||
|
}
|
||||||
|
return React.createElement(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Flow;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (import.meta.hot) {
|
||||||
|
// @ts-ignore
|
||||||
|
import.meta.hot.accept((newModule) => {
|
||||||
|
// A hot module replace for Flow.tsx happens when any JS/TS imported through @JsModule
|
||||||
|
// or similar is updated because this updates generated-flow-imports.js and that in turn
|
||||||
|
// is imported by this file. We have no means of hot replacing those files, e.g. some
|
||||||
|
// custom lit element so we need to reload the page. */
|
||||||
|
if (newModule) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
329
backend/src/main/frontend/generated/flow/ReactAdapter.tsx
Normal file
329
backend/src/main/frontend/generated/flow/ReactAdapter.tsx
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2000-2026 Vaadin Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
import { createRoot, Root } from 'react-dom/client';
|
||||||
|
import { createElement, type Dispatch, type ReactElement, type ReactNode, useEffect, useReducer } from 'react';
|
||||||
|
|
||||||
|
type FlowStateKeyChangedAction<K extends string, V> = Readonly<{
|
||||||
|
type: 'stateKeyChanged';
|
||||||
|
key: K;
|
||||||
|
value: V;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type FlowStateReducerAction = FlowStateKeyChangedAction<string, unknown>;
|
||||||
|
|
||||||
|
function stateReducer<S extends Readonly<Record<string, unknown>>>(state: S, action: FlowStateReducerAction): S {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'stateKeyChanged':
|
||||||
|
const { value } = action;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
key: value
|
||||||
|
} as S;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispatchEvent<T> = T extends undefined ? () => boolean : (value: T) => boolean;
|
||||||
|
|
||||||
|
const emptyAction: Dispatch<unknown> = () => {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object with APIs exposed for using in the {@link ReactAdapterElement#render}
|
||||||
|
* implementation.
|
||||||
|
*/
|
||||||
|
export type RenderHooks = {
|
||||||
|
/**
|
||||||
|
* A hook API for using stateful JS properties of the Web Component from
|
||||||
|
* the React `render()`.
|
||||||
|
*
|
||||||
|
* @typeParam T - Type of the state value
|
||||||
|
*
|
||||||
|
* @param key - Web Component property name, which is used for two-way
|
||||||
|
* value propagation from the server and back.
|
||||||
|
* @param initialValue - Fallback initial value (optional). Only applies if
|
||||||
|
* the Java component constructor does not invoke `setState`.
|
||||||
|
* @returns A tuple with two values:
|
||||||
|
* 1. The current state.
|
||||||
|
* 2. The `set` function for changing the state and triggering render
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
readonly useState: ReactAdapterElement['useState'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook helper to simplify dispatching a `CustomEvent` on the Web
|
||||||
|
* Component from React.
|
||||||
|
*
|
||||||
|
* @typeParam T - The type for `event.detail` value (optional).
|
||||||
|
*
|
||||||
|
* @param type - The `CustomEvent` type string.
|
||||||
|
* @param options - The settings for the `CustomEvent`.
|
||||||
|
* @returns The `dispatch` function. The function parameters change
|
||||||
|
* depending on the `T` generic type:
|
||||||
|
* - For `undefined` type (default), has no parameters.
|
||||||
|
* - For other types, has one parameter for the `event.detail` value of that type.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
readonly useCustomEvent: ReactAdapterElement['useCustomEvent'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook helper to generate the content element with name attribute to bind
|
||||||
|
* the server-side Flow element for this component.
|
||||||
|
*
|
||||||
|
* This is used together with {@link ReactAdapterComponent::getContentElement}
|
||||||
|
* to have server-side component attach to the correct client element.
|
||||||
|
*
|
||||||
|
* Usage as follows:
|
||||||
|
*
|
||||||
|
* const content = hooks.useContent('content');
|
||||||
|
* return <>
|
||||||
|
* {content}
|
||||||
|
* </>;
|
||||||
|
*
|
||||||
|
* Note! Not adding the 'content' element into the dom will have the
|
||||||
|
* server throw a IllegalStateException for element with tag name not found.
|
||||||
|
*
|
||||||
|
* @param name - The name attribute of the element
|
||||||
|
*/
|
||||||
|
readonly useContent: ReactAdapterElement['useContent'];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ReadyCallbackFunction {
|
||||||
|
(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class for Web Components that render using React. Enables creating
|
||||||
|
* adapters for integrating React components with Flow. Intended for use with
|
||||||
|
* `ReactAdapterComponent` Flow Java class.
|
||||||
|
*/
|
||||||
|
export abstract class ReactAdapterElement extends HTMLElement {
|
||||||
|
#root: Root | undefined = undefined;
|
||||||
|
#rootRendered: boolean = false;
|
||||||
|
#rendering: ReactNode | undefined = undefined;
|
||||||
|
|
||||||
|
#state: Record<string, unknown> = Object.create(null);
|
||||||
|
#stateSetters = new Map<string, Dispatch<unknown>>();
|
||||||
|
#customEvents = new Map<string, DispatchEvent<unknown>>();
|
||||||
|
#dispatchFlowState: Dispatch<FlowStateReducerAction> = emptyAction;
|
||||||
|
|
||||||
|
#readyCallback = new Map<string, ReadyCallbackFunction>();
|
||||||
|
|
||||||
|
readonly #renderHooks: RenderHooks;
|
||||||
|
|
||||||
|
readonly #Wrapper: () => ReactElement | null;
|
||||||
|
|
||||||
|
#unmounting?: Promise<void>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#renderHooks = {
|
||||||
|
useState: this.useState.bind(this),
|
||||||
|
useCustomEvent: this.useCustomEvent.bind(this),
|
||||||
|
useContent: this.useContent.bind(this)
|
||||||
|
};
|
||||||
|
this.#Wrapper = this.#renderWrapper.bind(this);
|
||||||
|
this.#markAsUsed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connectedCallback() {
|
||||||
|
this.#rendering = createElement(this.#Wrapper);
|
||||||
|
const createNewRoot = this.dispatchEvent(
|
||||||
|
new CustomEvent('flow-portal-add', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
children: this.#rendering,
|
||||||
|
domNode: this
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createNewRoot || this.#root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#unmounting;
|
||||||
|
|
||||||
|
this.#root = createRoot(this);
|
||||||
|
this.#maybeRenderRoot();
|
||||||
|
this.#root.render(this.#rendering);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a callback for specified element identifier to be called when
|
||||||
|
* react element is ready.
|
||||||
|
* <p>
|
||||||
|
* For internal use only. May be renamed or removed in a future release.
|
||||||
|
*
|
||||||
|
* @param id element identifier that callback is for
|
||||||
|
* @param readyCallback callback method to be informed on element ready state
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public addReadyCallback(id: string, readyCallback: ReadyCallbackFunction) {
|
||||||
|
this.#readyCallback.set(id, readyCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback() {
|
||||||
|
if (!this.#root) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('flow-portal-remove', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
children: this.#rendering,
|
||||||
|
domNode: this
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.#unmounting = Promise.resolve();
|
||||||
|
await this.#unmounting;
|
||||||
|
this.#root.unmount();
|
||||||
|
this.#root = undefined;
|
||||||
|
}
|
||||||
|
this.#rootRendered = false;
|
||||||
|
this.#rendering = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook API for using stateful JS properties of the Web Component from
|
||||||
|
* the React `render()`.
|
||||||
|
*
|
||||||
|
* @typeParam T - Type of the state value
|
||||||
|
*
|
||||||
|
* @param key - Web Component property name, which is used for two-way
|
||||||
|
* value propagation from the server and back.
|
||||||
|
* @param initialValue - Fallback initial value (optional). Only applies if
|
||||||
|
* the Java component constructor does not invoke `setState`.
|
||||||
|
* @returns A tuple with two values:
|
||||||
|
* 1. The current state.
|
||||||
|
* 2. The `set` function for changing the state and triggering render
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected useState<T>(key: string, initialValue?: T): [value: T, setValue: Dispatch<T>] {
|
||||||
|
if (this.#stateSetters.has(key)) {
|
||||||
|
return [this.#state[key] as T, this.#stateSetters.get(key)!];
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = ((this as Record<string, unknown>)[key] as T) ?? initialValue!;
|
||||||
|
this.#state[key] = value;
|
||||||
|
Object.defineProperty(this, key, {
|
||||||
|
enumerable: true,
|
||||||
|
get(): T {
|
||||||
|
return this.#state[key];
|
||||||
|
},
|
||||||
|
set(nextValue: T) {
|
||||||
|
this.#state[key] = nextValue;
|
||||||
|
this.#dispatchFlowState({ type: 'stateKeyChanged', key, value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dispatchChangedEvent = this.useCustomEvent<{ value: T }>(`${key}-changed`, { detail: { value } });
|
||||||
|
const setValue = (value: T) => {
|
||||||
|
this.#state[key] = value;
|
||||||
|
dispatchChangedEvent({ value });
|
||||||
|
this.#dispatchFlowState({ type: 'stateKeyChanged', key, value });
|
||||||
|
};
|
||||||
|
this.#stateSetters.set(key, setValue as Dispatch<unknown>);
|
||||||
|
return [value, setValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook helper to simplify dispatching a `CustomEvent` on the Web
|
||||||
|
* Component from React.
|
||||||
|
*
|
||||||
|
* @typeParam T - The type for `event.detail` value (optional).
|
||||||
|
*
|
||||||
|
* @param type - The `CustomEvent` type string.
|
||||||
|
* @param options - The settings for the `CustomEvent`.
|
||||||
|
* @returns The `dispatch` function. The function parameters change
|
||||||
|
* depending on the `T` generic type:
|
||||||
|
* - For `undefined` type (default), has no parameters.
|
||||||
|
* - For other types, has one parameter for the `event.detail` value of that type.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected useCustomEvent<T = undefined>(type: string, options: CustomEventInit<T> = {}): DispatchEvent<T> {
|
||||||
|
if (!this.#customEvents.has(type)) {
|
||||||
|
const dispatch = ((detail?: T) => {
|
||||||
|
const eventInitDict =
|
||||||
|
detail === undefined
|
||||||
|
? options
|
||||||
|
: {
|
||||||
|
...options,
|
||||||
|
detail
|
||||||
|
};
|
||||||
|
const event = new CustomEvent(type, eventInitDict);
|
||||||
|
return this.dispatchEvent(event);
|
||||||
|
}) as DispatchEvent<T>;
|
||||||
|
this.#customEvents.set(type, dispatch as DispatchEvent<unknown>);
|
||||||
|
return dispatch;
|
||||||
|
}
|
||||||
|
return this.#customEvents.get(type)! as DispatchEvent<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Web Component render function. To be implemented by users with React.
|
||||||
|
*
|
||||||
|
* @param hooks - the adapter APIs exposed for the implementation.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected abstract render(hooks: RenderHooks): ReactElement | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare content container for Flow to bind server Element to.
|
||||||
|
*
|
||||||
|
* @param name container name attribute matching server name attribute
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected useContent(name: string): ReactElement | null {
|
||||||
|
useEffect(() => {
|
||||||
|
this.#readyCallback.get(name)?.();
|
||||||
|
}, []);
|
||||||
|
return createElement('flow-content-container', { name, style: { display: 'contents' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
#maybeRenderRoot() {
|
||||||
|
if (this.#rootRendered || !this.#root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#root.render(createElement(this.#Wrapper));
|
||||||
|
this.#rootRendered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderWrapper(): ReactElement | null {
|
||||||
|
const [state, dispatchFlowState] = useReducer(stateReducer, this.#state);
|
||||||
|
this.#state = state;
|
||||||
|
this.#dispatchFlowState = dispatchFlowState;
|
||||||
|
return this.render(this.#renderHooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
#markAsUsed(): void {
|
||||||
|
// @ts-ignore
|
||||||
|
let vaadinObject = window.Vaadin || {};
|
||||||
|
// @ts-ignore
|
||||||
|
if (vaadinObject.developmentMode) {
|
||||||
|
vaadinObject.registrations = vaadinObject.registrations || [];
|
||||||
|
vaadinObject.registrations.push({
|
||||||
|
is: 'ReactAdapterElement',
|
||||||
|
version: '25.0.8'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import '@vaadin/app-layout/src/vaadin-app-layout.js';
|
||||||
|
import '@vaadin/vertical-layout/src/vaadin-vertical-layout.js';
|
||||||
|
import '@vaadin/app-layout/src/vaadin-drawer-toggle.js';
|
||||||
|
import '@vaadin/button/src/vaadin-button.js';
|
||||||
|
import '@vaadin/tooltip/src/vaadin-tooltip.js';
|
||||||
|
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||||
|
import '@vaadin/side-nav/src/vaadin-side-nav.js';
|
||||||
|
import '@vaadin/side-nav/src/vaadin-side-nav-item.js';
|
||||||
|
import '@vaadin/icons/vaadin-iconset.js';
|
||||||
|
import '@vaadin/icon/src/vaadin-icon.js';
|
||||||
1
backend/src/main/frontend/generated/flow/generated-flow-imports.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/flow/generated-flow-imports.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import '@vaadin/app-layout/src/vaadin-app-layout.js';
|
||||||
|
import '@vaadin/vertical-layout/src/vaadin-vertical-layout.js';
|
||||||
|
import '@vaadin/app-layout/src/vaadin-drawer-toggle.js';
|
||||||
|
import '@vaadin/button/src/vaadin-button.js';
|
||||||
|
import '@vaadin/tooltip/src/vaadin-tooltip.js';
|
||||||
|
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||||
|
import '@vaadin/side-nav/src/vaadin-side-nav.js';
|
||||||
|
import '@vaadin/side-nav/src/vaadin-side-nav-item.js';
|
||||||
|
import '@vaadin/icons/vaadin-iconset.js';
|
||||||
|
import '@vaadin/icon/src/vaadin-icon.js';
|
||||||
|
import '@vaadin/common-frontend/ConnectionIndicator.js';
|
||||||
|
import 'Frontend/generated/jar-resources/ReactRouterOutletElement.tsx';
|
||||||
|
|
||||||
|
const loadOnDemand = (key) => {
|
||||||
|
const pending = [];
|
||||||
|
if (key === '1e3e1195126ded8f48ea9283d0ff579a0216bfc273c30bdac8e5152f029351ee') {
|
||||||
|
pending.push(import('./chunks/chunk-bb2f082c2d2806895673c8e73955a0121cc631902992dbe3083e4b276ee78c5f.js'));
|
||||||
|
}
|
||||||
|
return Promise.all(pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Vaadin = window.Vaadin || {};
|
||||||
|
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||||
|
window.Vaadin.Flow.loadOnDemand = loadOnDemand;
|
||||||
|
window.Vaadin.Flow.resetFocus = () => {
|
||||||
|
let ae=document.activeElement;
|
||||||
|
while(ae&&ae.shadowRoot) ae = ae.shadowRoot.activeElement;
|
||||||
|
return !ae || ae.blur() || ae.focus() || true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { injectGlobalWebcomponentCss } from 'Frontend/generated/jar-resources/theme-util.js';
|
||||||
|
|
||||||
|
import '@vaadin/app-layout/src/vaadin-app-layout.js';
|
||||||
|
import '@vaadin/vertical-layout/src/vaadin-vertical-layout.js';
|
||||||
|
import '@vaadin/app-layout/src/vaadin-drawer-toggle.js';
|
||||||
|
import '@vaadin/button/src/vaadin-button.js';
|
||||||
|
import '@vaadin/tooltip/src/vaadin-tooltip.js';
|
||||||
|
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||||
|
import '@vaadin/side-nav/src/vaadin-side-nav.js';
|
||||||
|
import '@vaadin/side-nav/src/vaadin-side-nav-item.js';
|
||||||
|
import '@vaadin/icons/vaadin-iconset.js';
|
||||||
|
import '@vaadin/icon/src/vaadin-icon.js';
|
||||||
|
import '@vaadin/common-frontend/ConnectionIndicator.js';
|
||||||
|
import 'Frontend/generated/jar-resources/ReactRouterOutletElement.tsx';
|
||||||
|
|
||||||
|
const loadOnDemand = (key) => {
|
||||||
|
const pending = [];
|
||||||
|
if (key === '1e3e1195126ded8f48ea9283d0ff579a0216bfc273c30bdac8e5152f029351ee') {
|
||||||
|
pending.push(import('./chunks/chunk-bb2f082c2d2806895673c8e73955a0121cc631902992dbe3083e4b276ee78c5f.js'));
|
||||||
|
}
|
||||||
|
return Promise.all(pending);
|
||||||
|
}
|
||||||
|
window.Vaadin = window.Vaadin || {};
|
||||||
|
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||||
|
window.Vaadin.Flow.loadOnDemand = loadOnDemand;
|
||||||
|
window.Vaadin.Flow.resetFocus = () => {
|
||||||
|
let ae=document.activeElement;
|
||||||
|
while(ae&&ae.shadowRoot) ae = ae.shadowRoot.activeElement;
|
||||||
|
return !ae || ae.blur() || ae.focus() || true;
|
||||||
|
}
|
||||||
26
backend/src/main/frontend/generated/index.tsx
Normal file
26
backend/src/main/frontend/generated/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* This file is auto-generated by Vaadin.
|
||||||
|
* If you want to customize the entry point, you can copy this file or create
|
||||||
|
* your own `index.tsx` in your frontend directory.
|
||||||
|
* By default, the `index.tsx` file should be in `./frontend/` folder.
|
||||||
|
*
|
||||||
|
* NOTE:
|
||||||
|
* - You need to restart the dev-server after adding the new `index.tsx` file.
|
||||||
|
* After that, all modifications to `index.tsx` are recompiled automatically.
|
||||||
|
* - `index.js` is also supported if you don't want to use TypeScript.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
import { createElement } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { RouterProvider } from 'react-router';
|
||||||
|
import { router } from 'Frontend/generated/routes.js';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outlet = document.getElementById('outlet')!;
|
||||||
|
let root = (outlet as any)._root ?? createRoot(outlet);
|
||||||
|
(outlet as any)._root = root;
|
||||||
|
root.render(createElement(App));
|
||||||
|
|
||||||
78
backend/src/main/frontend/generated/jar-resources/Flow.d.ts
vendored
Normal file
78
backend/src/main/frontend/generated/jar-resources/Flow.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
export interface FlowConfig {
|
||||||
|
imports?: () => Promise<any>;
|
||||||
|
}
|
||||||
|
interface AppConfig {
|
||||||
|
productionMode: boolean;
|
||||||
|
appId: string;
|
||||||
|
uidl: any;
|
||||||
|
}
|
||||||
|
interface AppInitResponse {
|
||||||
|
appConfig: AppConfig;
|
||||||
|
pushScript?: string;
|
||||||
|
}
|
||||||
|
interface Router {
|
||||||
|
render: (ctx: NavigationParameters, shouldUpdateHistory: boolean) => Promise<void>;
|
||||||
|
}
|
||||||
|
interface HTMLRouterContainer extends HTMLElement {
|
||||||
|
onBeforeEnter?: (ctx: NavigationParameters, cmd: PreventAndRedirectCommands, router: Router) => void | Promise<any>;
|
||||||
|
onBeforeLeave?: (ctx: NavigationParameters, cmd: PreventCommands, router: Router) => void | Promise<any>;
|
||||||
|
serverConnected?: (cancel: boolean, url?: NavigationParameters) => void;
|
||||||
|
serverPaused?: () => void;
|
||||||
|
}
|
||||||
|
interface FlowRoute {
|
||||||
|
action: (params: NavigationParameters) => Promise<HTMLRouterContainer>;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
export interface NavigationParameters {
|
||||||
|
pathname: string;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
export interface PreventCommands {
|
||||||
|
prevent: () => any;
|
||||||
|
continue?: () => any;
|
||||||
|
}
|
||||||
|
export interface PreventAndRedirectCommands extends PreventCommands {
|
||||||
|
redirect: (route: string) => any;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Client API for flow UI operations.
|
||||||
|
*/
|
||||||
|
export declare class Flow {
|
||||||
|
config: FlowConfig;
|
||||||
|
response?: AppInitResponse;
|
||||||
|
pathname: string;
|
||||||
|
container: HTMLRouterContainer;
|
||||||
|
private isActive;
|
||||||
|
private baseRegex;
|
||||||
|
private appShellTitle;
|
||||||
|
private navigation;
|
||||||
|
constructor(config?: FlowConfig);
|
||||||
|
/**
|
||||||
|
* Return a `route` object for vaadin-router in an one-element array.
|
||||||
|
*
|
||||||
|
* The `FlowRoute` object `path` property handles any route,
|
||||||
|
* and the `action` returns the flow container without updating the content,
|
||||||
|
* delaying the actual Flow server call to the `onBeforeEnter` phase.
|
||||||
|
*
|
||||||
|
* This is a specific API for its use with `vaadin-router`.
|
||||||
|
*/
|
||||||
|
get serverSideRoutes(): [FlowRoute];
|
||||||
|
loadingStarted(): void;
|
||||||
|
loadingFinished(): void;
|
||||||
|
private get action();
|
||||||
|
private flowLeave;
|
||||||
|
private flowNavigate;
|
||||||
|
private getFlowRoutePath;
|
||||||
|
private getFlowRouteQuery;
|
||||||
|
private flowInit;
|
||||||
|
private loadScript;
|
||||||
|
private findNonce;
|
||||||
|
private injectAppIdScript;
|
||||||
|
private flowInitClient;
|
||||||
|
private flowInitUi;
|
||||||
|
private collectBrowserDetails;
|
||||||
|
private addConnectionIndicator;
|
||||||
|
private offlineStubAction;
|
||||||
|
private isFlowClientLoaded;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
497
backend/src/main/frontend/generated/jar-resources/Flow.js
Normal file
497
backend/src/main/frontend/generated/jar-resources/Flow.js
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
import { ConnectionIndicator, ConnectionState } from '@vaadin/common-frontend';
|
||||||
|
class FlowUiInitializationError extends Error {
|
||||||
|
}
|
||||||
|
// flow uses body for keeping references
|
||||||
|
const flowRoot = window.document.body;
|
||||||
|
const $wnd = window;
|
||||||
|
const ROOT_NODE_ID = 1; // See StateTree.java
|
||||||
|
function getClients() {
|
||||||
|
return Object.keys($wnd.Vaadin.Flow.clients)
|
||||||
|
.filter((key) => key !== 'TypeScript')
|
||||||
|
.map((id) => $wnd.Vaadin.Flow.clients[id]);
|
||||||
|
}
|
||||||
|
function sendEvent(eventName, data) {
|
||||||
|
getClients().forEach((client) => client.sendEventMessage(ROOT_NODE_ID, eventName, data));
|
||||||
|
}
|
||||||
|
// In the future could be replaced with RegExp.escape()
|
||||||
|
function escapeRegExp(pattern) {
|
||||||
|
return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Client API for flow UI operations.
|
||||||
|
*/
|
||||||
|
export class Flow {
|
||||||
|
config;
|
||||||
|
response = undefined;
|
||||||
|
pathname = '';
|
||||||
|
container;
|
||||||
|
// flag used to inform Testbench whether a server route is in progress
|
||||||
|
isActive = false;
|
||||||
|
baseRegex = /^\//;
|
||||||
|
appShellTitle;
|
||||||
|
navigation = '';
|
||||||
|
constructor(config) {
|
||||||
|
// Set window.name early so @PreserveOnRefresh can use it to identify the browser tab
|
||||||
|
// Only set if not already set to preserve any existing value
|
||||||
|
if (!window.name) {
|
||||||
|
window.name = `v-${Math.random()}`;
|
||||||
|
}
|
||||||
|
flowRoot.$ = flowRoot.$ || [];
|
||||||
|
this.config = config || {};
|
||||||
|
// TB checks for the existence of window.Vaadin.Flow in order
|
||||||
|
// to consider that TB needs to wait for `initFlow()`.
|
||||||
|
$wnd.Vaadin = $wnd.Vaadin || {};
|
||||||
|
$wnd.Vaadin.Flow = $wnd.Vaadin.Flow || {};
|
||||||
|
$wnd.Vaadin.Flow.clients = {
|
||||||
|
TypeScript: {
|
||||||
|
isActive: () => this.isActive
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Set browser details collection function as global for use by refresh()
|
||||||
|
$wnd.Vaadin.Flow.getBrowserDetailsParameters = this.collectBrowserDetails.bind(this);
|
||||||
|
// Regular expression used to remove the app-context
|
||||||
|
const elm = document.head.querySelector('base');
|
||||||
|
this.baseRegex = new RegExp(`^${
|
||||||
|
// IE11 does not support document.baseURI
|
||||||
|
escapeRegExp(decodeURIComponent((document.baseURI || (elm && elm.href) || '/').replace(/^https?:\/\/[^/]+/i, '')))}`);
|
||||||
|
this.appShellTitle = document.title;
|
||||||
|
// Put a vaadin-connection-indicator in the dom
|
||||||
|
this.addConnectionIndicator();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return a `route` object for vaadin-router in an one-element array.
|
||||||
|
*
|
||||||
|
* The `FlowRoute` object `path` property handles any route,
|
||||||
|
* and the `action` returns the flow container without updating the content,
|
||||||
|
* delaying the actual Flow server call to the `onBeforeEnter` phase.
|
||||||
|
*
|
||||||
|
* This is a specific API for its use with `vaadin-router`.
|
||||||
|
*/
|
||||||
|
get serverSideRoutes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
path: '(.*)',
|
||||||
|
action: this.action
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
loadingStarted() {
|
||||||
|
// Make Testbench know that server request is in progress
|
||||||
|
this.isActive = true;
|
||||||
|
$wnd.Vaadin.connectionState.loadingStarted();
|
||||||
|
}
|
||||||
|
loadingFinished() {
|
||||||
|
// Make Testbench know that server request has finished
|
||||||
|
this.isActive = false;
|
||||||
|
$wnd.Vaadin.connectionState.loadingFinished();
|
||||||
|
if ($wnd.Vaadin.listener) {
|
||||||
|
// Listeners registered, do not register again.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$wnd.Vaadin.listener = {};
|
||||||
|
// Listen for click on router-links -> 'link' navigation trigger
|
||||||
|
// and on <a> nodes -> 'client' navigation trigger.
|
||||||
|
// Use capture phase to detect prevented / stopped events.
|
||||||
|
document.addEventListener('click', (_e) => {
|
||||||
|
if (_e.target) {
|
||||||
|
if (_e.composedPath().some((node) => node instanceof HTMLElement && node.hasAttribute('router-link'))) {
|
||||||
|
this.navigation = 'link';
|
||||||
|
}
|
||||||
|
else if (_e.composedPath().some((node) => node.nodeName === 'A')) {
|
||||||
|
this.navigation = 'client';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
capture: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
get action() {
|
||||||
|
// Return a function which is bound to the flow instance, thus we can use
|
||||||
|
// the syntax `...serverSideRoutes` in vaadin-router.
|
||||||
|
return async (params) => {
|
||||||
|
// Store last action pathname so as we can check it in events
|
||||||
|
this.pathname = params.pathname;
|
||||||
|
if ($wnd.Vaadin.connectionState.online) {
|
||||||
|
try {
|
||||||
|
await this.flowInit();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof FlowUiInitializationError) {
|
||||||
|
// error initializing Flow: assume connection lost
|
||||||
|
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;
|
||||||
|
return this.offlineStubAction();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// insert an offline stub
|
||||||
|
return this.offlineStubAction();
|
||||||
|
}
|
||||||
|
// When an action happens, navigation will be resolved `onBeforeEnter`
|
||||||
|
this.container.onBeforeEnter = (ctx, cmd) => this.flowNavigate(ctx, cmd);
|
||||||
|
// For covering the 'server -> client' use case
|
||||||
|
this.container.onBeforeLeave = (ctx, cmd) => this.flowLeave(ctx, cmd);
|
||||||
|
return this.container;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Send a remote call to `JavaScriptBootstrapUI` to check
|
||||||
|
// whether navigation has to be cancelled.
|
||||||
|
async flowLeave(ctx, cmd) {
|
||||||
|
// server -> server, viewing offline stub, or browser is offline
|
||||||
|
const { connectionState } = $wnd.Vaadin;
|
||||||
|
if (this.pathname === ctx.pathname || !this.isFlowClientLoaded() || connectionState.offline) {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
// 'server -> client'
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.loadingStarted();
|
||||||
|
// The callback to run from server side to cancel navigation
|
||||||
|
this.container.serverConnected = (cancel) => {
|
||||||
|
resolve(cmd && cancel ? cmd.prevent() : cmd?.continue?.());
|
||||||
|
this.loadingFinished();
|
||||||
|
};
|
||||||
|
// Call server side to check whether we can leave the view
|
||||||
|
sendEvent('ui-leave-navigation', { route: this.getFlowRoutePath(ctx), query: this.getFlowRouteQuery(ctx) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Send the remote call to `JavaScriptBootstrapUI` to render the flow
|
||||||
|
// route specified by the context
|
||||||
|
async flowNavigate(ctx, cmd) {
|
||||||
|
if (this.response) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.loadingStarted();
|
||||||
|
// The callback to run from server side once the view is ready
|
||||||
|
this.container.serverConnected = (cancel, redirectContext) => {
|
||||||
|
if (cmd && cancel) {
|
||||||
|
resolve(cmd.prevent());
|
||||||
|
}
|
||||||
|
else if (cmd && cmd.redirect && redirectContext) {
|
||||||
|
resolve(cmd.redirect(redirectContext.pathname));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cmd?.continue?.();
|
||||||
|
this.container.style.display = '';
|
||||||
|
resolve(this.container);
|
||||||
|
}
|
||||||
|
this.loadingFinished();
|
||||||
|
};
|
||||||
|
this.container.serverPaused = () => {
|
||||||
|
this.loadingFinished();
|
||||||
|
};
|
||||||
|
// Call server side to navigate to the given route
|
||||||
|
sendEvent('ui-navigate', {
|
||||||
|
route: this.getFlowRoutePath(ctx),
|
||||||
|
query: this.getFlowRouteQuery(ctx),
|
||||||
|
appShellTitle: this.appShellTitle,
|
||||||
|
historyState: history.state,
|
||||||
|
trigger: this.navigation
|
||||||
|
});
|
||||||
|
// Default to history navigation trigger.
|
||||||
|
// Link and client cases are handled by click listener in loadingFinished().
|
||||||
|
this.navigation = 'history';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No server response => offline or erroneous connection
|
||||||
|
return Promise.resolve(this.container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getFlowRoutePath(context) {
|
||||||
|
return decodeURIComponent(context.pathname).replace(this.baseRegex, '');
|
||||||
|
}
|
||||||
|
getFlowRouteQuery(context) {
|
||||||
|
return (context.search && context.search.substring(1)) || '';
|
||||||
|
}
|
||||||
|
// import flow client modules and initialize UI in server side.
|
||||||
|
async flowInit() {
|
||||||
|
// Do not start flow twice
|
||||||
|
if (!this.isFlowClientLoaded()) {
|
||||||
|
$wnd.Vaadin.Flow.nonce = this.findNonce();
|
||||||
|
// show flow progress indicator
|
||||||
|
this.loadingStarted();
|
||||||
|
// Initialize server side UI
|
||||||
|
this.response = await this.flowInitUi();
|
||||||
|
const { pushScript, appConfig } = this.response;
|
||||||
|
if (typeof pushScript === 'string') {
|
||||||
|
await this.loadScript(pushScript);
|
||||||
|
}
|
||||||
|
const { appId } = appConfig;
|
||||||
|
// we use a custom tag for the flow app container
|
||||||
|
// This must be created before bootstrapMod.init is called as that call
|
||||||
|
// can handle a UIDL from the server, which relies on the container being available
|
||||||
|
const tag = `flow-container-${appId.toLowerCase()}`;
|
||||||
|
const serverCreatedContainer = document.querySelector(tag);
|
||||||
|
if (serverCreatedContainer) {
|
||||||
|
this.container = serverCreatedContainer;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.container = document.createElement(tag);
|
||||||
|
this.container.id = appId;
|
||||||
|
}
|
||||||
|
flowRoot.$[appId] = this.container;
|
||||||
|
// Load bootstrap script with server side parameters
|
||||||
|
const bootstrapMod = await import('./FlowBootstrap');
|
||||||
|
bootstrapMod.init(this.response);
|
||||||
|
// Load custom modules defined by user
|
||||||
|
if (typeof this.config.imports === 'function') {
|
||||||
|
this.injectAppIdScript(appId);
|
||||||
|
await this.config.imports();
|
||||||
|
}
|
||||||
|
// Load flow-client module
|
||||||
|
const clientMod = await import('./FlowClient');
|
||||||
|
await this.flowInitClient(clientMod);
|
||||||
|
// hide flow progress indicator
|
||||||
|
this.loadingFinished();
|
||||||
|
}
|
||||||
|
// It might be that components created from server expect that their content has been rendered.
|
||||||
|
// Appending eagerly the container we avoid these kind of errors.
|
||||||
|
// Note that the client router will move this container to the outlet if the navigation succeed
|
||||||
|
if (this.container && !this.container.isConnected) {
|
||||||
|
this.container.style.display = 'none';
|
||||||
|
document.body.appendChild(this.container);
|
||||||
|
}
|
||||||
|
return this.response;
|
||||||
|
}
|
||||||
|
async loadScript(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.onload = () => resolve();
|
||||||
|
script.onerror = reject;
|
||||||
|
script.src = url;
|
||||||
|
const { nonce } = $wnd.Vaadin.Flow;
|
||||||
|
if (nonce !== undefined) {
|
||||||
|
script.setAttribute('nonce', nonce);
|
||||||
|
}
|
||||||
|
document.body.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
findNonce() {
|
||||||
|
let nonce;
|
||||||
|
const scriptTags = document.head.getElementsByTagName('script');
|
||||||
|
for (const scriptTag of scriptTags) {
|
||||||
|
if (scriptTag.nonce) {
|
||||||
|
nonce = scriptTag.nonce;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
injectAppIdScript(appId) {
|
||||||
|
const appIdWithoutHashCode = appId.substring(0, appId.lastIndexOf('-'));
|
||||||
|
const scriptAppId = document.createElement('script');
|
||||||
|
scriptAppId.type = 'module';
|
||||||
|
scriptAppId.setAttribute('data-app-id', appIdWithoutHashCode);
|
||||||
|
const { nonce } = $wnd.Vaadin.Flow;
|
||||||
|
if (nonce !== undefined) {
|
||||||
|
scriptAppId.setAttribute('nonce', nonce);
|
||||||
|
}
|
||||||
|
document.body.append(scriptAppId);
|
||||||
|
}
|
||||||
|
// After the flow-client javascript module has been loaded, this initializes flow UI
|
||||||
|
// in the browser.
|
||||||
|
async flowInitClient(clientMod) {
|
||||||
|
clientMod.init();
|
||||||
|
// client init is async, we need to loop until initialized
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
// client `isActive() == true` while initializing or processing
|
||||||
|
const initializing = getClients().reduce((prev, client) => prev || client.isActive(), false);
|
||||||
|
if (!initializing) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 5);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Returns the `appConfig` object
|
||||||
|
async flowInitUi() {
|
||||||
|
// appConfig was sent in the index.html request
|
||||||
|
const initial = $wnd.Vaadin && $wnd.Vaadin.TypeScript && $wnd.Vaadin.TypeScript.initial;
|
||||||
|
if (initial) {
|
||||||
|
$wnd.Vaadin.TypeScript.initial = undefined;
|
||||||
|
return Promise.resolve(initial);
|
||||||
|
}
|
||||||
|
// send a request to the `JavaScriptBootstrapHandler`
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const httpRequest = xhr;
|
||||||
|
// Collect browser details to send with init request as JSON
|
||||||
|
const browserDetails = this.collectBrowserDetails();
|
||||||
|
const browserDetailsParam = browserDetails
|
||||||
|
? `&v-browserDetails=${encodeURIComponent(JSON.stringify(browserDetails))}`
|
||||||
|
: '';
|
||||||
|
const requestPath = `?v-r=init&location=${encodeURIComponent(this.getFlowRoutePath(location))}&query=${encodeURIComponent(this.getFlowRouteQuery(location))}${browserDetailsParam}`;
|
||||||
|
httpRequest.open('GET', requestPath);
|
||||||
|
httpRequest.onerror = () => reject(new FlowUiInitializationError(`Invalid server response when initializing Flow UI.
|
||||||
|
${httpRequest.status}
|
||||||
|
${httpRequest.responseText}`));
|
||||||
|
httpRequest.onload = () => {
|
||||||
|
const contentType = httpRequest.getResponseHeader('content-type');
|
||||||
|
if (contentType && contentType.indexOf('application/json') !== -1) {
|
||||||
|
resolve(JSON.parse(httpRequest.responseText));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
httpRequest.onerror();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
httpRequest.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Collects browser details parameters
|
||||||
|
collectBrowserDetails() {
|
||||||
|
const params = {};
|
||||||
|
/* Screen height and width */
|
||||||
|
params['v-sh'] = $wnd.screen.height;
|
||||||
|
params['v-sw'] = $wnd.screen.width;
|
||||||
|
/* Browser window dimensions */
|
||||||
|
params['v-wh'] = $wnd.innerHeight;
|
||||||
|
params['v-ww'] = $wnd.innerWidth;
|
||||||
|
/* Body element dimensions */
|
||||||
|
params['v-bh'] = $wnd.document.body.clientHeight;
|
||||||
|
params['v-bw'] = $wnd.document.body.clientWidth;
|
||||||
|
/* Current time */
|
||||||
|
const date = new Date();
|
||||||
|
params['v-curdate'] = date.getTime();
|
||||||
|
/* Current timezone offset (including DST shift) */
|
||||||
|
const tzo1 = date.getTimezoneOffset();
|
||||||
|
/* Compare the current tz offset with the first offset from the end
|
||||||
|
of the year that differs --- if less that, we are in DST, otherwise
|
||||||
|
we are in normal time */
|
||||||
|
let dstDiff = 0;
|
||||||
|
let rawTzo = tzo1;
|
||||||
|
for (let m = 12; m > 0; m -= 1) {
|
||||||
|
date.setUTCMonth(m);
|
||||||
|
const tzo2 = date.getTimezoneOffset();
|
||||||
|
if (tzo1 !== tzo2) {
|
||||||
|
dstDiff = tzo1 > tzo2 ? tzo1 - tzo2 : tzo2 - tzo1;
|
||||||
|
rawTzo = tzo1 > tzo2 ? tzo1 : tzo2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Time zone offset */
|
||||||
|
params['v-tzo'] = tzo1;
|
||||||
|
/* DST difference */
|
||||||
|
params['v-dstd'] = dstDiff;
|
||||||
|
/* Time zone offset without DST */
|
||||||
|
params['v-rtzo'] = rawTzo;
|
||||||
|
/* DST in effect? */
|
||||||
|
params['v-dston'] = tzo1 !== rawTzo;
|
||||||
|
/* Time zone id (if available) */
|
||||||
|
try {
|
||||||
|
params['v-tzid'] = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
params['v-tzid'] = '';
|
||||||
|
}
|
||||||
|
/* Window name */
|
||||||
|
if ($wnd.name) {
|
||||||
|
params['v-wn'] = $wnd.name;
|
||||||
|
}
|
||||||
|
/* Detect touch device support */
|
||||||
|
let supportsTouch = false;
|
||||||
|
try {
|
||||||
|
$wnd.document.createEvent('TouchEvent');
|
||||||
|
supportsTouch = true;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
/* Chrome and IE10 touch detection */
|
||||||
|
supportsTouch = 'ontouchstart' in $wnd || typeof $wnd.navigator.msMaxTouchPoints !== 'undefined';
|
||||||
|
}
|
||||||
|
params['v-td'] = supportsTouch;
|
||||||
|
/* Device Pixel Ratio */
|
||||||
|
params['v-pr'] = $wnd.devicePixelRatio;
|
||||||
|
if ($wnd.navigator.platform) {
|
||||||
|
params['v-np'] = $wnd.navigator.platform;
|
||||||
|
}
|
||||||
|
/* Color scheme from CSS color-scheme property */
|
||||||
|
const colorScheme = getComputedStyle(document.documentElement).colorScheme.trim();
|
||||||
|
// "normal" is the default value and means no color scheme is set
|
||||||
|
params['v-cs'] = colorScheme && colorScheme !== 'normal' ? colorScheme : '';
|
||||||
|
/* Theme name - detect which theme is in use */
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
let themeName = '';
|
||||||
|
if (computedStyle.getPropertyValue('--vaadin-lumo-theme').trim()) {
|
||||||
|
themeName = 'lumo';
|
||||||
|
}
|
||||||
|
else if (computedStyle.getPropertyValue('--vaadin-aura-theme').trim()) {
|
||||||
|
themeName = 'aura';
|
||||||
|
}
|
||||||
|
params['v-tn'] = themeName;
|
||||||
|
/* Stringify each value (they are parsed on the server side) */
|
||||||
|
const stringParams = {};
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
const value = params[key];
|
||||||
|
if (typeof value !== 'undefined') {
|
||||||
|
stringParams[key] = value.toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return stringParams;
|
||||||
|
}
|
||||||
|
// Create shared connection state store and connection indicator
|
||||||
|
addConnectionIndicator() {
|
||||||
|
// add connection indicator to DOM
|
||||||
|
ConnectionIndicator.create();
|
||||||
|
// Listen to browser online/offline events and update the loading indicator accordingly.
|
||||||
|
// Note: if flow-client is loaded, it instead handles the state transitions.
|
||||||
|
$wnd.addEventListener('online', () => {
|
||||||
|
if (!this.isFlowClientLoaded()) {
|
||||||
|
// Send an HTTP HEAD request for sw.js to verify server reachability.
|
||||||
|
// We do not expect sw.js to be cached, so the request goes to the
|
||||||
|
// server rather than being served from local cache.
|
||||||
|
// Require network-level failure to revert the state to CONNECTION_LOST
|
||||||
|
// (HTTP error code is ok since it still verifies server's presence).
|
||||||
|
$wnd.Vaadin.connectionState.state = ConnectionState.RECONNECTING;
|
||||||
|
const http = new XMLHttpRequest();
|
||||||
|
http.open('HEAD', 'sw.js');
|
||||||
|
http.onload = () => {
|
||||||
|
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTED;
|
||||||
|
};
|
||||||
|
http.onerror = () => {
|
||||||
|
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;
|
||||||
|
};
|
||||||
|
// Postpone request to reduce potential net::ERR_INTERNET_DISCONNECTED
|
||||||
|
// errors that sometimes occurs even if browser says it is online
|
||||||
|
setTimeout(() => http.send(), 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$wnd.addEventListener('offline', () => {
|
||||||
|
if (!this.isFlowClientLoaded()) {
|
||||||
|
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async offlineStubAction() {
|
||||||
|
const offlineStub = document.createElement('iframe');
|
||||||
|
const offlineStubPath = './offline-stub.html';
|
||||||
|
offlineStub.setAttribute('src', offlineStubPath);
|
||||||
|
offlineStub.setAttribute('style', 'width: 100%; height: 100%; border: 0');
|
||||||
|
this.response = undefined;
|
||||||
|
let onlineListener;
|
||||||
|
const removeOfflineStubAndOnlineListener = () => {
|
||||||
|
if (onlineListener !== undefined) {
|
||||||
|
$wnd.Vaadin.connectionState.removeStateChangeListener(onlineListener);
|
||||||
|
onlineListener = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
offlineStub.onBeforeEnter = (ctx, _cmds, router) => {
|
||||||
|
onlineListener = () => {
|
||||||
|
if ($wnd.Vaadin.connectionState.online) {
|
||||||
|
removeOfflineStubAndOnlineListener();
|
||||||
|
router.render(ctx, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$wnd.Vaadin.connectionState.addStateChangeListener(onlineListener);
|
||||||
|
};
|
||||||
|
offlineStub.onBeforeLeave = (_ctx, _cmds, _router) => {
|
||||||
|
removeOfflineStubAndOnlineListener();
|
||||||
|
};
|
||||||
|
return offlineStub;
|
||||||
|
}
|
||||||
|
isFlowClientLoaded() {
|
||||||
|
return this.response !== undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=Flow.js.map
|
||||||
File diff suppressed because one or more lines are too long
1
backend/src/main/frontend/generated/jar-resources/FlowBootstrap.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/jar-resources/FlowBootstrap.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const init: (appInitResponse: any) => void;
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/* This is a copy of the regular `BootstrapHandler.js` in the flow-server
|
||||||
|
module, but with the following modifications:
|
||||||
|
- The main function is exported as an ES module for lazy initialization.
|
||||||
|
- Application configuration is passed as a parameter instead of using
|
||||||
|
replacement placeholders as in the regular bootstrapping.
|
||||||
|
- It reuses `Vaadin.Flow.clients` if exists.
|
||||||
|
- Fixed lint errors.
|
||||||
|
*/
|
||||||
|
const init = function (appInitResponse) {
|
||||||
|
window.Vaadin = window.Vaadin || {};
|
||||||
|
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||||
|
|
||||||
|
var apps = {};
|
||||||
|
var widgetsets = {};
|
||||||
|
|
||||||
|
var log;
|
||||||
|
if (typeof window.console === undefined || !window.location.search.match(/[&?]debug(&|$)/)) {
|
||||||
|
/* If no console.log present, just use a no-op */
|
||||||
|
log = function () {};
|
||||||
|
} else if (typeof window.console.log === 'function') {
|
||||||
|
/* If it's a function, use it with apply */
|
||||||
|
log = function () {
|
||||||
|
window.console.log.apply(window.console, arguments);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
/* In IE, its a native function for which apply is not defined, but it works
|
||||||
|
without a proper 'this' reference */
|
||||||
|
log = window.console.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isInitializedInDom = function (appId) {
|
||||||
|
var appDiv = document.getElementById(appId);
|
||||||
|
if (!appDiv) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < appDiv.childElementCount; i++) {
|
||||||
|
var className = appDiv.childNodes[i].className;
|
||||||
|
/* If the app div contains a child with the class
|
||||||
|
'v-app-loading' we have only received the HTML
|
||||||
|
but not yet started the widget set
|
||||||
|
(UIConnector removes the v-app-loading div). */
|
||||||
|
if (className && className.indexOf('v-app-loading') != -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Needed for Testbench compatibility, but prevents any Vaadin 7 app from
|
||||||
|
* bootstrapping unless the legacy vaadinBootstrap.js file is loaded before
|
||||||
|
* this script.
|
||||||
|
*/
|
||||||
|
window.Vaadin = window.Vaadin || {};
|
||||||
|
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Needed for wrapping custom javascript functionality in the components (i.e. connectors)
|
||||||
|
*/
|
||||||
|
window.Vaadin.Flow.tryCatchWrapper = function (originalFunction, component) {
|
||||||
|
return function () {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const result = originalFunction.apply(this, arguments);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`There seems to be an error in ${component}:
|
||||||
|
${error.message}
|
||||||
|
Please submit an issue to https://github.com/vaadin/flow-components/issues/new/choose`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!window.Vaadin.Flow.initApplication) {
|
||||||
|
window.Vaadin.Flow.clients = window.Vaadin.Flow.clients || {};
|
||||||
|
|
||||||
|
window.Vaadin.Flow.initApplication = function (appId, config) {
|
||||||
|
var testbenchId = appId.replace(/-\d+$/, '');
|
||||||
|
|
||||||
|
if (apps[appId]) {
|
||||||
|
if (
|
||||||
|
window.Vaadin &&
|
||||||
|
window.Vaadin.Flow &&
|
||||||
|
window.Vaadin.Flow.clients &&
|
||||||
|
window.Vaadin.Flow.clients[testbenchId] &&
|
||||||
|
window.Vaadin.Flow.clients[testbenchId].initializing
|
||||||
|
) {
|
||||||
|
throw new Error('Application ' + appId + ' is already being initialized');
|
||||||
|
}
|
||||||
|
if (isInitializedInDom(appId)) {
|
||||||
|
if (appInitResponse.appConfig.productionMode) {
|
||||||
|
throw new Error('Application ' + appId + ' already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old contents for Flow
|
||||||
|
var appDiv = document.getElementById(appId);
|
||||||
|
for (var i = 0; i < appDiv.childElementCount; i++) {
|
||||||
|
appDiv.childNodes[i].remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For devMode reset app config and restart widgetset as client
|
||||||
|
// is up and running after hrm update.
|
||||||
|
const getConfig = function (name) {
|
||||||
|
return config[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Export public data */
|
||||||
|
const app = {
|
||||||
|
getConfig: getConfig
|
||||||
|
};
|
||||||
|
apps[appId] = app;
|
||||||
|
|
||||||
|
if (widgetsets['client'].callback) {
|
||||||
|
log('Starting from bootstrap', appId);
|
||||||
|
widgetsets['client'].callback(appId);
|
||||||
|
} else {
|
||||||
|
log('Setting pending startup', appId);
|
||||||
|
widgetsets['client'].pendingApps.push(appId);
|
||||||
|
}
|
||||||
|
return apps[appId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log('init application', appId, config);
|
||||||
|
|
||||||
|
window.Vaadin.Flow.clients[testbenchId] = {
|
||||||
|
isActive: function () {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
initializing: true,
|
||||||
|
productionMode: mode
|
||||||
|
};
|
||||||
|
|
||||||
|
var getConfig = function (name) {
|
||||||
|
var value = config[name];
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Export public data */
|
||||||
|
var app = {
|
||||||
|
getConfig: getConfig
|
||||||
|
};
|
||||||
|
apps[appId] = app;
|
||||||
|
|
||||||
|
var widgetset = 'client';
|
||||||
|
widgetsets[widgetset] = {
|
||||||
|
pendingApps: []
|
||||||
|
};
|
||||||
|
if (widgetsets[widgetset].callback) {
|
||||||
|
log('Starting from bootstrap', appId);
|
||||||
|
widgetsets[widgetset].callback(appId);
|
||||||
|
} else {
|
||||||
|
log('Setting pending startup', appId);
|
||||||
|
widgetsets[widgetset].pendingApps.push(appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
};
|
||||||
|
window.Vaadin.Flow.getAppIds = function () {
|
||||||
|
var ids = [];
|
||||||
|
for (var id in apps) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(apps, id)) {
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
};
|
||||||
|
window.Vaadin.Flow.getApp = function (appId) {
|
||||||
|
return apps[appId];
|
||||||
|
};
|
||||||
|
window.Vaadin.Flow.registerWidgetset = function (widgetset, callback) {
|
||||||
|
log('Widgetset registered', widgetset);
|
||||||
|
var ws = widgetsets[widgetset];
|
||||||
|
if (ws && ws.pendingApps) {
|
||||||
|
ws.callback = callback;
|
||||||
|
for (var i = 0; i < ws.pendingApps.length; i++) {
|
||||||
|
var appId = ws.pendingApps[i];
|
||||||
|
log('Starting from register widgetset', appId);
|
||||||
|
callback(appId);
|
||||||
|
}
|
||||||
|
ws.pendingApps = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Flow bootstrap loaded');
|
||||||
|
if (appInitResponse.appConfig.productionMode && typeof window.__gwtStatsEvent != 'function') {
|
||||||
|
window.Vaadin.Flow.gwtStatsEvents = [];
|
||||||
|
window.__gwtStatsEvent = function (event) {
|
||||||
|
window.Vaadin.Flow.gwtStatsEvents.push(event);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var config = appInitResponse.appConfig;
|
||||||
|
var mode = appInitResponse.appConfig.productionMode;
|
||||||
|
window.Vaadin.Flow.initApplication(config.appId, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { init };
|
||||||
1
backend/src/main/frontend/generated/jar-resources/FlowClient.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/jar-resources/FlowClient.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const init: () => void;
|
||||||
1090
backend/src/main/frontend/generated/jar-resources/FlowClient.js
Normal file
1090
backend/src/main/frontend/generated/jar-resources/FlowClient.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
|||||||
|
import { Outlet } from 'react-router';
|
||||||
|
import { ReactAdapterElement } from "Frontend/generated/flow/ReactAdapter.js";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
class ReactRouterOutletElement extends ReactAdapterElement {
|
||||||
|
public async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
this.style.display = 'contents';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): React.ReactElement | null {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('react-router-outlet', ReactRouterOutletElement);
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
||||||
|
import { timeOut } from '@vaadin/component-base/src/async.js';
|
||||||
|
import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js';
|
||||||
|
|
||||||
|
window.Vaadin.Flow.comboBoxConnector = {};
|
||||||
|
window.Vaadin.Flow.comboBoxConnector.initLazy = (comboBox) => {
|
||||||
|
// Check whether the connector was already initialized for the ComboBox
|
||||||
|
if (comboBox.$connector) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
comboBox.$connector = {};
|
||||||
|
|
||||||
|
// holds pageIndex -> callback pairs of subsequent indexes (current active range)
|
||||||
|
const pageCallbacks = {};
|
||||||
|
let cache = {};
|
||||||
|
let lastFilter = '';
|
||||||
|
const placeHolder = new window.Vaadin.ComboBoxPlaceholder();
|
||||||
|
|
||||||
|
const serverFacade = (() => {
|
||||||
|
// Private variables
|
||||||
|
let lastFilterSentToServer = '';
|
||||||
|
let dataCommunicatorResetNeeded = false;
|
||||||
|
|
||||||
|
// Public methods
|
||||||
|
const needsDataCommunicatorReset = () => (dataCommunicatorResetNeeded = true);
|
||||||
|
const getLastFilterSentToServer = () => lastFilterSentToServer;
|
||||||
|
const requestData = (startIndex, endIndex, params) => {
|
||||||
|
const count = endIndex - startIndex;
|
||||||
|
const filter = params.filter;
|
||||||
|
|
||||||
|
comboBox.$server.setViewportRange(startIndex, count, filter);
|
||||||
|
lastFilterSentToServer = filter;
|
||||||
|
if (dataCommunicatorResetNeeded) {
|
||||||
|
comboBox.$server.resetDataCommunicator();
|
||||||
|
dataCommunicatorResetNeeded = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
needsDataCommunicatorReset,
|
||||||
|
getLastFilterSentToServer,
|
||||||
|
requestData
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const clearPageCallbacks = (pages = Object.keys(pageCallbacks)) => {
|
||||||
|
// Flush and empty the existing requests
|
||||||
|
pages.forEach((page) => {
|
||||||
|
pageCallbacks[page]([], comboBox.size);
|
||||||
|
delete pageCallbacks[page];
|
||||||
|
|
||||||
|
// Empty the comboBox's internal cache without invoking observers by filling
|
||||||
|
// the filteredItems array with placeholders (comboBox will request for data when it
|
||||||
|
// encounters a placeholder)
|
||||||
|
const pageStart = parseInt(page) * comboBox.pageSize;
|
||||||
|
const pageEnd = pageStart + comboBox.pageSize;
|
||||||
|
const end = Math.min(pageEnd, comboBox.filteredItems.length);
|
||||||
|
for (let i = pageStart; i < end; i++) {
|
||||||
|
comboBox.filteredItems[i] = placeHolder;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
comboBox.dataProvider = function (params, callback) {
|
||||||
|
if (params.pageSize != comboBox.pageSize) {
|
||||||
|
throw 'Invalid pageSize';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comboBox._clientSideFilter) {
|
||||||
|
// For clientside filter we first make sure we have all data which we also
|
||||||
|
// filter based on comboBox.filter. While later we only filter clientside data.
|
||||||
|
|
||||||
|
if (cache[0]) {
|
||||||
|
performClientSideFilter(cache[0], params.filter, callback);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// If client side filter is enabled then we need to first ask all data
|
||||||
|
// and filter it on client side, otherwise next time when user will
|
||||||
|
// input another filter, eg. continue to type, the local cache will be only
|
||||||
|
// what was received for the first filter, which may not be the whole
|
||||||
|
// data from server (keep in mind that client side filter is enabled only
|
||||||
|
// when the items count does not exceed one page).
|
||||||
|
params.filter = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterChanged = params.filter !== lastFilter;
|
||||||
|
if (filterChanged) {
|
||||||
|
cache = {};
|
||||||
|
lastFilter = params.filter;
|
||||||
|
comboBox._filterDebouncer = Debouncer.debounce(comboBox._filterDebouncer, timeOut.after(500), () => {
|
||||||
|
if (serverFacade.getLastFilterSentToServer() === params.filter) {
|
||||||
|
// Fixes the case when the filter changes
|
||||||
|
// to something else and back to the original value
|
||||||
|
// within debounce timeout, and the
|
||||||
|
// DataCommunicator thinks it doesn't need to send data
|
||||||
|
serverFacade.needsDataCommunicatorReset();
|
||||||
|
}
|
||||||
|
if (params.filter !== lastFilter) {
|
||||||
|
throw new Error("Expected params.filter to be '" + lastFilter + "' but was '" + params.filter + "'");
|
||||||
|
}
|
||||||
|
// Remove the debouncer before clearing page callbacks.
|
||||||
|
// This makes sure that they are executed.
|
||||||
|
comboBox._filterDebouncer = undefined;
|
||||||
|
// Call the method again after debounce.
|
||||||
|
clearPageCallbacks();
|
||||||
|
comboBox.dataProvider(params, callback);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Postpone the execution of new callbacks if there is an active debouncer.
|
||||||
|
// They will be executed when the page callbacks are cleared within the debouncer.
|
||||||
|
if (comboBox._filterDebouncer) {
|
||||||
|
pageCallbacks[params.page] = callback;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache[params.page]) {
|
||||||
|
// This may happen after skipping pages by scrolling fast
|
||||||
|
commitPage(params.page, callback);
|
||||||
|
} else {
|
||||||
|
pageCallbacks[params.page] = callback;
|
||||||
|
const maxRangeCount = Math.max(params.pageSize * 2, 500); // Max item count in active range
|
||||||
|
const activePages = Object.keys(pageCallbacks).map((page) => parseInt(page));
|
||||||
|
const rangeMin = Math.min(...activePages);
|
||||||
|
const rangeMax = Math.max(...activePages);
|
||||||
|
|
||||||
|
if (activePages.length * params.pageSize > maxRangeCount) {
|
||||||
|
if (params.page === rangeMin) {
|
||||||
|
clearPageCallbacks([String(rangeMax)]);
|
||||||
|
} else {
|
||||||
|
clearPageCallbacks([String(rangeMin)]);
|
||||||
|
}
|
||||||
|
comboBox.dataProvider(params, callback);
|
||||||
|
} else if (rangeMax - rangeMin + 1 !== activePages.length) {
|
||||||
|
// Wasn't a sequential page index, clear the cache so combo-box will request for new pages
|
||||||
|
clearPageCallbacks();
|
||||||
|
} else {
|
||||||
|
// The requested page was sequential, extend the requested range
|
||||||
|
const startIndex = params.pageSize * rangeMin;
|
||||||
|
const endIndex = params.pageSize * (rangeMax + 1);
|
||||||
|
|
||||||
|
serverFacade.requestData(startIndex, endIndex, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
comboBox.$connector.clear = (start, length) => {
|
||||||
|
const firstPageToClear = Math.floor(start / comboBox.pageSize);
|
||||||
|
const numberOfPagesToClear = Math.ceil(length / comboBox.pageSize);
|
||||||
|
|
||||||
|
for (let i = firstPageToClear; i < firstPageToClear + numberOfPagesToClear; i++) {
|
||||||
|
delete cache[i];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
comboBox.$connector.filter = (item, filter) => {
|
||||||
|
filter = filter ? filter.toString().toLowerCase() : '';
|
||||||
|
return comboBox._getItemLabel(item, comboBox.itemLabelPath).toString().toLowerCase().indexOf(filter) > -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
comboBox.$connector.set = (index, items, filter) => {
|
||||||
|
if (filter != serverFacade.getLastFilterSentToServer()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index % comboBox.pageSize != 0) {
|
||||||
|
throw 'Got new data to index ' + index + ' which is not aligned with the page size of ' + comboBox.pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 0 && items.length === 0 && pageCallbacks[0]) {
|
||||||
|
// Makes sure that the dataProvider callback is called even when server
|
||||||
|
// returns empty data set (no items match the filter).
|
||||||
|
cache[0] = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstPageToSet = index / comboBox.pageSize;
|
||||||
|
const updatedPageCount = Math.ceil(items.length / comboBox.pageSize);
|
||||||
|
|
||||||
|
for (let i = 0; i < updatedPageCount; i++) {
|
||||||
|
let page = firstPageToSet + i;
|
||||||
|
let slice = items.slice(i * comboBox.pageSize, (i + 1) * comboBox.pageSize);
|
||||||
|
|
||||||
|
cache[page] = slice;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
comboBox.$connector.updateData = (items) => {
|
||||||
|
const itemsMap = new Map(items.map((item) => [item.key, item]));
|
||||||
|
|
||||||
|
comboBox.filteredItems = comboBox.filteredItems.map((item) => {
|
||||||
|
return itemsMap.get(item.key) || item;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
comboBox.$connector.updateSize = function (newSize) {
|
||||||
|
if (!comboBox._clientSideFilter) {
|
||||||
|
// FIXME: It may be that this size set is unnecessary, since when
|
||||||
|
// providing data to combobox via callback we may use data's size.
|
||||||
|
// However, if this size reflect the whole data size, including
|
||||||
|
// data not fetched yet into client side, and combobox expect it
|
||||||
|
// to be set as such, the at least, we don't need it in case the
|
||||||
|
// filter is clientSide only, since it'll increase the height of
|
||||||
|
// the popup at only at first user filter to this size, while the
|
||||||
|
// filtered items count are less.
|
||||||
|
comboBox.size = newSize;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
comboBox.$connector.reset = function () {
|
||||||
|
// Cancel pending requests, as clearCache below will set the combo
|
||||||
|
// in a state where it will always request new data, regardless
|
||||||
|
// what is in the cache already.
|
||||||
|
if (comboBox._filterDebouncer) {
|
||||||
|
comboBox._filterDebouncer.cancel();
|
||||||
|
comboBox._filterDebouncer = undefined;
|
||||||
|
}
|
||||||
|
clearPageCallbacks();
|
||||||
|
cache = {};
|
||||||
|
comboBox.clearCache();
|
||||||
|
};
|
||||||
|
|
||||||
|
comboBox.$connector.confirm = function (id, filter) {
|
||||||
|
if (filter != serverFacade.getLastFilterSentToServer()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're done applying changes from this batch, resolve pending
|
||||||
|
// callbacks
|
||||||
|
let activePages = Object.getOwnPropertyNames(pageCallbacks);
|
||||||
|
for (let i = 0; i < activePages.length; i++) {
|
||||||
|
let page = activePages[i];
|
||||||
|
|
||||||
|
if (cache[page]) {
|
||||||
|
commitPage(page, pageCallbacks[page]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let server know we're done
|
||||||
|
comboBox.$server.confirmUpdate(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitPage = function (page, callback) {
|
||||||
|
let data = cache[page];
|
||||||
|
|
||||||
|
if (comboBox._clientSideFilter) {
|
||||||
|
performClientSideFilter(data, comboBox.filter, callback);
|
||||||
|
} else {
|
||||||
|
// Remove the data if server-side filtering, but keep it for client-side
|
||||||
|
// filtering
|
||||||
|
delete cache[page];
|
||||||
|
|
||||||
|
// FIXME: It may be that we ought to provide data.length instead of
|
||||||
|
// comboBox.size and remove updateSize function.
|
||||||
|
callback(data, comboBox.size);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perform filter on client side (here) using the items from specified page
|
||||||
|
// and submitting the filtered items to specified callback.
|
||||||
|
// The filter used is the one from combobox, not the lastFilter stored since
|
||||||
|
// that may not reflect user's input.
|
||||||
|
const performClientSideFilter = function (page, filter, callback) {
|
||||||
|
let filteredItems = page;
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
filteredItems = page.filter((item) => comboBox.$connector.filter(item, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(filteredItems, filteredItems.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent setting the custom value as the 'value'-prop automatically
|
||||||
|
comboBox.addEventListener('custom-value-set', (e) => e.preventDefault());
|
||||||
|
|
||||||
|
comboBox.itemClassNameGenerator = function (item) {
|
||||||
|
return item.className || '';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Vaadin.ComboBoxPlaceholder = ComboBoxPlaceholder;
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
function getContainer(appId, nodeId) {
|
||||||
|
try {
|
||||||
|
return window.Vaadin.Flow.clients[appId].getByNodeId(nodeId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Could not get node %s from app %s', nodeId, appId);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the connector for a context menu element.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} contextMenu
|
||||||
|
* @param {string} appId
|
||||||
|
*/
|
||||||
|
function initLazy(contextMenu, appId) {
|
||||||
|
if (contextMenu.$connector) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
contextMenu.$connector = {
|
||||||
|
/**
|
||||||
|
* Generates and assigns the items to the context menu.
|
||||||
|
*
|
||||||
|
* @param {number} nodeId
|
||||||
|
*/
|
||||||
|
generateItems(nodeId) {
|
||||||
|
const items = generateItemsTree(appId, nodeId);
|
||||||
|
|
||||||
|
contextMenu.items = items;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an items tree compatible with the context-menu web component
|
||||||
|
* by traversing the given Flow DOM tree of context menu item nodes
|
||||||
|
* whose root node is identified by the `nodeId` argument.
|
||||||
|
*
|
||||||
|
* The app id is required to access the store of Flow DOM nodes.
|
||||||
|
*
|
||||||
|
* @param {string} appId
|
||||||
|
* @param {number} nodeId
|
||||||
|
*/
|
||||||
|
function generateItemsTree(appId, nodeId) {
|
||||||
|
const container = getContainer(appId, nodeId);
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(container.children).map((child) => {
|
||||||
|
const item = {
|
||||||
|
component: child,
|
||||||
|
checked: child._checked,
|
||||||
|
keepOpen: child._keepOpen,
|
||||||
|
className: child.className,
|
||||||
|
theme: child.__theme
|
||||||
|
};
|
||||||
|
// Do not hardcode tag name to allow `vaadin-menu-bar-item`
|
||||||
|
if (child._hasVaadinItemMixin && child._containerNodeId) {
|
||||||
|
item.children = generateItemsTree(appId, child._containerNodeId);
|
||||||
|
}
|
||||||
|
child._item = item;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the checked state for a context menu item.
|
||||||
|
*
|
||||||
|
* This method is supposed to be called when the context menu item is closed,
|
||||||
|
* so there is no need for triggering a re-render eagarly.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} component
|
||||||
|
* @param {boolean} checked
|
||||||
|
*/
|
||||||
|
function setChecked(component, checked) {
|
||||||
|
if (component._item) {
|
||||||
|
component._item.checked = checked;
|
||||||
|
|
||||||
|
// Set the attribute in the connector to show the checkmark
|
||||||
|
// without having to re-render the whole menu while opened.
|
||||||
|
if (component._item.keepOpen) {
|
||||||
|
component.toggleAttribute('menu-item-checked', checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the keep open state for a context menu item.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} component
|
||||||
|
* @param {boolean} keepOpen
|
||||||
|
*/
|
||||||
|
function setKeepOpen(component, keepOpen) {
|
||||||
|
if (component._item) {
|
||||||
|
component._item.keepOpen = keepOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the theme for a context menu item.
|
||||||
|
*
|
||||||
|
* This method is supposed to be called when the context menu item is closed,
|
||||||
|
* so there is no need for triggering a re-render eagarly.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} component
|
||||||
|
* @param {string | undefined | null} theme
|
||||||
|
*/
|
||||||
|
function setTheme(component, theme) {
|
||||||
|
if (component._item) {
|
||||||
|
component._item.theme = theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Vaadin.Flow.contextMenuConnector = {
|
||||||
|
initLazy,
|
||||||
|
generateItemsTree,
|
||||||
|
setChecked,
|
||||||
|
setKeepOpen,
|
||||||
|
setTheme
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import * as Gestures from '@vaadin/component-base/src/gestures.js';
|
||||||
|
|
||||||
|
function init(target) {
|
||||||
|
if (target.$contextMenuTargetConnector) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.$contextMenuTargetConnector = {
|
||||||
|
openOnHandler(e) {
|
||||||
|
// used by Grid to prevent context menu on selection column click
|
||||||
|
if (target.preventContextMenu && target.preventContextMenu(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.$contextMenuTargetConnector.openEvent = e;
|
||||||
|
let detail = {};
|
||||||
|
if (target.getContextMenuBeforeOpenDetail) {
|
||||||
|
detail = target.getContextMenuBeforeOpenDetail(e);
|
||||||
|
}
|
||||||
|
target.dispatchEvent(
|
||||||
|
new CustomEvent('vaadin-context-menu-before-open', {
|
||||||
|
detail: detail
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateOpenOn(eventType) {
|
||||||
|
this.removeListener();
|
||||||
|
this.openOnEventType = eventType;
|
||||||
|
|
||||||
|
customElements.whenDefined('vaadin-context-menu').then(() => {
|
||||||
|
if (Gestures.gestures[eventType]) {
|
||||||
|
Gestures.addListener(target, eventType, this.openOnHandler);
|
||||||
|
} else {
|
||||||
|
target.addEventListener(eventType, this.openOnHandler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeListener() {
|
||||||
|
if (this.openOnEventType) {
|
||||||
|
if (Gestures.gestures[this.openOnEventType]) {
|
||||||
|
Gestures.removeListener(target, this.openOnEventType, this.openOnHandler);
|
||||||
|
} else {
|
||||||
|
target.removeEventListener(this.openOnEventType, this.openOnHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openMenu(contextMenu) {
|
||||||
|
contextMenu.open(this.openEvent);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeConnector() {
|
||||||
|
this.removeListener();
|
||||||
|
target.$contextMenuTargetConnector = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Vaadin.Flow.contextMenuTargetConnector = { init };
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// Full cdn version: 25.0.7-undefined
|
||||||
3
backend/src/main/frontend/generated/jar-resources/copilot.d.ts
vendored
Normal file
3
backend/src/main/frontend/generated/jar-resources/copilot.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { registerImporter, createChildrenDefinitions } from './copilot/figma-public/figma-api';
|
||||||
|
export type { FigmaNode, ImportMetadata } from './copilot/figma-public/figma-api';
|
||||||
|
export type { ComponentDefinition, ComponentDefinitionProperties } from './copilot/shared/flow-utils';
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { aB as a, aA as i } from "./copilot/copilot-BvIxHaRg.js";
|
||||||
|
export {
|
||||||
|
a as createChildrenDefinitions,
|
||||||
|
i as registerImporter
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { M as t, w as n, j as a, ay as i, b as o } from "./copilot-BvIxHaRg.js";
|
||||||
|
class l extends t {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments), this.eventBusRemovers = [], this.messageHandlers = {}, this.handleESC = (e) => {
|
||||||
|
const s = n.getPanelByTag(this.tagName);
|
||||||
|
!a.active && s && !s.individual || e.key === "Escape" && i(this);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
onEventBus(e, s) {
|
||||||
|
this.eventBusRemovers.push(o.on(e, s));
|
||||||
|
}
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback(), this.addESCListener();
|
||||||
|
}
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback(), this.eventBusRemovers.forEach((e) => e()), this.removeESCListener();
|
||||||
|
}
|
||||||
|
addESCListener() {
|
||||||
|
document.addEventListener("keydown", this.handleESC);
|
||||||
|
}
|
||||||
|
removeESCListener() {
|
||||||
|
document.removeEventListener("keydown", this.handleESC);
|
||||||
|
}
|
||||||
|
onCommand(e, s) {
|
||||||
|
this.messageHandlers[e] = s;
|
||||||
|
}
|
||||||
|
handleMessage(e) {
|
||||||
|
return this.messageHandlers[e.command] ? (this.messageHandlers[e.command].call(this, e), !0) : !1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
l as B
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user