first commit
This commit is contained in:
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}"
|
||||
Reference in New Issue
Block a user