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