BIG FAT
This commit is contained in:
31
CLAUDE.md
31
CLAUDE.md
@ -0,0 +1,31 @@
|
|||||||
|
# Introduction to your profile
|
||||||
|
You are a senior software engineer who has specialized in developing
|
||||||
|
Apps. You are educated on clean code and loves it! Furthermore, you want
|
||||||
|
to have good and clean software abstraction. Your focus is also on
|
||||||
|
the product itself to understand the customer's need. You have several
|
||||||
|
years experience in Flutter Development.
|
||||||
|
|
||||||
|
# Introduction to the app
|
||||||
|
This app is made for the company "Holzleitner GmbH" in Germany. This company
|
||||||
|
is a seller for electronics such as dishwasher, fridges, oven, etc.
|
||||||
|
The goal of the app is according to the CEO, to digitalize the logistics of that company.
|
||||||
|
The company has several delivery drivers under contract. The delivery driver are the user of that app.
|
||||||
|
They should be able to track today's deliveries, add notes to specific deliveries,
|
||||||
|
sign the completion of a delivery, add a refund (ger. "Gutschrift") to the delivery.
|
||||||
|
One delivery contracter MAY have multiple delivery driver. Each contracter has one account for the app.
|
||||||
|
So, multiple drivers use the same account and SHOULD not interfere with each other. The contractor
|
||||||
|
can manage its cars in the app.
|
||||||
|
|
||||||
|
The app should have multiple phases:
|
||||||
|
1. Car selection of the today's car of the driver
|
||||||
|
2. Loading phase. For the selected car, the barcodes of the goods are scanned and assigned to that car.
|
||||||
|
3. Delivery Phase. The drivers now see the current deliveries for today.
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
If you get asked for changing the app, first analyze the architecture of the app
|
||||||
|
by analyzing the given code files.
|
||||||
|
|
||||||
|
# How you need to behave if I ask you something?
|
||||||
|
If I give you a specific task (such as adding features, removing bad code smell, etc.) you have to
|
||||||
|
first analyze the code. Find the specific points in code that are potentially effected by my task.
|
||||||
|
Justify every step you make. Validate if your step is good or bad. Print a decision table.
|
||||||
85
docs/finish_delivery.md
Normal file
85
docs/finish_delivery.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Lieferungs-Abschluss: Ablauf & bekannte Themen
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt den Request-Ablauf, der ausgelöst wird, wenn der
|
||||||
|
Fahrer nach der Unterschrift "Lieferung abschließen" auslöst, sowie bekannte
|
||||||
|
Schwachstellen, die mittelfristig adressiert werden sollten.
|
||||||
|
|
||||||
|
Stand: 2026-04-26.
|
||||||
|
|
||||||
|
## Aufrufkette
|
||||||
|
|
||||||
|
UI: `SignatureView.onSigned` → `_onSign(customer, driver)`
|
||||||
|
(`lib/feature/delivery/detail/presentation/delivery_detail_page.dart`)
|
||||||
|
|
||||||
|
BLoC: dispatched `FinishDeliveryEvent` → `TourBloc._finishDelivery`
|
||||||
|
(`lib/feature/delivery/bloc/tour_bloc.dart`)
|
||||||
|
|
||||||
|
Repository (`lib/feature/delivery/repository/tour_repository.dart`):
|
||||||
|
|
||||||
|
1. `uploadDriverSignature(deliveryId, driverSignature)`
|
||||||
|
2. `uploadCustomerSignature(deliveryId, customerSignature)`
|
||||||
|
3. `finishDelivery(deliveryId)`
|
||||||
|
|
||||||
|
Daraus ergeben sich 7 sequenzielle HTTP-Requests (jeweils `await`):
|
||||||
|
|
||||||
|
| Reihenfolge | HTTP | Endpoint | Zweck |
|
||||||
|
| ----------- | ------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 1 | GET | `/v1/uploadFile` | `uploadId` für Fahrer-Signatur holen |
|
||||||
|
| 2 | POST | `/v1/uploadFile/{uploadId}` | Multipart-Upload `delivery_{id}_signature_driver.jpg` |
|
||||||
|
| 3 | PATCH | `/v1/uploadFile/{uploadId}` | Upload commit, liefert `ObjectID` |
|
||||||
|
| 4 | GET | `/v1/uploadFile` | `uploadId` für Kunden-Signatur holen |
|
||||||
|
| 5 | POST | `/v1/uploadFile/{uploadId}` | Multipart-Upload `delivery_{id}_signature_customer.jpg` |
|
||||||
|
| 6 | PATCH | `/v1/uploadFile/{uploadId}` | Upload commit |
|
||||||
|
| 7 | POST | `_web_finishDelivery` | Atomarer Abschluss: setzt `_SV_DELIVERY_STATE = "geliefert"` und `_SV_DELIVERY_DELIVERED_AT = <Zeitstempel>`, räumt entfernte Artikel auf. Body `{ "delivery_id": <id>, "delivered_at": "<yyyy-MM-ddTHH:mm:ss>" }` |
|
||||||
|
|
||||||
|
Die GET/POST/PATCH-Sequenz pro Signatur (Schritte 1–3 bzw. 4–6) ist vom
|
||||||
|
ERP-/Dokumentenverwaltungssystem so vorgegeben und wird hier **nicht**
|
||||||
|
angepasst.
|
||||||
|
|
||||||
|
Der frühere zusätzliche Aufruf von `_web_updateDelivery` mit `state = finished`
|
||||||
|
(historisch Schritt 7) ist entfallen: `_web_finishDelivery` setzt State und
|
||||||
|
Lieferzeitpunkt jetzt atomar in einem einzigen `UPDATE` auf `Belegkopf`.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
### 1. ~~Doppelter Abschluss-Call~~ — erledigt (2026-04-26)
|
||||||
|
|
||||||
|
Status: behoben. `_web_updateDelivery` wird im Abschluss-Flow nicht mehr
|
||||||
|
aufgerufen. `_web_finishDelivery` schreibt `_SV_DELIVERY_STATE` und
|
||||||
|
`_SV_DELIVERY_DELIVERED_AT` atomar in einem einzigen `UPDATE` auf
|
||||||
|
`Belegkopf` und führt anschließend `_removeArticles` aus.
|
||||||
|
|
||||||
|
### 3. Hartcodierte Sequenz ohne Retry, generisches Error-Reporting
|
||||||
|
|
||||||
|
Die 7 Requests laufen strikt nacheinander mit `await`. Bei einem Fehler an
|
||||||
|
einer beliebigen Stelle landet der Flow in `TourBloc._handleError` und
|
||||||
|
emittiert eine generische Meldung "Fehler beim Abschließen der Lieferung",
|
||||||
|
ohne den genauen Schritt zu nennen.
|
||||||
|
|
||||||
|
Risiken:
|
||||||
|
- Partial-Failure-Zustände sind möglich:
|
||||||
|
- Fehler in 1–3: keine Fahrer-Signatur, kein Abschluss.
|
||||||
|
- Fehler in 4–6: Fahrer-Signatur ist hochgeladen, Kunden-Signatur nicht,
|
||||||
|
Lieferung weiterhin offen.
|
||||||
|
- Fehler in 7 (`_web_finishDelivery`): beide Signaturen liegen am ERP,
|
||||||
|
State und Lieferzeitpunkt aber nicht gesetzt — Lieferung bleibt
|
||||||
|
`laufend`. Da der Endpoint atomar ist, gibt es keinen Zwischen-Zustand
|
||||||
|
"State gesetzt, Zeitstempel fehlt" oder umgekehrt.
|
||||||
|
- Schlechtes Netz / Funkloch beim Fahrer ist Realität → Wahrscheinlichkeit
|
||||||
|
ist nicht klein.
|
||||||
|
- Fahrer kann den Schritt blind wiederholen, ohne zu wissen, ob Signaturen
|
||||||
|
schon liegen → potenziell doppelte Bilddateien im DMS.
|
||||||
|
- Diagnose im Support ist mühsam, weil die Fehlermeldung nichts zur Stelle
|
||||||
|
sagt.
|
||||||
|
|
||||||
|
To-do (mittelfristig):
|
||||||
|
- Pro Repository-Schritt eine eigene, sprechende Fehlermeldung
|
||||||
|
("Fahrersignatur konnte nicht gespeichert werden", "Kundensignatur …",
|
||||||
|
"Lieferung konnte nicht als abgeschlossen markiert werden").
|
||||||
|
- Idempotenz prüfen: lassen sich die Schritte 1–6 ohne Doppel-Effekt
|
||||||
|
wiederholen? Falls ja, Retry-Strategie mit exponential backoff für
|
||||||
|
Netzfehler. Falls nein, mit Backend abstimmen.
|
||||||
|
- Server-Sicht "Wurde Schritt X für Lieferung Y schon erledigt?" einbauen,
|
||||||
|
damit ein Wiederaufnehmen nach App-Crash/Neustart möglich ist.
|
||||||
|
- Optional: Outbox-Pattern — Signaturen + Finish-Marker werden lokal
|
||||||
|
persistiert und im Hintergrund hochgeladen, statt blockierend im UI.
|
||||||
@ -20,7 +20,5 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
|
||||||
<string>13.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
997C0E4FB7B2C67AB8388B3F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB0CFA44E0F4A317CC3E8B41 /* Pods_RunnerTests.framework */; };
|
997C0E4FB7B2C67AB8388B3F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB0CFA44E0F4A317CC3E8B41 /* Pods_RunnerTests.framework */; };
|
||||||
CAFC9CC8D4DE6A37AF21BCD7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F2499FC75E94DB5A00A1507 /* Pods_Runner.framework */; };
|
A8B379B8CE90BAD414DCE8D3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F2499FC75E94DB5A00A1507 /* Pods_Runner.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -80,7 +80,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
CAFC9CC8D4DE6A37AF21BCD7 /* Pods_Runner.framework in Frameworks */,
|
A8B379B8CE90BAD414DCE8D3 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,12 +2,15 @@ import Flutter
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,19 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<!-- Kamera-Berechtigung -->
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<key>NSCameraUsageDescription</key>
|
<true/>
|
||||||
<string>Wir benötigen Zugriff auf deine Kamera zum Scannen von Barcodes.</string>
|
|
||||||
|
|
||||||
<!-- Weitere iOS-Einstellungen -->
|
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
|
||||||
<string>Diese App benötigt keinen Standortzugriff.</string>
|
|
||||||
<!-- GPS Permissions -->
|
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
|
||||||
<string>Diese App benötigt deinen Standort, um die Lieferdistanz zu berechnen.</string>
|
|
||||||
|
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
|
||||||
<string>Diese App benötigt deinen Standort, um die Lieferdistanz zu berechnen.</string>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@ -33,10 +22,50 @@
|
|||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>myapp</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Wir benötigen Zugriff auf deine Kamera zum Scannen von Barcodes.</string>
|
||||||
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
|
<string>Diese App benötigt deinen Standort, um die Lieferdistanz zu berechnen.</string>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>Diese App benötigt deinen Standort, um die Lieferdistanz zu berechnen.</string>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>FlutterSceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@ -54,21 +83,5 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Editor</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>myapp</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -5,10 +5,11 @@ part 'customer.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||||
class CustomerDTO {
|
class CustomerDTO {
|
||||||
CustomerDTO({required this.name, required this.address});
|
CustomerDTO({required this.name, required this.address, this.eMail});
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
AddressDTO address;
|
AddressDTO address;
|
||||||
|
String? eMail;
|
||||||
|
|
||||||
factory CustomerDTO.fromJson(Map<String, dynamic> json) => _$CustomerDTOFromJson(json);
|
factory CustomerDTO.fromJson(Map<String, dynamic> json) => _$CustomerDTOFromJson(json);
|
||||||
Map<dynamic, dynamic> toJson() => _$CustomerDTOToJson(this);
|
Map<dynamic, dynamic> toJson() => _$CustomerDTOToJson(this);
|
||||||
|
|||||||
@ -9,7 +9,12 @@ part of 'customer.dart';
|
|||||||
CustomerDTO _$CustomerDTOFromJson(Map<String, dynamic> json) => CustomerDTO(
|
CustomerDTO _$CustomerDTOFromJson(Map<String, dynamic> json) => CustomerDTO(
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
address: AddressDTO.fromJson(json['address'] as Map<String, dynamic>),
|
address: AddressDTO.fromJson(json['address'] as Map<String, dynamic>),
|
||||||
|
eMail: json['e_mail'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$CustomerDTOToJson(CustomerDTO instance) =>
|
Map<String, dynamic> _$CustomerDTOToJson(CustomerDTO instance) =>
|
||||||
<String, dynamic>{'name': instance.name, 'address': instance.address};
|
<String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'address': instance.address,
|
||||||
|
'e_mail': instance.eMail,
|
||||||
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:hl_lieferservice/model/delivery.dart';
|
import 'package:hl_lieferservice/model/delivery.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
part 'delivery_update.g.dart';
|
part 'delivery_update.g.dart';
|
||||||
@ -74,7 +75,9 @@ class DeliveryUpdateDTO {
|
|||||||
carId: delivery.carId?.toString() ,
|
carId: delivery.carId?.toString() ,
|
||||||
selectedPaymentMethodId: delivery.payment.id,
|
selectedPaymentMethodId: delivery.payment.id,
|
||||||
options: delivery.options.map(DeliveryOptionUpdateDTO.fromEntity).toList(),
|
options: delivery.options.map(DeliveryOptionUpdateDTO.fromEntity).toList(),
|
||||||
finishedDate: DateTime.now().millisecondsSinceEpoch.toString()
|
finishedDate: delivery.state == DeliveryState.finished
|
||||||
|
? DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now())
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,15 +15,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
: super(Unauthenticated()) {
|
: super(Unauthenticated()) {
|
||||||
on<SetAuthenticatedEvent>(_auth);
|
on<SetAuthenticatedEvent>(_auth);
|
||||||
on<Logout>(_logout);
|
on<Logout>(_logout);
|
||||||
|
on<SessionExpiredEvent>(_sessionExpired);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _auth(
|
Future<void> _auth(
|
||||||
SetAuthenticatedEvent event,
|
SetAuthenticatedEvent event,
|
||||||
Emitter<AuthState> emit,
|
Emitter<AuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
operationBloc.add(LoadOperation());
|
|
||||||
await Future.delayed(Duration(seconds: 5));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debugPrint("Retrieve user information");
|
debugPrint("Retrieve user information");
|
||||||
|
|
||||||
@ -31,7 +29,6 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
var state = Authenticated(sessionId: event.sessionId, user: response);
|
var state = Authenticated(sessionId: event.sessionId, user: response);
|
||||||
locator.registerSingleton<Authenticated>(state);
|
locator.registerSingleton<Authenticated>(state);
|
||||||
emit(state);
|
emit(state);
|
||||||
operationBloc.add(FinishOperation());
|
|
||||||
} catch (err, st) {
|
} catch (err, st) {
|
||||||
debugPrint("Failed to retrieve user information");
|
debugPrint("Failed to retrieve user information");
|
||||||
debugPrint(err.toString());
|
debugPrint(err.toString());
|
||||||
@ -46,6 +43,19 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _logout(Logout event, Emitter<AuthState> emit) async {
|
Future<void> _logout(Logout event, Emitter<AuthState> emit) async {
|
||||||
|
if (locator.isRegistered<Authenticated>()) {
|
||||||
|
locator.unregister<Authenticated>();
|
||||||
|
}
|
||||||
emit(Unauthenticated());
|
emit(Unauthenticated());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _sessionExpired(
|
||||||
|
SessionExpiredEvent event,
|
||||||
|
Emitter<AuthState> emit,
|
||||||
|
) async {
|
||||||
|
if (locator.isRegistered<Authenticated>()) {
|
||||||
|
locator.unregister<Authenticated>();
|
||||||
|
}
|
||||||
|
emit(Unauthenticated(sessionExpired: true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,3 +11,5 @@ class Logout extends AuthEvent {
|
|||||||
|
|
||||||
Logout({required this.username});
|
Logout({required this.username});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SessionExpiredEvent extends AuthEvent {}
|
||||||
@ -2,7 +2,11 @@ import 'package:hl_lieferservice/feature/authentication/model/user.dart';
|
|||||||
|
|
||||||
abstract class AuthState {}
|
abstract class AuthState {}
|
||||||
|
|
||||||
class Unauthenticated extends AuthState {}
|
class Unauthenticated extends AuthState {
|
||||||
|
final bool sessionExpired;
|
||||||
|
Unauthenticated({this.sessionExpired = false});
|
||||||
|
}
|
||||||
|
|
||||||
class Authenticated extends AuthState {
|
class Authenticated extends AuthState {
|
||||||
User user;
|
User user;
|
||||||
String sessionId;
|
String sessionId;
|
||||||
|
|||||||
@ -18,7 +18,8 @@ class LoginEnforcer extends StatelessWidget {
|
|||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
return LoginPage();
|
final expired = state is Unauthenticated && state.sessionExpired;
|
||||||
|
return LoginPage(sessionExpired: expired);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,9 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({super.key});
|
final bool sessionExpired;
|
||||||
|
|
||||||
|
const LoginPage({super.key, this.sessionExpired = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _LoginPageState();
|
State<StatefulWidget> createState() => _LoginPageState();
|
||||||
@ -60,7 +62,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
|
|
||||||
debugPrint("🔵 Opening browser to: http://localhost:3000/login");
|
debugPrint("🔵 Opening browser to: http://localhost:3000/login");
|
||||||
|
|
||||||
final loginUrl = Uri.parse('http://100.72.100.33:3000/login');
|
final loginUrl = Uri.parse('http://192.168.1.9:3000/login');
|
||||||
final launched = await launchUrl(
|
final launched = await launchUrl(
|
||||||
loginUrl,
|
loginUrl,
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
@ -127,7 +129,21 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(),
|
appBar: AppBar(),
|
||||||
body: Center(
|
body: Column(
|
||||||
|
children: [
|
||||||
|
if (widget.sessionExpired)
|
||||||
|
MaterialBanner(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
content: const Text(
|
||||||
|
"Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.",
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.orange.shade800,
|
||||||
|
leading: const Icon(Icons.warning_amber_rounded, color: Colors.white),
|
||||||
|
actions: [const SizedBox.shrink()],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -180,6 +196,9 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/model/selection.dart';
|
||||||
|
import 'package:hl_lieferservice/model/car.dart';
|
||||||
|
|
||||||
|
import 'events.dart';
|
||||||
|
import 'state.dart';
|
||||||
|
|
||||||
|
class CarSelectBloc extends Bloc<CarSelectEvent, CarSelectState> {
|
||||||
|
final CarSelectionRepository repository;
|
||||||
|
|
||||||
|
CarSelectBloc({required this.repository}) : super(CarSelectInitial()) {
|
||||||
|
on<CarSelectLoad>(_load);
|
||||||
|
on<CarSelectConfirm>(_confirm);
|
||||||
|
on<CarSelectChange>(_change);
|
||||||
|
on<CarSelectCancel>(_cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load(
|
||||||
|
CarSelectLoad event,
|
||||||
|
Emitter<CarSelectState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
emit(CarSelectLoading());
|
||||||
|
|
||||||
|
final CarSelection? stored = await repository.getSelection();
|
||||||
|
final today = DateTime.now();
|
||||||
|
|
||||||
|
final bool validForToday =
|
||||||
|
stored != null &&
|
||||||
|
stored.selectedCarId != null &&
|
||||||
|
stored.selectedCarPlate != null &&
|
||||||
|
stored.date.year == today.year &&
|
||||||
|
stored.date.month == today.month &&
|
||||||
|
stored.date.day == today.day;
|
||||||
|
|
||||||
|
if (validForToday) {
|
||||||
|
emit(
|
||||||
|
CarSelectComplete(
|
||||||
|
selectedCar: Car(
|
||||||
|
id: stored.selectedCarId!,
|
||||||
|
plate: stored.selectedCarPlate!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(CarSelectRequired());
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('CarSelectBloc._load failed: $e');
|
||||||
|
debugPrint('Stacktrace: $st');
|
||||||
|
emit(CarSelectFailed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _change(CarSelectChange event, Emitter<CarSelectState> emit) {
|
||||||
|
final previousCar =
|
||||||
|
state is CarSelectComplete ? (state as CarSelectComplete).selectedCar : null;
|
||||||
|
emit(CarSelectRequired(previousCar: previousCar));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancel(CarSelectCancel event, Emitter<CarSelectState> emit) {
|
||||||
|
// Restore without touching SharedPreferences — no tour reload needed.
|
||||||
|
emit(CarSelectComplete(selectedCar: event.car));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirm(
|
||||||
|
CarSelectConfirm event,
|
||||||
|
Emitter<CarSelectState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final today = DateTime.now();
|
||||||
|
await repository.saveSelection(
|
||||||
|
CarSelection(
|
||||||
|
date: today,
|
||||||
|
selectedCarId: event.car.id,
|
||||||
|
selectedCarPlate: event.car.plate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
emit(CarSelectComplete(selectedCar: event.car));
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('CarSelectBloc._confirm failed: $e');
|
||||||
|
debugPrint('Stacktrace: $st');
|
||||||
|
emit(CarSelectFailed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:hl_lieferservice/model/car.dart';
|
||||||
|
|
||||||
|
abstract class CarSelectEvent {}
|
||||||
|
|
||||||
|
/// Fired at app startup to check if a car has already been selected for today.
|
||||||
|
class CarSelectLoad extends CarSelectEvent {}
|
||||||
|
|
||||||
|
/// Fired when the driver confirms their car choice for the day.
|
||||||
|
class CarSelectConfirm extends CarSelectEvent {
|
||||||
|
final Car car;
|
||||||
|
|
||||||
|
CarSelectConfirm({required this.car});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fired when the driver wants to switch to a different car.
|
||||||
|
/// Resets the selection so the enforcer shows the picker again.
|
||||||
|
class CarSelectChange extends CarSelectEvent {}
|
||||||
|
|
||||||
|
/// Fired when the driver cancels the change and wants to keep the previous car.
|
||||||
|
/// Restores [CarSelectComplete] without writing to SharedPreferences.
|
||||||
|
class CarSelectCancel extends CarSelectEvent {
|
||||||
|
final Car car;
|
||||||
|
|
||||||
|
CarSelectCancel({required this.car});
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:hl_lieferservice/model/car.dart';
|
||||||
|
|
||||||
|
abstract class CarSelectState {}
|
||||||
|
|
||||||
|
class CarSelectInitial extends CarSelectState {}
|
||||||
|
|
||||||
|
class CarSelectLoading extends CarSelectState {}
|
||||||
|
|
||||||
|
/// No valid car selection exists for today — the driver must choose.
|
||||||
|
/// [previousCar] is set when the driver triggered a manual change,
|
||||||
|
/// allowing the page to pre-highlight the current car and offer a cancel.
|
||||||
|
class CarSelectRequired extends CarSelectState {
|
||||||
|
final Car? previousCar;
|
||||||
|
|
||||||
|
CarSelectRequired({this.previousCar});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A car has been selected for today. The selection is persisted locally.
|
||||||
|
class CarSelectComplete extends CarSelectState {
|
||||||
|
final Car selectedCar;
|
||||||
|
|
||||||
|
CarSelectComplete({required this.selectedCar});
|
||||||
|
}
|
||||||
|
|
||||||
|
class CarSelectFailed extends CarSelectState {}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hl_lieferservice/model/car.dart';
|
||||||
|
|
||||||
|
class CarSelectionCard extends StatelessWidget {
|
||||||
|
final Car car;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const CarSelectionCard({
|
||||||
|
super.key,
|
||||||
|
required this.car,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = Theme.of(context).primaryColor;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side: isSelected
|
||||||
|
? BorderSide(color: color, width: 2)
|
||||||
|
: BorderSide.none,
|
||||||
|
),
|
||||||
|
color: isSelected
|
||||||
|
? color.withValues(alpha: 0.08)
|
||||||
|
: null,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.local_shipping,
|
||||||
|
size: 32,
|
||||||
|
color: isSelected ? color : Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
car.plate,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
Icon(Icons.check_circle, color: color),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/events.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/presentation/car_selection_page.dart';
|
||||||
|
|
||||||
|
class CarSelectionEnforcer extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const CarSelectionEnforcer({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CarSelectionEnforcer> createState() => _CarSelectionEnforcerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CarSelectionEnforcerState extends State<CarSelectionEnforcer> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<CarSelectBloc>().add(CarSelectLoad());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<CarSelectBloc, CarSelectState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// Show a full-screen spinner only while the persisted selection is
|
||||||
|
// being read from SharedPreferences (at most one frame on cold start).
|
||||||
|
if (state is CarSelectInitial || state is CarSelectLoading) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is CarSelectFailed) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 72,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
"Fehler beim Laden der Fahrzeugauswahl.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () =>
|
||||||
|
context.read<CarSelectBloc>().add(CarSelectLoad()),
|
||||||
|
child: const Text("Erneut versuchen"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For both CarSelectRequired and CarSelectComplete, keep Home alive
|
||||||
|
// in the widget tree so initState is never re-triggered. The selection
|
||||||
|
// page is overlaid on top when a (re-)selection is required.
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
widget.child,
|
||||||
|
if (state is CarSelectRequired)
|
||||||
|
Positioned.fill(
|
||||||
|
child: CarSelectionPage(previousCar: state.previousCar),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
232
lib/feature/car_selection/presentation/car_selection_page.dart
Normal file
232
lib/feature/car_selection/presentation/car_selection_page.dart
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/events.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/presentation/car_selection_card.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_event.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/presentation/car_dialog.dart';
|
||||||
|
import 'package:hl_lieferservice/model/car.dart';
|
||||||
|
|
||||||
|
class CarSelectionPage extends StatefulWidget {
|
||||||
|
/// When set, the page is in "change" mode: the car is pre-highlighted
|
||||||
|
/// and a cancel button is shown to revert without choosing a new car.
|
||||||
|
final Car? previousCar;
|
||||||
|
|
||||||
|
const CarSelectionPage({super.key, this.previousCar});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CarSelectionPage> createState() => _CarSelectionPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CarSelectionPageState extends State<CarSelectionPage> {
|
||||||
|
Car? _selectedCar;
|
||||||
|
|
||||||
|
bool get _isChanging => widget.previousCar != null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedCar = widget.previousCar;
|
||||||
|
final authState = context.read<AuthBloc>().state as Authenticated;
|
||||||
|
context.read<CarsBloc>().add(CarLoad(teamId: authState.user.number));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAddCar() {
|
||||||
|
final authState = context.read<AuthBloc>().state as Authenticated;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => CarDialog(
|
||||||
|
onAction: (plate) {
|
||||||
|
context.read<CarsBloc>().add(
|
||||||
|
CarAdd(teamId: authState.user.number, plate: plate),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onConfirm() {
|
||||||
|
if (_selectedCar == null) return;
|
||||||
|
context.read<CarSelectBloc>().add(CarSelectConfirm(car: _selectedCar!));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCarList(List<Car> cars) {
|
||||||
|
if (cars.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.local_shipping_outlined, size: 72, color: Colors.grey),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
"Noch kein Fahrzeug vorhanden.",
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"Füge zuerst ein Fahrzeug hinzu, bevor du fortfahren kannst.",
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _onAddCar,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text("Fahrzeug hinzufügen"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final authState = context.read<AuthBloc>().state as Authenticated;
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
context.read<CarsBloc>().add(
|
||||||
|
CarLoad(teamId: authState.user.number, force: true),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
itemCount: cars.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final car = cars[index];
|
||||||
|
return CarSelectionCard(
|
||||||
|
car: car,
|
||||||
|
isSelected: _selectedCar?.id == car.id,
|
||||||
|
onTap: () => setState(() => _selectedCar = car),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<CarSelectBloc, CarSelectState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is CarSelectFailed) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("Fehler beim Speichern der Fahrzeugauswahl."),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: _isChanging
|
||||||
|
? AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => context.read<CarSelectBloc>().add(
|
||||||
|
CarSelectCancel(car: widget.previousCar!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: const Text("Fahrzeug wechseln"),
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (!_isChanging) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 4),
|
||||||
|
child: Text(
|
||||||
|
"Fahrzeug auswählen",
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 4, 20, 16),
|
||||||
|
child: Text(
|
||||||
|
"Wähle das Fahrzeug aus, das du heute verwendest.",
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<CarsBloc, CarsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is CarsLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is CarsLoaded) {
|
||||||
|
return _buildCarList(state.cars);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state is CarsLoadingFailed) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 72,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
"Fehler beim Laden der Fahrzeuge.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
final authState =
|
||||||
|
context.read<AuthBloc>().state
|
||||||
|
as Authenticated;
|
||||||
|
context.read<CarsBloc>().add(
|
||||||
|
CarLoad(
|
||||||
|
teamId: authState.user.number,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text("Erneut versuchen"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: _selectedCar != null ? _onConfirm : null,
|
||||||
|
child: const Text("Auswählen"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/feature/car_selection/presentation/selected_car_bar.dart
Normal file
56
lib/feature/car_selection/presentation/selected_car_bar.dart
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/events.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
|
|
||||||
|
class SelectedCarBar extends StatelessWidget {
|
||||||
|
const SelectedCarBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<CarSelectBloc, CarSelectState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is! CarSelectComplete) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Divider(height: 1, thickness: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.local_shipping,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
state.selectedCar.plate,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () =>
|
||||||
|
context.read<CarSelectBloc>().add(CarSelectChange()),
|
||||||
|
icon: const Icon(Icons.swap_horiz, size: 18),
|
||||||
|
label: const Text("Wechseln"),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:hl_lieferservice/feature/cars/model/selection.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class CarSelectionRepository {
|
||||||
|
static const _keyDate = 'car_selection_date';
|
||||||
|
static const _keyCarId = 'car_selection_car_id';
|
||||||
|
static const _keyCarPlate = 'car_selection_car_plate';
|
||||||
|
|
||||||
|
/// Returns the stored [CarSelection], or null if nothing has been saved yet.
|
||||||
|
Future<CarSelection?> getSelection() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
final dateString = prefs.getString(_keyDate);
|
||||||
|
final carId = prefs.getInt(_keyCarId);
|
||||||
|
final plate = prefs.getString(_keyCarPlate);
|
||||||
|
|
||||||
|
if (dateString == null || carId == null || plate == null) return null;
|
||||||
|
|
||||||
|
return CarSelection(
|
||||||
|
date: DateTime.parse(dateString),
|
||||||
|
selectedCarId: carId,
|
||||||
|
selectedCarPlate: plate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists the given [selection] locally on this device.
|
||||||
|
Future<void> saveSelection(CarSelection selection) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
await prefs.setString(_keyDate, selection.date.toIso8601String());
|
||||||
|
await prefs.setInt(_keyCarId, selection.selectedCarId!);
|
||||||
|
await prefs.setString(_keyCarPlate, selection.selectedCarPlate!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,7 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/exceptions.dart';
|
||||||
import 'package:hl_lieferservice/feature/cars/repository/cars_repository.dart';
|
import 'package:hl_lieferservice/feature/cars/repository/cars_repository.dart';
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
||||||
@ -10,8 +13,9 @@ import 'cars_state.dart';
|
|||||||
class CarsBloc extends Bloc<CarEvents, CarsState> {
|
class CarsBloc extends Bloc<CarEvents, CarsState> {
|
||||||
CarsRepository repository;
|
CarsRepository repository;
|
||||||
OperationBloc opBloc;
|
OperationBloc opBloc;
|
||||||
|
AuthBloc authBloc;
|
||||||
|
|
||||||
CarsBloc({required this.repository, required this.opBloc})
|
CarsBloc({required this.repository, required this.opBloc, required this.authBloc})
|
||||||
: super(CarsInitial()) {
|
: super(CarsInitial()) {
|
||||||
on<CarAdd>(_carAdd);
|
on<CarAdd>(_carAdd);
|
||||||
on<CarEdit>(_carEdit);
|
on<CarEdit>(_carEdit);
|
||||||
@ -19,12 +23,27 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
on<CarLoad>(_carLoad);
|
on<CarLoad>(_carLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleError(Object e, String fallbackMessage) {
|
||||||
|
if (e is UserUnauthorized) {
|
||||||
|
authBloc.add(SessionExpiredEvent());
|
||||||
|
} else {
|
||||||
|
opBloc.add(FailOperation(message: fallbackMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _carLoad(CarLoad event, Emitter<CarsState> emit) async {
|
Future<void> _carLoad(CarLoad event, Emitter<CarsState> emit) async {
|
||||||
|
// Skip the API call if cars are already loaded and no force-refresh requested.
|
||||||
|
if (state is CarsLoaded && !event.force) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
emit(CarsLoading());
|
emit(CarsLoading());
|
||||||
List<Car> cars = await repository.getAll(event.teamId);
|
List<Car> cars = await repository.getAll(event.teamId);
|
||||||
emit(CarsLoaded(cars: cars, teamId: event.teamId));
|
emit(CarsLoaded(cars: cars, teamId: event.teamId));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e is UserUnauthorized) {
|
||||||
|
authBloc.add(SessionExpiredEvent());
|
||||||
|
return;
|
||||||
|
}
|
||||||
emit(CarsLoadingFailed());
|
emit(CarsLoadingFailed());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +52,6 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
Car newCar = await repository.add(event.teamId, event.plate);
|
Car newCar = await repository.add(event.teamId, event.plate);
|
||||||
|
|
||||||
if (currentState is CarsLoaded) {
|
if (currentState is CarsLoaded) {
|
||||||
@ -46,7 +64,7 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
|
|
||||||
opBloc.add(FinishOperation(message: "Auto erfolgreich hinzugefügt"));
|
opBloc.add(FinishOperation(message: "Auto erfolgreich hinzugefügt"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
opBloc.add(FailOperation(message: "Fehler beim Hinzufügen eines Autos"));
|
_handleError(e, "Fehler beim Hinzufügen eines Autos");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +72,6 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
await repository.edit(event.teamId, event.newCar);
|
await repository.edit(event.teamId, event.newCar);
|
||||||
|
|
||||||
if (currentState is CarsLoaded) {
|
if (currentState is CarsLoaded) {
|
||||||
@ -74,7 +91,7 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
|
|
||||||
opBloc.add(FinishOperation(message: "Auto erfolgreich editiert"));
|
opBloc.add(FinishOperation(message: "Auto erfolgreich editiert"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
opBloc.add(FailOperation(message: "Fehler beim Editieren des Autos"));
|
_handleError(e, "Fehler beim Editieren des Autos");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +99,6 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
await repository.delete(event.carId, event.teamId);
|
await repository.delete(event.carId, event.teamId);
|
||||||
|
|
||||||
if (currentState is CarsLoaded) {
|
if (currentState is CarsLoaded) {
|
||||||
@ -100,7 +116,7 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
|
|
||||||
opBloc.add(FinishOperation(message: "Auto erfolgreich gelöscht"));
|
opBloc.add(FinishOperation(message: "Auto erfolgreich gelöscht"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
opBloc.add(FailOperation(message: "Fehler beim Löschen des Autos"));
|
_handleError(e, "Fehler beim Löschen des Autos");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,11 @@ abstract class CarEvents {}
|
|||||||
class CarLoad extends CarEvents {
|
class CarLoad extends CarEvents {
|
||||||
String teamId;
|
String teamId;
|
||||||
|
|
||||||
CarLoad({required this.teamId});
|
/// If [force] is true the API is always called, bypassing the cache.
|
||||||
|
/// Use this for pull-to-refresh. Defaults to false.
|
||||||
|
bool force;
|
||||||
|
|
||||||
|
CarLoad({required this.teamId, this.force = false});
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarEdit extends CarEvents {
|
class CarEdit extends CarEvents {
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Settings for the driver to select a car for the current workday.
|
||||||
|
*/
|
||||||
|
class CarSelection {
|
||||||
|
final DateTime date;
|
||||||
|
final int? selectedCarId;
|
||||||
|
final String? selectedCarPlate;
|
||||||
|
|
||||||
|
CarSelection({
|
||||||
|
required this.date,
|
||||||
|
this.selectedCarId,
|
||||||
|
this.selectedCarPlate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'car_dialog.dart';
|
|||||||
|
|
||||||
class CarCard extends StatelessWidget {
|
class CarCard extends StatelessWidget {
|
||||||
final Car car;
|
final Car car;
|
||||||
|
final bool isSelected;
|
||||||
final Function(Car car) onDelete;
|
final Function(Car car) onDelete;
|
||||||
final Function(Car car, String newName) onEdit;
|
final Function(Car car, String newName) onEdit;
|
||||||
|
|
||||||
@ -13,11 +14,20 @@ class CarCard extends StatelessWidget {
|
|||||||
required this.car,
|
required this.car,
|
||||||
required this.onEdit,
|
required this.onEdit,
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
|
this.isSelected = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final primary = Theme.of(context).primaryColor;
|
||||||
return Card(
|
return Card(
|
||||||
|
color: isSelected ? primary.withValues(alpha: 0.08) : null,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: isSelected
|
||||||
|
? BorderSide(color: primary, width: 2)
|
||||||
|
: BorderSide.none,
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -30,12 +40,29 @@ class CarCard extends StatelessWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.local_shipping,
|
Icons.local_shipping,
|
||||||
size: 32,
|
size: 32,
|
||||||
color: Theme.of(context).primaryColor,
|
color: primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 10),
|
padding: const EdgeInsets.only(left: 10),
|
||||||
child: Text(car.plate),
|
child: Text(
|
||||||
|
car.plate,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isSelected
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSelected)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
size: 20,
|
||||||
|
color: primary,
|
||||||
|
semanticLabel: 'Aktuell ausgewählt',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class CarManagementOverview extends StatefulWidget {
|
class CarManagementOverview extends StatefulWidget {
|
||||||
final List<Car> cars;
|
final List<Car> cars;
|
||||||
|
final int? selectedCarId;
|
||||||
final Function(String plate) onAdd;
|
final Function(String plate) onAdd;
|
||||||
final Function(String id) onDelete;
|
final Function(String id) onDelete;
|
||||||
final Function(String id, String plate) onEdit;
|
final Function(String id, String plate) onEdit;
|
||||||
|
final Future<void> Function() onRefresh;
|
||||||
|
|
||||||
const CarManagementOverview({
|
const CarManagementOverview({
|
||||||
super.key,
|
super.key,
|
||||||
@ -16,6 +18,8 @@ class CarManagementOverview extends StatefulWidget {
|
|||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
required this.onEdit,
|
required this.onEdit,
|
||||||
required this.onAdd,
|
required this.onAdd,
|
||||||
|
required this.onRefresh,
|
||||||
|
this.selectedCarId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,30 +44,14 @@ class _CarManagementOverviewState extends State<CarManagementOverview> {
|
|||||||
widget.onEdit(car.id.toString(), newName);
|
widget.onEdit(car.id.toString(), newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCarOverview() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(padding: const EdgeInsets.all(15), child: Text("Fahrzeuge", style: Theme.of(context).textTheme.headlineSmall),),
|
|
||||||
Expanded(child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: widget.cars.isEmpty ? const Center(child: Text("keine Fahrzeuge vorhanden")) : ListView.builder(
|
|
||||||
itemBuilder:
|
|
||||||
(context, index) => CarCard(
|
|
||||||
car: widget.cars[index],
|
|
||||||
onEdit: _editCar,
|
|
||||||
onDelete: _removeCar,
|
|
||||||
),
|
|
||||||
itemCount: widget.cars.length,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Fahrzeuge"),
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: _addCar,
|
onPressed: _addCar,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
@ -72,7 +60,34 @@ class _CarManagementOverviewState extends State<CarManagementOverview> {
|
|||||||
color: Theme.of(context).colorScheme.onSecondary,
|
color: Theme.of(context).colorScheme.onSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: _buildCarOverview(),
|
body: RefreshIndicator(
|
||||||
|
onRefresh: widget.onRefresh,
|
||||||
|
child: widget.cars.isEmpty
|
||||||
|
? ListView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
children: const [
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: Center(child: Text("keine Fahrzeuge vorhanden")),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
itemCount: widget.cars.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final car = widget.cars[index];
|
||||||
|
return CarCard(
|
||||||
|
car: car,
|
||||||
|
isSelected: widget.selectedCarId == car.id,
|
||||||
|
onEdit: _editCar,
|
||||||
|
onDelete: _removeCar,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart';
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_event.dart';
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_event.dart';
|
||||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_state.dart';
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_state.dart';
|
||||||
@ -37,7 +39,55 @@ class _CarManagementPageState extends State<CarManagementPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _refresh() async {
|
||||||
|
context.read<CarsBloc>().add(CarLoad(teamId: _authState.user.number, force: true));
|
||||||
|
}
|
||||||
|
|
||||||
void _remove(String id) {
|
void _remove(String id) {
|
||||||
|
final carId = int.parse(id);
|
||||||
|
|
||||||
|
final carSelectState = context.read<CarSelectBloc>().state;
|
||||||
|
if (carSelectState is CarSelectComplete &&
|
||||||
|
carSelectState.selectedCar.id == carId) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Dieses Fahrzeug ist aktuell ausgewählt und kann nicht gelöscht werden. "
|
||||||
|
"Bitte wähle zuerst ein anderes Fahrzeug aus.",
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tourState = context.read<TourBloc>().state;
|
||||||
|
if (tourState is! TourLoaded) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Die Tourdaten sind noch nicht verfügbar. "
|
||||||
|
"Bitte versuche es in Kürze erneut.",
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tourState.tour.hasUndeliveredLoadedArticles(carId)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Dieses Fahrzeug hat noch geladene Artikel, die nicht ausgeliefert wurden. "
|
||||||
|
"Bitte schließe alle offenen Lieferungen ab, bevor du das Fahrzeug löschst.",
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
context.read<CarsBloc>().add(
|
context.read<CarsBloc>().add(
|
||||||
CarDelete(carId: id, teamId: _authState.user.number),
|
CarDelete(carId: id, teamId: _authState.user.number),
|
||||||
);
|
);
|
||||||
@ -68,11 +118,20 @@ class _CarManagementPageState extends State<CarManagementPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state is CarsLoaded) {
|
if (state is CarsLoaded) {
|
||||||
|
return BlocBuilder<CarSelectBloc, CarSelectState>(
|
||||||
|
builder: (context, selectState) {
|
||||||
|
final int? selectedCarId = selectState is CarSelectComplete
|
||||||
|
? selectState.selectedCar.id
|
||||||
|
: null;
|
||||||
return CarManagementOverview(
|
return CarManagementOverview(
|
||||||
cars: state.cars,
|
cars: state.cars,
|
||||||
|
selectedCarId: selectedCarId,
|
||||||
onEdit: _edit,
|
onEdit: _edit,
|
||||||
onAdd: _add,
|
onAdd: _add,
|
||||||
onDelete: _remove,
|
onDelete: _remove,
|
||||||
|
onRefresh: _refresh,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,16 +9,20 @@ import 'package:hl_lieferservice/feature/delivery/overview/service/distance_serv
|
|||||||
import 'package:hl_lieferservice/feature/delivery/overview/service/reorder_service.dart';
|
import 'package:hl_lieferservice/feature/delivery/overview/service/reorder_service.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/util.dart';
|
import 'package:hl_lieferservice/feature/delivery/util.dart';
|
||||||
import 'package:hl_lieferservice/model/tour.dart';
|
import 'package:hl_lieferservice/model/tour.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/exceptions.dart';
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
class TourBloc extends Bloc<TourEvent, TourState> {
|
class TourBloc extends Bloc<TourEvent, TourState> {
|
||||||
OperationBloc opBloc;
|
OperationBloc opBloc;
|
||||||
|
AuthBloc authBloc;
|
||||||
TourRepository tourRepository;
|
TourRepository tourRepository;
|
||||||
StreamSubscription? _combinedSubscription;
|
StreamSubscription? _combinedSubscription;
|
||||||
|
|
||||||
TourBloc({required this.opBloc, required this.tourRepository})
|
TourBloc({required this.opBloc, required this.authBloc, required this.tourRepository})
|
||||||
: super(TourInitial()) {
|
: super(TourInitial()) {
|
||||||
_combinedSubscription = CombineLatestStream.combine2(
|
_combinedSubscription = CombineLatestStream.combine2(
|
||||||
tourRepository.tour,
|
tourRepository.tour,
|
||||||
@ -61,17 +65,23 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_combinedSubscription?.cancel();
|
_combinedSubscription?.cancel();
|
||||||
|
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleError(Object e, String fallbackMessage) {
|
||||||
|
if (e is UserUnauthorized) {
|
||||||
|
authBloc.add(SessionExpiredEvent());
|
||||||
|
} else {
|
||||||
|
opBloc.add(FailOperation(message: fallbackMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _setArticleAmount(
|
void _setArticleAmount(
|
||||||
SetArticleAmountEvent event,
|
SetArticleAmountEvent event,
|
||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.setArticleAmount(
|
await tourRepository.setArticleAmount(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
@ -79,15 +89,9 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
event.amount,
|
event.amount,
|
||||||
event.reason,
|
event.reason,
|
||||||
);
|
);
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
opBloc.add(
|
debugPrint("$e $st");
|
||||||
FailOperation(message: "Fehler beim Ändern der Menge des Artikels"),
|
_handleError(e, "Fehler beim Ändern der Menge des Artikels");
|
||||||
);
|
|
||||||
|
|
||||||
debugPrint("$e");
|
|
||||||
debugPrint("$st");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,7 +130,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
Map<String, double> distances = {};
|
Map<String, double> distances = {};
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
emit(TourRequestingDistances(tour: event.tour, payments: event.payments));
|
emit(TourRequestingDistances(tour: event.tour, payments: event.payments));
|
||||||
|
|
||||||
@ -135,7 +138,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
distances[delivery.id] = await DistanceService.getDistanceByRoad(
|
distances[delivery.id] = await DistanceService.getDistanceByRoad(
|
||||||
delivery.customer.address.toString(),
|
delivery.customer.address.toString(),
|
||||||
);
|
);
|
||||||
} catch (e,st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Laden der Distanz: $e");
|
debugPrint("Fehler beim Laden der Distanz: $e");
|
||||||
debugPrint("$st");
|
debugPrint("$st");
|
||||||
|
|
||||||
@ -145,7 +148,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
// If an error occurred, then the distances will be empty
|
// If an error occurred, then the distances will be empty
|
||||||
// If the distances are empty then they shouldn't be displayed
|
// If the distances are empty then they shouldn't be displayed
|
||||||
add(
|
add(
|
||||||
@ -251,17 +253,11 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
) async {
|
) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.reactivateDelivery(event.deliveryId);
|
await tourRepository.reactivateDelivery(event.deliveryId);
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e");
|
debugPrint("$e $st");
|
||||||
debugPrint("$st");
|
_handleError(e, "Fehler beim Zurückstellen der Lieferung");
|
||||||
opBloc.add(
|
|
||||||
FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -269,17 +265,11 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
void _holdDelivery(HoldDeliveryEvent event, Emitter<TourState> emit) async {
|
void _holdDelivery(HoldDeliveryEvent event, Emitter<TourState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.holdDelivery(event.deliveryId);
|
await tourRepository.holdDelivery(event.deliveryId);
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e");
|
debugPrint("$e $st");
|
||||||
debugPrint("$st");
|
_handleError(e, "Fehler beim Zurückstellen der Lieferung");
|
||||||
opBloc.add(
|
|
||||||
FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -290,24 +280,17 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
) async {
|
) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.cancelDelivery(event.deliveryId);
|
await tourRepository.cancelDelivery(event.deliveryId);
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e");
|
debugPrint("$e $st");
|
||||||
debugPrint("$st");
|
_handleError(e, "Fehler beim Stornieren der Lieferung");
|
||||||
opBloc.add(
|
|
||||||
FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scan(ScanArticleEvent event, Emitter<TourState> emit) async {
|
void _scan(ScanArticleEvent event, Emitter<TourState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
try {
|
try {
|
||||||
@ -333,9 +316,8 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("FEHLER beim Scannen eines Artikels: $e");
|
debugPrint("FEHLER beim Scannen eines Artikels: $e $st");
|
||||||
debugPrint("$st");
|
_handleError(e, "Fehler beim Scannen des Artikels");
|
||||||
opBloc.add(FailOperation(message: "Fehler beim Scannen des Artikels"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -347,17 +329,15 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.scanArticle(
|
await tourRepository.scanArticle(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
event.carId,
|
event.carId,
|
||||||
event.internalArticleId,
|
event.internalArticleId,
|
||||||
);
|
);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint(st.toString());
|
debugPrint("$e $st");
|
||||||
opBloc.add(FailOperation(message: "Fehler beim Scannen des Artikels"));
|
_handleError(e, "Fehler beim Scannen des Artikels");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,34 +345,27 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
Future<void> _assignCar(AssignCarEvent event, Emitter<TourState> emit) async {
|
Future<void> _assignCar(AssignCarEvent event, Emitter<TourState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.assignCar(event.deliveryId, event.carId);
|
await tourRepository.assignCar(event.deliveryId, event.carId);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint(st.toString());
|
debugPrint("$e $st");
|
||||||
opBloc.add(
|
_handleError(e, "Fehler beim Zuweisen des Fahrzeugs");
|
||||||
FailOperation(message: "Fehler beim Zuweisen des Fahrzeugs"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _load(LoadTour event, Emitter<TourState> emit) async {
|
Future<void> _load(LoadTour event, Emitter<TourState> emit) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
try {
|
try {
|
||||||
emit(TourLoading());
|
emit(TourLoading());
|
||||||
await tourRepository.loadTourOfToday(event.teamId);
|
await tourRepository.loadTourOfToday(event.teamId);
|
||||||
await tourRepository.loadPaymentOptions();
|
await tourRepository.loadPaymentOptions();
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// go to the error state in order to give the user the chance
|
if (e is UserUnauthorized) {
|
||||||
// to reload if necessary.
|
authBloc.add(SessionExpiredEvent());
|
||||||
|
return;
|
||||||
|
}
|
||||||
emit(TourLoadingFailed());
|
emit(TourLoadingFailed());
|
||||||
opBloc.add(
|
opBloc.add(FailOperation(message: "Fehler beim Laden der heutigen Fahrten"));
|
||||||
FailOperation(message: "Fehler beim Laden der heutigen Fahrten"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,7 +374,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
try {
|
try {
|
||||||
@ -415,11 +387,9 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await tourRepository.finishDelivery(event.deliveryId);
|
await tourRepository.finishDelivery(event.deliveryId);
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
opBloc.add(FailOperation(message: "Failed to update delivery"));
|
debugPrint("$e $st");
|
||||||
debugPrint(st.toString());
|
_handleError(e, "Fehler beim Abschließen der Lieferung");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -429,14 +399,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
await tourRepository.updatePayment(event.deliveryId, event.payment);
|
await tourRepository.updatePayment(event.deliveryId, event.payment);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint(st.toString());
|
debugPrint("$e $st");
|
||||||
opBloc.add(
|
_handleError(e, "Fehler beim Aktualisieren des Betrags");
|
||||||
FailOperation(message: "Fehler beim Aktualisieren des Betrags"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,18 +411,14 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
await tourRepository.updateOption(
|
await tourRepository.updateOption(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
event.key,
|
event.key,
|
||||||
event.value,
|
event.value,
|
||||||
);
|
);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$st");
|
debugPrint("$e $st");
|
||||||
opBloc.add(
|
_handleError(e, "Fehler beim Aktualisieren der Optionen");
|
||||||
FailOperation(message: "Fehler beim Aktualisieren der Optionen"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,26 +426,15 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
UpdateDiscountEvent event,
|
UpdateDiscountEvent event,
|
||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
await tourRepository.updateDiscount(
|
await tourRepository.updateDiscount(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
event.reason,
|
event.reason,
|
||||||
event.value,
|
event.value,
|
||||||
);
|
);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint(
|
debugPrint("Fehler beim Aktualisieren des Discounts: $e $st");
|
||||||
"Fehler beim Hinzufügen eins Discounts zur Lieferung: ${event.deliveryId}:",
|
_handleError(e, "Fehler beim Aktualisieren des Discounts");
|
||||||
);
|
|
||||||
debugPrint("$e");
|
|
||||||
debugPrint("$st");
|
|
||||||
|
|
||||||
opBloc.add(
|
|
||||||
FailOperation(message: "Fehler beim Hinzufügen des Discounts: $e"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,51 +442,28 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
RemoveDiscountEvent event,
|
RemoveDiscountEvent event,
|
||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.removeDiscount(event.deliveryId);
|
await tourRepository.removeDiscount(event.deliveryId);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint(
|
debugPrint("Fehler beim Löschen des Discounts: $e $st");
|
||||||
"Fehler beim Löschen des Discounts der Lieferung: ${event.deliveryId}:",
|
_handleError(e, "Fehler beim Löschen des Discounts");
|
||||||
);
|
|
||||||
debugPrint("$e");
|
|
||||||
debugPrint("$st");
|
|
||||||
|
|
||||||
opBloc.add(
|
|
||||||
FailOperation(message: "Fehler beim Löschen des Discounts: $e"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addDiscount(AddDiscountEvent event, Emitter<TourState> emit) async {
|
void _addDiscount(AddDiscountEvent event, Emitter<TourState> emit) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.addDiscount(
|
await tourRepository.addDiscount(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
event.reason,
|
event.reason,
|
||||||
event.value,
|
event.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint(
|
debugPrint("Fehler beim Hinzufügen des Discounts: $e $st");
|
||||||
"Fehler beim Hinzufügen eins Discounts zur Lieferung: ${event.deliveryId}:",
|
_handleError(e, "Fehler beim Hinzufügen des Discounts");
|
||||||
);
|
|
||||||
debugPrint("$e");
|
|
||||||
debugPrint("$st");
|
|
||||||
|
|
||||||
opBloc.add(
|
|
||||||
FailOperation(message: "Fehler beim Hinzufügen des Discounts: $e"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unscan(UnscanArticleEvent event, Emitter<TourState> emit) async {
|
void _unscan(UnscanArticleEvent event, Emitter<TourState> emit) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.unscan(
|
await tourRepository.unscan(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
@ -543,29 +471,18 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
event.newAmount,
|
event.newAmount,
|
||||||
event.reason,
|
event.reason,
|
||||||
);
|
);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Unscan des Artikels: ${event.articleId}:");
|
debugPrint("Fehler beim Unscan des Artikels ${event.articleId}: $e $st");
|
||||||
debugPrint("$e");
|
_handleError(e, "Fehler beim Unscan des Artikels");
|
||||||
debugPrint("$st");
|
|
||||||
|
|
||||||
opBloc.add(FailOperation(message: "Fehler beim Unscan des Artikels: $e"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resetAmount(ResetScanAmountEvent event, Emitter<TourState> emit) async {
|
void _resetAmount(ResetScanAmountEvent event, Emitter<TourState> emit) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tourRepository.resetScan(event.articleId, event.deliveryId);
|
await tourRepository.resetScan(event.articleId, event.deliveryId);
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Unscan des Artikels: ${event.articleId}:");
|
debugPrint("Fehler beim Zurücksetzen Artikel ${event.articleId}: $e $st");
|
||||||
debugPrint("$e");
|
_handleError(e, "Fehler beim Zurücksetzen");
|
||||||
debugPrint("$st");
|
|
||||||
|
|
||||||
opBloc.add(FailOperation(message: "Fehler beim Zurücksetzen: $e"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/exceptions.dart';
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
@ -15,6 +18,7 @@ import 'package:hl_lieferservice/feature/delivery/detail/repository/note_reposit
|
|||||||
class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
||||||
final NoteRepository repository;
|
final NoteRepository repository;
|
||||||
final OperationBloc opBloc;
|
final OperationBloc opBloc;
|
||||||
|
final AuthBloc authBloc;
|
||||||
final String deliveryId;
|
final String deliveryId;
|
||||||
|
|
||||||
StreamSubscription? _combinedSubscription;
|
StreamSubscription? _combinedSubscription;
|
||||||
@ -22,6 +26,7 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
NoteBloc({
|
NoteBloc({
|
||||||
required this.repository,
|
required this.repository,
|
||||||
required this.opBloc,
|
required this.opBloc,
|
||||||
|
required this.authBloc,
|
||||||
required this.deliveryId,
|
required this.deliveryId,
|
||||||
}) : super(NoteInitial()) {
|
}) : super(NoteInitial()) {
|
||||||
_combinedSubscription = CombineLatestStream.combine3(
|
_combinedSubscription = CombineLatestStream.combine3(
|
||||||
@ -60,10 +65,17 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_combinedSubscription?.cancel();
|
_combinedSubscription?.cancel();
|
||||||
|
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleError(Object e, String fallbackMessage) {
|
||||||
|
if (e is UserUnauthorized) {
|
||||||
|
authBloc.add(SessionExpiredEvent());
|
||||||
|
} else {
|
||||||
|
opBloc.add(FailOperation(message: fallbackMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _dataUpdated(DataUpdated event, Emitter<NoteState> emit) async {
|
Future<void> _dataUpdated(DataUpdated event, Emitter<NoteState> emit) async {
|
||||||
emit(
|
emit(
|
||||||
NoteLoaded(
|
NoteLoaded(
|
||||||
@ -82,32 +94,21 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
RemoveImageNote event,
|
RemoveImageNote event,
|
||||||
Emitter<NoteState> emit,
|
Emitter<NoteState> emit,
|
||||||
) async {
|
) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.deleteImage(event.deliveryId, event.objectId);
|
await repository.deleteImage(event.deliveryId, event.objectId);
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Löschen des Bildes: $e");
|
debugPrint("Fehler beim Löschen des Bildes: $e $st");
|
||||||
debugPrint(st.toString());
|
_handleError(e, "Fehler beim Löschen des Bildes");
|
||||||
|
|
||||||
opBloc.add(FailOperation(message: e.toString()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _upload(AddImageNote event, Emitter<NoteState> emit) async {
|
Future<void> _upload(AddImageNote event, Emitter<NoteState> emit) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Uint8List imageBytes = await event.file.readAsBytes();
|
Uint8List imageBytes = await event.file.readAsBytes();
|
||||||
await repository.addImage(event.deliveryId, imageBytes);
|
await repository.addImage(event.deliveryId, imageBytes);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Hinzufügen des Bildes: $e");
|
debugPrint("Fehler beim Hinzufügen des Bildes: $e $st");
|
||||||
debugPrint(st.toString());
|
_handleError(e, "Fehler beim Hinzufügen des Bildes");
|
||||||
|
|
||||||
opBloc.add(FailOperation(message: e.toString()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,61 +118,41 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
try {
|
try {
|
||||||
await repository.loadNotes(event.delivery.id);
|
await repository.loadNotes(event.delivery.id);
|
||||||
await repository.loadTemplates();
|
await repository.loadTemplates();
|
||||||
|
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Herunterladen der Notizen: $e");
|
debugPrint("Fehler beim Herunterladen der Notizen: $e $st");
|
||||||
debugPrint(st.toString());
|
if (e is UserUnauthorized) {
|
||||||
|
authBloc.add(SessionExpiredEvent());
|
||||||
opBloc.add(
|
return;
|
||||||
FailOperation(message: "Notizen konnten nicht heruntergeladen werden."),
|
}
|
||||||
);
|
opBloc.add(FailOperation(message: "Notizen konnten nicht heruntergeladen werden."));
|
||||||
|
|
||||||
emit.call(NoteLoadingFailed());
|
emit.call(NoteLoadingFailed());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _add(AddNote event, Emitter<NoteState> emit) async {
|
Future<void> _add(AddNote event, Emitter<NoteState> emit) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.addNote(event.deliveryId, event.note);
|
await repository.addNote(event.deliveryId, event.note);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
|
debugPrint("Fehler beim Hinzufügen der Notiz: $e $st");
|
||||||
debugPrint(st.toString());
|
_handleError(e, "Fehler beim Hinzufügen der Notiz");
|
||||||
|
|
||||||
opBloc.add(FailOperation(message: e.toString()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _edit(EditNote event, Emitter<NoteState> emit) async {
|
Future<void> _edit(EditNote event, Emitter<NoteState> emit) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.editNote(event.noteId, event.content);
|
await repository.editNote(event.noteId, event.content);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
|
debugPrint("Fehler beim Editieren der Notiz: $e $st");
|
||||||
debugPrint(st.toString());
|
_handleError(e, "Fehler beim Editieren der Notiz");
|
||||||
|
|
||||||
opBloc.add(FailOperation(message: e.toString()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _remove(RemoveNote event, Emitter<NoteState> emit) async {
|
Future<void> _remove(RemoveNote event, Emitter<NoteState> emit) async {
|
||||||
opBloc.add(LoadOperation());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.deleteNote(event.noteId);
|
await repository.deleteNote(event.noteId);
|
||||||
opBloc.add(FinishOperation());
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
|
debugPrint("Fehler beim Löschen der Notiz: $e $st");
|
||||||
debugPrint(st.toString());
|
_handleError(e, "Notiz konnte nicht gelöscht werden");
|
||||||
|
|
||||||
opBloc.add(
|
|
||||||
FailOperation(message: "Notizen konnte nicht gelöscht werden."),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import '../../../bloc/tour_bloc.dart';
|
import '../../../bloc/tour_bloc.dart';
|
||||||
import '../../../bloc/tour_state.dart';
|
import '../../../bloc/tour_state.dart';
|
||||||
|
|
||||||
|
enum _StatusAction { hold, cancel, reactivate }
|
||||||
|
|
||||||
class DeliveryStepInfo extends StatefulWidget {
|
class DeliveryStepInfo extends StatefulWidget {
|
||||||
final Delivery delivery;
|
final Delivery delivery;
|
||||||
|
|
||||||
@ -39,76 +41,73 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
|
|||||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _deliveryStatusChangeActions() {
|
Widget _statusOverflow() {
|
||||||
List<Widget> actions = [];
|
final state = widget.delivery.state;
|
||||||
|
final List<PopupMenuEntry<_StatusAction>> entries;
|
||||||
|
|
||||||
if (widget.delivery.state == DeliveryState.ongoing) {
|
if (state == DeliveryState.ongoing) {
|
||||||
actions = [
|
entries = const [
|
||||||
Column(
|
PopupMenuItem(
|
||||||
|
value: _StatusAction.hold,
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
Icon(Icons.change_circle, color: Colors.orangeAccent),
|
||||||
onPressed: () {
|
SizedBox(width: 12),
|
||||||
|
Text("Zurückstellen"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _StatusAction.cancel,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.cancel, color: Colors.red),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text("Abbrechen"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
entries = const [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _StatusAction.reactivate,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.published_with_changes, color: Colors.blueAccent),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text("Reaktivieren"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return PopupMenuButton<_StatusAction>(
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
tooltip: "Status ändern",
|
||||||
|
itemBuilder: (context) => entries,
|
||||||
|
onSelected: (action) {
|
||||||
|
switch (action) {
|
||||||
|
case _StatusAction.hold:
|
||||||
context.read<TourBloc>().add(
|
context.read<TourBloc>().add(
|
||||||
HoldDeliveryEvent(deliveryId: widget.delivery.id),
|
HoldDeliveryEvent(deliveryId: widget.delivery.id),
|
||||||
);
|
);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
break;
|
||||||
icon: Icon(
|
case _StatusAction.cancel:
|
||||||
Icons.change_circle,
|
|
||||||
color: Colors.orangeAccent,
|
|
||||||
size: 42,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text("Zurückstellen"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.read<TourBloc>().add(
|
context.read<TourBloc>().add(
|
||||||
CancelDeliveryEvent(deliveryId: widget.delivery.id),
|
CancelDeliveryEvent(deliveryId: widget.delivery.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
break;
|
||||||
//style: IconButton.styleFrom(backgroundColor: Colors.red),
|
case _StatusAction.reactivate:
|
||||||
icon: Icon(Icons.cancel, color: Colors.red, size: 42),
|
|
||||||
),
|
|
||||||
Text("Abbrechen"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.delivery.state == DeliveryState.canceled ||
|
|
||||||
widget.delivery.state == DeliveryState.onhold ||
|
|
||||||
widget.delivery.state == DeliveryState.finished) {
|
|
||||||
actions = [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.read<TourBloc>().add(
|
context.read<TourBloc>().add(
|
||||||
ReactivateDeliveryEvent(deliveryId: widget.delivery.id),
|
ReactivateDeliveryEvent(deliveryId: widget.delivery.id),
|
||||||
);
|
);
|
||||||
},
|
break;
|
||||||
icon: Icon(
|
|
||||||
Icons.published_with_changes,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
size: 42
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text("Reaktivieren"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: actions,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,55 +118,46 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
|
|||||||
color: Theme.of(context).colorScheme.onSecondary,
|
color: Theme.of(context).colorScheme.onSecondary,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: Column(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
child: Builder(
|
||||||
children: [
|
builder: (context) {
|
||||||
Column(
|
final phone = widget.delivery.contactPerson?.phoneNumber;
|
||||||
|
final bool hasPhone = phone != null && phone.isNotEmpty;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton.filled(
|
IconButton.filled(
|
||||||
onPressed:
|
onPressed: hasPhone
|
||||||
widget.delivery.contactPerson?.phoneNumber != null
|
|
||||||
? () async {
|
? () async {
|
||||||
await launchUrl(
|
await launchUrl(
|
||||||
Uri(
|
Uri(scheme: "tel", path: phone),
|
||||||
scheme: "tel",
|
|
||||||
path:
|
|
||||||
widget
|
|
||||||
.delivery
|
|
||||||
.contactPerson
|
|
||||||
?.phoneNumber!,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
icon: Icon(Icons.phone),
|
icon: const Icon(Icons.phone),
|
||||||
),
|
),
|
||||||
Text("Anrufen"),
|
const Text("Anrufen"),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Column(
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton.filled(
|
IconButton.filled(
|
||||||
onPressed: () {
|
onPressed: () => _launchMapsUrl("google"),
|
||||||
_launchMapsUrl("google");
|
icon: const Icon(Icons.map_outlined),
|
||||||
},
|
|
||||||
icon: Icon(Icons.map_outlined),
|
|
||||||
),
|
),
|
||||||
Text("Google Maps"),
|
const Text("Google Maps"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
_statusOverflow(),
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(top: 10, bottom: 10),
|
|
||||||
child: Divider(),
|
|
||||||
),
|
|
||||||
|
|
||||||
_deliveryStatusChangeActions(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -176,6 +166,16 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _customerInformation() {
|
Widget _customerInformation() {
|
||||||
|
final phone = widget.delivery.contactPerson?.phoneNumber;
|
||||||
|
final String phoneText = (phone != null && phone.isNotEmpty)
|
||||||
|
? phone
|
||||||
|
: "keine Nummer angegeben";
|
||||||
|
|
||||||
|
final email = widget.delivery.customer.email;
|
||||||
|
final String emailText = (email != null && email.isNotEmpty)
|
||||||
|
? email
|
||||||
|
: "keine E-Mail angegeben";
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Card(
|
child: Card(
|
||||||
@ -227,10 +227,25 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(Icons.phone, color: Theme.of(context).primaryColor),
|
Icon(Icons.phone, color: Theme.of(context).primaryColor),
|
||||||
Padding(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10),
|
||||||
|
child: Text(phoneText),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 15),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.mail, color: Theme.of(context).primaryColor),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 10),
|
padding: const EdgeInsets.only(left: 10),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.delivery.contactPerson?.phoneNumber.toString() ??
|
emailText,
|
||||||
"",
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -275,30 +290,68 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _deliveryAgreements() {
|
Widget _agreementsAndDesiredTime() {
|
||||||
String agreements = "keine Vereinbarungen getroffen!";
|
String agreements = "keine Vereinbarungen getroffen!";
|
||||||
if (widget.delivery.specialAgreements != null &&
|
if (widget.delivery.specialAgreements != null &&
|
||||||
widget.delivery.specialAgreements != "") {
|
widget.delivery.specialAgreements != "") {
|
||||||
agreements = widget.delivery.specialAgreements!;
|
agreements = widget.delivery.specialAgreements!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final desiredTime = widget.delivery.desiredTime;
|
||||||
|
final bool hasDesiredTime = desiredTime != null && desiredTime.isNotEmpty;
|
||||||
|
final primary = Theme.of(context).primaryColor;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).colorScheme.onSecondary,
|
color: Theme.of(context).colorScheme.onSecondary,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (hasDesiredTime) ...[
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
child: Icon(
|
child: Icon(Icons.schedule, color: primary, size: 28),
|
||||||
Icons.warning,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
size: 28,
|
|
||||||
),
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Wunschtermin",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
desiredTime,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
],
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
child: Icon(Icons.warning, color: primary, size: 28),
|
||||||
),
|
),
|
||||||
Expanded(child: Text(agreements)),
|
Expanded(child: Text(agreements)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -330,7 +383,7 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 10),
|
padding: const EdgeInsets.only(top: 10),
|
||||||
child: _deliveryAgreements(),
|
child: _agreementsAndDesiredTime(),
|
||||||
),
|
),
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@ -275,10 +275,14 @@ class NoteService {
|
|||||||
LocalDocuFrameConfiguration config = getConfig();
|
LocalDocuFrameConfiguration config = getConfig();
|
||||||
|
|
||||||
return urls.map((url) async {
|
return urls.map((url) async {
|
||||||
return (await http.get(
|
final response = await http.get(
|
||||||
Uri.parse("${config.backendUrl}$url"),
|
Uri.parse("${config.backendUrl}$url"),
|
||||||
headers: getSessionOrThrow(),
|
headers: getSessionOrThrow(),
|
||||||
)).bodyBytes;
|
);
|
||||||
|
if (response.statusCode == HttpStatus.unauthorized) {
|
||||||
|
throw UserUnauthorized();
|
||||||
|
}
|
||||||
|
return response.bodyBytes;
|
||||||
}).toList();
|
}).toList();
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("An error occured:");
|
debugPrint("An error occured:");
|
||||||
|
|||||||
@ -1,32 +1,30 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hl_lieferservice/model/delivery.dart' show DeliveryState;
|
||||||
import 'package:hl_lieferservice/model/tour.dart';
|
import 'package:hl_lieferservice/model/tour.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class DeliveryInfo extends StatelessWidget {
|
class DeliveryInfo extends StatelessWidget {
|
||||||
final Tour tour;
|
final Tour tour;
|
||||||
|
final int? selectedCarId;
|
||||||
|
|
||||||
const DeliveryInfo({super.key, required this.tour});
|
const DeliveryInfo({super.key, required this.tour, this.selectedCarId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String date = DateFormat("dd.MM.yyyy").format(tour.date);
|
final String date = DateFormat("dd.MM.yyyy").format(tour.date);
|
||||||
String amountDeliveries = tour.deliveries.length.toString();
|
final relevantDeliveries = selectedCarId != null
|
||||||
|
? tour.deliveries.where((d) => d.carId == selectedCarId).toList()
|
||||||
|
: tour.deliveries;
|
||||||
|
final total = relevantDeliveries.length;
|
||||||
|
final done = relevantDeliveries
|
||||||
|
.where((d) => d.state == DeliveryState.finished)
|
||||||
|
.length;
|
||||||
|
final progress = total > 0 ? done / total : 0.0;
|
||||||
|
final allDone = total > 0 && done == total;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15),
|
||||||
child: Column(
|
child: SizedBox(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 10),
|
|
||||||
child: Text(
|
|
||||||
"Informationen",
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Card(
|
child: Card(
|
||||||
color: Theme.of(context).colorScheme.onSecondary,
|
color: Theme.of(context).colorScheme.onSecondary,
|
||||||
@ -40,9 +38,9 @@ class DeliveryInfo extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.calendar_month),
|
const Icon(Icons.calendar_month),
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.only(left: 5),
|
padding: EdgeInsets.only(left: 5),
|
||||||
child: Text("Datum"),
|
child: Text("Datum"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -50,32 +48,40 @@ class DeliveryInfo extends StatelessWidget {
|
|||||||
Text(date),
|
Text(date),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
Padding(
|
Row(
|
||||||
padding: const EdgeInsets.only(top: 15),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.local_shipping_outlined),
|
const Icon(Icons.local_shipping_outlined),
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.only(left: 5),
|
padding: EdgeInsets.only(left: 5),
|
||||||
child: Text("Lieferungen"),
|
child: Text("Lieferungen"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(amountDeliveries),
|
Text("$done / $total"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
minHeight: 6,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
allDone ? Colors.green : Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
import 'package:hl_lieferservice/model/delivery.dart';
|
import 'package:hl_lieferservice/model/delivery.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart';
|
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart';
|
||||||
|
|
||||||
@ -18,60 +19,132 @@ class DeliveryListItem extends StatelessWidget {
|
|||||||
required this.distance,
|
required this.distance,
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _leading(BuildContext context) {
|
|
||||||
if (delivery.state == DeliveryState.finished) {
|
|
||||||
return Icon(Icons.check_circle, color: Colors.green);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delivery.state == DeliveryState.canceled) {
|
|
||||||
return Icon(Icons.cancel_rounded, color: Colors.red);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delivery.state == DeliveryState.onhold) {
|
|
||||||
return Icon(Icons.pause_circle, color: Colors.orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.location_on, color: Theme.of(context).primaryColor),
|
|
||||||
Text("${distance.toStringAsFixed(2)}km"),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _goToDelivery(BuildContext context) {
|
void _goToDelivery(BuildContext context) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder:
|
builder: (context) => BlocProvider(
|
||||||
(context) => BlocProvider(
|
create: (context) => NoteBloc(
|
||||||
create:
|
|
||||||
(context) => NoteBloc(
|
|
||||||
deliveryId: delivery.id,
|
deliveryId: delivery.id,
|
||||||
opBloc: context.read<OperationBloc>(),
|
opBloc: context.read<OperationBloc>(),
|
||||||
repository: NoteRepository(
|
authBloc: context.read<AuthBloc>(),
|
||||||
service: NoteService(),
|
repository: NoteRepository(service: NoteService()),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
child: DeliveryDetail(deliveryId: delivery.id),
|
child: DeliveryDetail(deliveryId: delivery.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(Color, Color, IconData, String) _stateStyle(BuildContext context) {
|
||||||
|
switch (delivery.state) {
|
||||||
|
case DeliveryState.finished:
|
||||||
|
return (
|
||||||
|
Colors.green.withValues(alpha: 0.07),
|
||||||
|
Colors.green.withValues(alpha: 0.35),
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
"Abgeschlossen",
|
||||||
|
);
|
||||||
|
case DeliveryState.canceled:
|
||||||
|
return (
|
||||||
|
Colors.red.withValues(alpha: 0.07),
|
||||||
|
Colors.red.withValues(alpha: 0.35),
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
"Storniert",
|
||||||
|
);
|
||||||
|
case DeliveryState.onhold:
|
||||||
|
return (
|
||||||
|
Colors.orange.withValues(alpha: 0.07),
|
||||||
|
Colors.orange.withValues(alpha: 0.35),
|
||||||
|
Icons.pause_circle_rounded,
|
||||||
|
"Pausiert",
|
||||||
|
);
|
||||||
|
case DeliveryState.ongoing:
|
||||||
|
return (
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
Colors.transparent,
|
||||||
|
Icons.local_shipping_outlined,
|
||||||
|
"${distance.toStringAsFixed(1)} km",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
final (cardColor, borderColor, icon, statusLabel) = _stateStyle(context);
|
||||||
title: Text(
|
final isOngoing = delivery.state == DeliveryState.ongoing;
|
||||||
delivery.customer.name,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
final iconColor = switch (delivery.state) {
|
||||||
|
DeliveryState.finished => Colors.green,
|
||||||
|
DeliveryState.canceled => Colors.red,
|
||||||
|
DeliveryState.onhold => Colors.orange,
|
||||||
|
DeliveryState.ongoing => Theme.of(context).primaryColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
elevation: 0,
|
||||||
|
color: cardColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: borderColor),
|
||||||
),
|
),
|
||||||
leading: _leading(context),
|
child: InkWell(
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
borderRadius: BorderRadius.circular(12),
|
||||||
subtitle: Text(delivery.customer.address.toString()),
|
|
||||||
trailing: Icon(Icons.arrow_forward_ios),
|
|
||||||
onTap: () => _goToDelivery(context),
|
onTap: () => _goToDelivery(context),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: iconColor, size: 28),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
delivery.customer.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isOngoing ? null : iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
delivery.customer.address.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
statusLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isOngoing
|
||||||
|
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||||
|
: iconColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios,
|
||||||
|
size: 14,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/overview/model/sorting_information.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
|
||||||
import 'package:hl_lieferservice/model/delivery.dart';
|
import 'package:hl_lieferservice/model/delivery.dart';
|
||||||
|
|
||||||
@ -56,79 +56,66 @@ class _DeliveryListState extends State<DeliveryList> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
List<Delivery> deliveries =
|
if (widget.sortType == SortType.custom) {
|
||||||
currentState.tour.deliveries
|
|
||||||
.where(
|
|
||||||
(delivery) =>
|
|
||||||
delivery.carId == widget.selectedCarId &&
|
|
||||||
delivery.allArticlesScanned() &&
|
|
||||||
delivery.state != DeliveryState.finished,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
List<Delivery> finishedDeliveries =
|
|
||||||
currentState.tour.deliveries
|
|
||||||
.where(
|
|
||||||
(delivery) =>
|
|
||||||
delivery.state == DeliveryState.finished &&
|
|
||||||
delivery.carId == widget.selectedCarId,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (deliveries.isEmpty) {
|
|
||||||
return ListView(
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
shrinkWrap: true,
|
|
||||||
children: [
|
|
||||||
Center(child: const Text("Keine Auslieferungen gefunden")),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (widget.sortType) {
|
|
||||||
case SortType.custom:
|
|
||||||
return _showCustomSortedList(
|
return _showCustomSortedList(
|
||||||
currentState.tour.deliveries,
|
currentState.tour.deliveries,
|
||||||
currentState.sortingInformation[widget.selectedCarId.toString()] ?? [],
|
currentState.sortingInformation[widget.selectedCarId.toString()] ?? [],
|
||||||
currentState.distances ?? {},
|
currentState.distances ?? {},
|
||||||
);
|
);
|
||||||
|
|
||||||
case SortType.nameAsc:
|
|
||||||
deliveries.sort(
|
|
||||||
(a, b) => a.customer.name.compareTo(b.customer.name),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SortType.nameDesc:
|
|
||||||
deliveries.sort(
|
|
||||||
(a, b) => b.customer.name.compareTo(a.customer.name),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SortType.distance:
|
|
||||||
deliveries.sort(
|
|
||||||
(a, b) => (currentState.distances![a.id] ?? 0.0).compareTo(
|
|
||||||
currentState.distances![b.id] ?? 0.0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//deliveries.addAll(finishedDeliveries);
|
final allDeliveries = currentState.tour.deliveries
|
||||||
|
.where((d) => d.carId == widget.selectedCarId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
return ListView.separated(
|
if (allDeliveries.isEmpty) {
|
||||||
separatorBuilder: (context, index) => const Divider(height: 0),
|
return ListView(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
children: const [
|
||||||
itemBuilder: (context, index) {
|
Center(child: Text("Keine Auslieferungen gefunden")),
|
||||||
Delivery delivery = deliveries[index];
|
],
|
||||||
|
|
||||||
return DeliveryListItem(
|
|
||||||
delivery: delivery,
|
|
||||||
distance: currentState.distances?[delivery.id] ?? 0.0,
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
itemCount: deliveries.length,
|
|
||||||
|
final ongoing = allDeliveries
|
||||||
|
.where((d) => d.state == DeliveryState.ongoing)
|
||||||
|
.toList();
|
||||||
|
final nonOngoing = allDeliveries
|
||||||
|
.where((d) => d.state != DeliveryState.ongoing)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
int Function(Delivery, Delivery) comparator;
|
||||||
|
switch (widget.sortType) {
|
||||||
|
case SortType.nameAsc:
|
||||||
|
comparator = (a, b) => a.customer.name.compareTo(b.customer.name);
|
||||||
|
break;
|
||||||
|
case SortType.nameDesc:
|
||||||
|
comparator = (a, b) => b.customer.name.compareTo(a.customer.name);
|
||||||
|
break;
|
||||||
|
case SortType.distance:
|
||||||
|
comparator = (a, b) =>
|
||||||
|
(currentState.distances?[a.id] ?? 0.0)
|
||||||
|
.compareTo(currentState.distances?[b.id] ?? 0.0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
comparator = (a, b) => a.customer.name.compareTo(b.customer.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
ongoing.sort(comparator);
|
||||||
|
nonOngoing.sort(comparator);
|
||||||
|
|
||||||
|
final sorted = [...ongoing, ...nonOngoing];
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
itemCount: sorted.length,
|
||||||
|
itemBuilder: (context, index) => DeliveryListItem(
|
||||||
|
delivery: sorted[index],
|
||||||
|
distance: currentState.distances?[sorted[index].id] ?? 0.0,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_info.dart';
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_info.dart';
|
||||||
@ -34,8 +36,14 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// Select the first car for initialization
|
// Pre-select today's car from the daily car selection.
|
||||||
|
// Falls back to the first available car if no selection exists.
|
||||||
|
final carSelectState = context.read<CarSelectBloc>().state;
|
||||||
|
if (carSelectState is CarSelectComplete) {
|
||||||
|
_selectedCarId = carSelectState.selectedCar.id;
|
||||||
|
} else {
|
||||||
_selectedCarId = widget.tour.driver.cars.firstOrNull?.id;
|
_selectedCarId = widget.tour.driver.cars.firstOrNull?.id;
|
||||||
|
}
|
||||||
_sortType = SortType.nameAsc;
|
_sortType = SortType.nameAsc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,54 +52,6 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
|
|||||||
context.read<TourBloc>().add(LoadTour(teamId: state.user.number));
|
context.read<TourBloc>().add(LoadTour(teamId: state.user.number));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _carSelection() {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 50,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children:
|
|
||||||
widget.tour.driver.cars.map((car) {
|
|
||||||
Color? backgroundColor;
|
|
||||||
Color? iconColor = Theme.of(context).primaryColor;
|
|
||||||
Color? textColor;
|
|
||||||
|
|
||||||
if (_selectedCarId == car.id) {
|
|
||||||
backgroundColor = Theme.of(context).primaryColor;
|
|
||||||
textColor = Theme.of(context).colorScheme.onSecondary;
|
|
||||||
iconColor = Theme.of(context).colorScheme.onSecondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_selectedCarId = car.id;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Chip(
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
label: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.local_shipping, color: iconColor, size: 20),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 5),
|
|
||||||
child: Text(
|
|
||||||
car.plate,
|
|
||||||
style: TextStyle(color: textColor, fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight the text of the active sorting type.
|
/// Highlight the text of the active sorting type.
|
||||||
TextStyle? _popupItemTextStyle() {
|
TextStyle? _popupItemTextStyle() {
|
||||||
return TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold);
|
return TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold);
|
||||||
@ -99,17 +59,23 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RefreshIndicator(
|
return BlocListener<CarSelectBloc, CarSelectState>(
|
||||||
|
listener: (context, carState) {
|
||||||
|
if (carState is CarSelectComplete) {
|
||||||
|
setState(() => _selectedCarId = carState.selectedCar.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: RefreshIndicator(
|
||||||
onRefresh: _loadTour,
|
onRefresh: _loadTour,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
//crossAxisAlignment: CrossAxisAlignment.start,
|
//crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
DeliveryInfo(tour: widget.tour),
|
DeliveryInfo(tour: widget.tour, selectedCarId: _selectedCarId),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 10,
|
left: 10,
|
||||||
right: 10,
|
right: 10,
|
||||||
top: 15,
|
top: 0,
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -191,16 +157,13 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20),
|
|
||||||
child: _carSelection(),
|
|
||||||
),
|
|
||||||
DeliveryList(
|
DeliveryList(
|
||||||
selectedCarId: _selectedCarId,
|
selectedCarId: _selectedCarId,
|
||||||
sortType: _sortType,
|
sortType: _sortType,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart';
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
|
||||||
|
|
||||||
@ -16,16 +18,45 @@ class DeliveryOverviewPage extends StatefulWidget {
|
|||||||
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
|
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<TourBloc, TourState>(
|
final carState = context.watch<CarSelectBloc>().state;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Auslieferung"),
|
||||||
|
centerTitle: false,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
actions: [
|
||||||
|
if (carState is CarSelectComplete)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.local_shipping,
|
||||||
|
color: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
carState.selectedCar.plate,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: BlocBuilder<TourBloc, TourState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is TourLoaded) {
|
if (state is TourLoaded) {
|
||||||
final currentState = state;
|
return DeliveryOverview(
|
||||||
|
tour: state.tour,
|
||||||
return Center(
|
distances: state.distances ?? {},
|
||||||
child: DeliveryOverview(
|
|
||||||
tour: currentState.tour,
|
|
||||||
distances: currentState.distances ?? {},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,8 +64,9 @@ class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
|
|||||||
return DeliveryLoadingFailedPage();
|
return DeliveryLoadingFailedPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container();
|
return const Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,6 +80,8 @@ class TourRepository {
|
|||||||
|
|
||||||
if (article.scannedAmount < article.amount) {
|
if (article.scannedAmount < article.amount) {
|
||||||
article.scannedAmount += 1;
|
article.scannedAmount += 1;
|
||||||
|
delivery.carId = int.tryParse(carId) ?? delivery.carId;
|
||||||
|
await service.assignCar(deliveryId, carId);
|
||||||
_tourStream.add(tour);
|
_tourStream.add(tour);
|
||||||
return ScanResult.scanned;
|
return ScanResult.scanned;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:hl_lieferservice/dto/delivery_response.dart';
|
import 'package:hl_lieferservice/dto/delivery_response.dart';
|
||||||
import 'package:hl_lieferservice/dto/delivery_update.dart';
|
import 'package:hl_lieferservice/dto/delivery_update.dart';
|
||||||
import 'package:hl_lieferservice/dto/delivery_update_response.dart';
|
import 'package:hl_lieferservice/dto/delivery_update_response.dart';
|
||||||
@ -271,10 +272,24 @@ class TourService {
|
|||||||
|
|
||||||
Future<BasicResponseDTO> finishDelivery(String deliveryId) async {
|
Future<BasicResponseDTO> finishDelivery(String deliveryId) async {
|
||||||
try {
|
try {
|
||||||
|
// ISO-8601 mit T-Separator: sprachunabhaengig fuer SQL-Server datetime.
|
||||||
|
// ('yyyy-MM-dd HH:mm:ss' OHNE T wird unter DATEFORMAT=DMY (DE) als YDM
|
||||||
|
// geparst und schlaegt fuer Tag > 12 fehl.)
|
||||||
|
// ERPframe arbeitet mit lokaler Zeit -> bewusst keine UTC-Konvertierung.
|
||||||
|
final String deliveredAt = DateFormat(
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss",
|
||||||
|
).format(DateTime.now());
|
||||||
|
|
||||||
|
var headers = {"Content-Type": "application/json"};
|
||||||
|
headers.addAll(getSessionOrThrow());
|
||||||
|
|
||||||
var response = await post(
|
var response = await post(
|
||||||
urlBuilder("_web_finishDelivery"),
|
urlBuilder("_web_finishDelivery"),
|
||||||
headers: getSessionOrThrow(),
|
headers: headers,
|
||||||
body: {"delivery_id": deliveryId},
|
body: jsonEncode({
|
||||||
|
"delivery_id": deliveryId,
|
||||||
|
"delivered_at": deliveredAt,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == HttpStatus.unauthorized) {
|
if (response.statusCode == HttpStatus.unauthorized) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,472 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/scan/presentation/scanner.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/settings/bloc/settings_bloc.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart';
|
|
||||||
import 'package:hl_lieferservice/model/article.dart';
|
|
||||||
import 'package:hl_lieferservice/model/car.dart';
|
|
||||||
import 'package:hl_lieferservice/model/delivery.dart';
|
|
||||||
import 'package:hl_lieferservice/model/tour.dart';
|
|
||||||
import 'package:hl_lieferservice/widget/home/bloc/navigation_event.dart';
|
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
|
||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
|
||||||
|
|
||||||
import '../../../widget/home/bloc/navigation_bloc.dart';
|
|
||||||
import '../../delivery/bloc/tour_bloc.dart';
|
|
||||||
|
|
||||||
class ArticleScanningScreen extends StatefulWidget {
|
|
||||||
const ArticleScanningScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ArticleScanningScreen> createState() => _ArticleScanningScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ArticleScanningScreenState extends State<ArticleScanningScreen> {
|
|
||||||
final FocusNode _focusNode = FocusNode();
|
|
||||||
String _buffer = '';
|
|
||||||
Timer? _bufferTimer;
|
|
||||||
int _selectedDelivery = 0;
|
|
||||||
int? _selectedCarId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
// Focus anfordern, um Keyboard-Events zu empfangen
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_focusNode.requestFocus();
|
|
||||||
});
|
|
||||||
|
|
||||||
final state = context.read<TourBloc>().state;
|
|
||||||
|
|
||||||
if (state is TourLoaded) {
|
|
||||||
setState(() {
|
|
||||||
_selectedCarId = state.tour.deliveries[_selectedDelivery].carId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_focusNode.dispose();
|
|
||||||
_bufferTimer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleKey(KeyEvent event) {
|
|
||||||
if (event is KeyDownEvent) {
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
|
||||||
// Enter = Scan abgeschlossen
|
|
||||||
_bufferTimer?.cancel();
|
|
||||||
if (_buffer.isNotEmpty) {
|
|
||||||
_handleBarcodeScanned(_buffer);
|
|
||||||
_buffer = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Zeichen zum Buffer hinzufügen
|
|
||||||
final character = event.character;
|
|
||||||
if (character != null && character.isNotEmpty) {
|
|
||||||
_buffer += character;
|
|
||||||
|
|
||||||
// Timer zurücksetzen
|
|
||||||
_bufferTimer?.cancel();
|
|
||||||
_bufferTimer = Timer(Duration(milliseconds: 1000), () {
|
|
||||||
// Nach 1 Sekunde ohne neue Eingabe: Buffer verarbeiten
|
|
||||||
if (_buffer.isNotEmpty) {
|
|
||||||
_handleBarcodeScanned(_buffer);
|
|
||||||
_buffer = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleBarcodeScanned(String barcode) {
|
|
||||||
if (_selectedCarId == null) {
|
|
||||||
context.read<OperationBloc>().add(
|
|
||||||
FailOperation(message: "Wählen Sie zu erst ein Fahrzeug aus"),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final state = context.read<TourBloc>().state as TourLoaded;
|
|
||||||
|
|
||||||
context.read<TourBloc>().add(
|
|
||||||
ScanArticleEvent(
|
|
||||||
articleNumber: barcode,
|
|
||||||
carId: _selectedCarId!.toString(),
|
|
||||||
deliveryId: state.tour.deliveries[_selectedDelivery].id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _carSelection(List<Car> cars, List<Delivery> deliveries) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Fahrzeug auswählen",
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 50,
|
|
||||||
child: ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children:
|
|
||||||
cars.map((car) {
|
|
||||||
Color? backgroundColor;
|
|
||||||
Color? iconColor = Theme.of(context).primaryColor;
|
|
||||||
Color? textColor;
|
|
||||||
|
|
||||||
if (_selectedCarId == car.id) {
|
|
||||||
backgroundColor = Theme.of(context).primaryColor;
|
|
||||||
textColor = Theme.of(context).colorScheme.onSecondary;
|
|
||||||
iconColor = Theme.of(context).colorScheme.onSecondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
context.read<TourBloc>().add(
|
|
||||||
AssignCarEvent(
|
|
||||||
deliveryId: deliveries[_selectedDelivery].id,
|
|
||||||
carId: car.id.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_selectedCarId = car.id;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Chip(
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
label: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.local_shipping,
|
|
||||||
color: iconColor,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 5),
|
|
||||||
child: Text(
|
|
||||||
car.plate,
|
|
||||||
style: TextStyle(
|
|
||||||
color: textColor,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _articles(List<Article> articles) {
|
|
||||||
List<Article> scannableArticles =
|
|
||||||
articles.where((article) => article.scannable).toList();
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 20, bottom: 20),
|
|
||||||
child: Text(
|
|
||||||
"Artikel",
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
scannableArticles.isEmpty
|
|
||||||
? Center(
|
|
||||||
child: Text(
|
|
||||||
'Keine Artikel zum Scannen vorhanden',
|
|
||||||
style: TextStyle(fontSize: 18),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ListView.separated(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: scannableArticles.length,
|
|
||||||
separatorBuilder:
|
|
||||||
(context, index) => Divider(
|
|
||||||
height: 0,
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final article = scannableArticles[index];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading:
|
|
||||||
article.scannedAmount == article.amount
|
|
||||||
? Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
color: Colors.green,
|
|
||||||
size: 32,
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
width: 32,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
'${article.scannedAmount}/${article.amount}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color:
|
|
||||||
article.scannedAmount > 0
|
|
||||||
? Colors.blue
|
|
||||||
: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
article.name,
|
|
||||||
style: TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
subtitle: Text("Artikelnr. ${article.articleNumber}"),
|
|
||||||
tileColor:
|
|
||||||
article.scannedAmount == article.amount
|
|
||||||
? Colors.green.withValues(alpha: 0.1)
|
|
||||||
: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectDelivery(int? index) {
|
|
||||||
setState(() {
|
|
||||||
_selectedDelivery = index!;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _navigation(List<Delivery> deliveries) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed:
|
|
||||||
_selectedDelivery > 0
|
|
||||||
? () => {
|
|
||||||
if (_selectedDelivery > 0)
|
|
||||||
{
|
|
||||||
setState(() {
|
|
||||||
_selectedDelivery -= 1;
|
|
||||||
_selectedCarId = deliveries[_selectedDelivery].carId;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Text("zurück"),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 20, right: 20),
|
|
||||||
child: DropdownButton(
|
|
||||||
menuWidth: MediaQuery.of(context).size.width,
|
|
||||||
isExpanded: true,
|
|
||||||
items:
|
|
||||||
deliveries
|
|
||||||
.where(
|
|
||||||
(delivery) => delivery.state != DeliveryState.finished,
|
|
||||||
)
|
|
||||||
.mapIndexed(
|
|
||||||
(index, delivery) => DropdownMenuItem(
|
|
||||||
value: index,
|
|
||||||
child: Text(
|
|
||||||
delivery.customer.name,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onChanged: _selectDelivery,
|
|
||||||
value: _selectedDelivery,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed:
|
|
||||||
_selectedDelivery < deliveries.length - 1
|
|
||||||
? () => {
|
|
||||||
if (_selectedDelivery + 1 < deliveries.length)
|
|
||||||
{
|
|
||||||
setState(() {
|
|
||||||
_selectedDelivery += 1;
|
|
||||||
_selectedCarId = deliveries[_selectedDelivery].carId;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Text("weiter"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _deliveryStepper(Tour tour) {
|
|
||||||
final settingsState = context.read<SettingsBloc>().state;
|
|
||||||
Widget scannerWidget = BarcodeScannerWidget(
|
|
||||||
onBarcodeDetected: _handleBarcodeScanned,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (settingsState is AppSettingsFailed) {
|
|
||||||
context.read<OperationBloc>().add(
|
|
||||||
FailOperation(
|
|
||||||
message:
|
|
||||||
"Einstellungen konnten nicht geladen werden. Nutze Kamera-Scanner.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settingsState is AppSettingsLoaded) {
|
|
||||||
if (settingsState.settings.useHardwareScanner) {
|
|
||||||
scannerWidget = Container();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also count aborted or hold deliveries as "delivered"
|
|
||||||
final allDeliveredOrAllScanned = tour.deliveries
|
|
||||||
.where((delivery) => delivery.state != DeliveryState.finished)
|
|
||||||
.every((delivery) => delivery.allArticlesScanned());
|
|
||||||
|
|
||||||
if (allDeliveredOrAllScanned) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(25),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 25),
|
|
||||||
child: Icon(
|
|
||||||
Icons.check_circle_outline,
|
|
||||||
size: 72,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text("Alles erledigt - es gibt nichts mehr zu scannen!"),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 25),
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
context.read<NavigationBloc>().add(
|
|
||||||
NavigateToIndex(index: 1),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text("Tour starten"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
scannerWidget,
|
|
||||||
_carSelection(tour.driver.cars, tour.deliveries),
|
|
||||||
_articles(tour.deliveries[_selectedDelivery].articles),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<TourBloc, TourState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state is TourLoaded) {
|
|
||||||
Delivery delivery = state.tour.deliveries[_selectedDelivery];
|
|
||||||
|
|
||||||
// Also count aborted or hold deliveries as "delivered"
|
|
||||||
final allDeliveredOrAllScanned = state.tour.deliveries
|
|
||||||
.where((delivery) => delivery.state != DeliveryState.finished)
|
|
||||||
.every((delivery) => delivery.allArticlesScanned());
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title:
|
|
||||||
allDeliveredOrAllScanned
|
|
||||||
? Text(
|
|
||||||
"Artikel scannen",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
delivery.customer.name,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
delivery.customer.address.toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
color: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
bottomNavigationBar:
|
|
||||||
allDeliveredOrAllScanned
|
|
||||||
? Text("")
|
|
||||||
: Padding(
|
|
||||||
padding: const EdgeInsets.all(25),
|
|
||||||
child: _navigation(
|
|
||||||
state.tour.deliveries
|
|
||||||
.where(
|
|
||||||
(delivery) =>
|
|
||||||
delivery.state == DeliveryState.ongoing,
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: KeyboardListener(
|
|
||||||
focusNode: _focusNode,
|
|
||||||
onKeyEvent: _handleKey,
|
|
||||||
child: _deliveryStepper(state.tour),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -53,11 +53,8 @@ class _BarcodeScannerWidgetState extends State<BarcodeScannerWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenHeight = MediaQuery.of(context).size.height;
|
|
||||||
final scannerHeight = screenHeight / 4;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: scannerHeight,
|
height: 150,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: _isDetected ? Colors.green : Colors.grey,
|
color: _isDetected ? Colors.green : Colors.grey,
|
||||||
|
|||||||
@ -122,10 +122,9 @@ class _SettingsPage extends State<SettingsPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: const Text("Einstellungen"),
|
||||||
"Einstellungen",
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,17 @@ import 'package:hl_lieferservice/dto/customer.dart';
|
|||||||
import 'address.dart';
|
import 'address.dart';
|
||||||
|
|
||||||
class Customer {
|
class Customer {
|
||||||
const Customer({required this.name, required this.address});
|
const Customer({required this.name, required this.address, this.email});
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final Address address;
|
final Address address;
|
||||||
|
final String? email;
|
||||||
|
|
||||||
factory Customer.fromDTO(CustomerDTO dto) {
|
factory Customer.fromDTO(CustomerDTO dto) {
|
||||||
return Customer(name: dto.name, address: Address.fromDTO(dto.address));
|
return Customer(
|
||||||
|
name: dto.name,
|
||||||
|
address: Address.fromDTO(dto.address),
|
||||||
|
email: dto.eMail,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,23 @@ class Tour {
|
|||||||
.length;
|
.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the car still has loaded articles assigned to a delivery
|
||||||
|
/// that has not been finished yet. Scannable articles count when their
|
||||||
|
/// effective scanned amount (scanned minus removed) is positive; non-scannable
|
||||||
|
/// articles count when their target amount is greater than zero.
|
||||||
|
bool hasUndeliveredLoadedArticles(int carId) {
|
||||||
|
return deliveries.any((delivery) {
|
||||||
|
if (delivery.carId != carId) return false;
|
||||||
|
if (delivery.state == DeliveryState.finished) return false;
|
||||||
|
return delivery.articles.any((article) {
|
||||||
|
if (article.scannable) {
|
||||||
|
return article.scannedAmount > article.scannedRemovedAmount;
|
||||||
|
}
|
||||||
|
return article.amount > 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Tour copyWith({
|
Tour copyWith({
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
String? discountArticleNumber,
|
String? discountArticleNumber,
|
||||||
|
|||||||
@ -4,7 +4,13 @@ import 'package:hl_lieferservice/bloc/app_bloc.dart';
|
|||||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/presentation/login_enforcer.dart';
|
import 'package:hl_lieferservice/feature/authentication/presentation/login_enforcer.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/service/userinfo.dart';
|
import 'package:hl_lieferservice/feature/authentication/service/userinfo.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/presentation/car_selection_enforcer.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/cars/presentation/car_management_page.dart';
|
import 'package:hl_lieferservice/feature/cars/presentation/car_management_page.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/repository/cars_repository.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/service/cars_service.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/repository/tour_repository.dart';
|
import 'package:hl_lieferservice/feature/delivery/repository/tour_repository.dart';
|
||||||
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
|
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
|
||||||
@ -46,11 +52,23 @@ class _DeliveryAppState extends State<DeliveryApp> {
|
|||||||
create:
|
create:
|
||||||
(context) => TourBloc(
|
(context) => TourBloc(
|
||||||
opBloc: context.read<OperationBloc>(),
|
opBloc: context.read<OperationBloc>(),
|
||||||
|
authBloc: context.read<AuthBloc>(),
|
||||||
tourRepository: TourRepository(
|
tourRepository: TourRepository(
|
||||||
service: TourService(),
|
service: TourService(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
CarSelectBloc(repository: CarSelectionRepository()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => CarsBloc(
|
||||||
|
repository: CarsRepository(service: CarService()),
|
||||||
|
opBloc: context.read<OperationBloc>(),
|
||||||
|
authBloc: context.read<AuthBloc>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
home: OperationViewEnforcer(
|
home: OperationViewEnforcer(
|
||||||
@ -67,7 +85,9 @@ class _DeliveryAppState extends State<DeliveryApp> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state is AppConfigLoaded) {
|
if (state is AppConfigLoaded) {
|
||||||
return LoginEnforcer(child: Home());
|
return LoginEnforcer(
|
||||||
|
child: CarSelectionEnforcer(child: Home()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container();
|
return Container();
|
||||||
|
|||||||
@ -7,15 +7,12 @@ import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
|||||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview_page.dart';
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview_page.dart';
|
||||||
import 'package:hl_lieferservice/feature/scan/presentation/scan_page.dart';
|
import 'package:hl_lieferservice/feature/scan/presentation/scan_page.dart';
|
||||||
import 'package:hl_lieferservice/widget/app_bar.dart';
|
import 'package:hl_lieferservice/feature/car_selection/presentation/selected_car_bar.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/settings/presentation/settings_page.dart';
|
||||||
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
|
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
|
||||||
import 'package:hl_lieferservice/widget/home/bloc/navigation_state.dart';
|
import 'package:hl_lieferservice/widget/home/bloc/navigation_state.dart';
|
||||||
import 'package:hl_lieferservice/widget/navigation_bar/presentation/navigation_bar.dart';
|
import 'package:hl_lieferservice/widget/navigation_bar/presentation/navigation_bar.dart';
|
||||||
|
|
||||||
import '../../../feature/cars/bloc/cars_bloc.dart';
|
|
||||||
import '../../../feature/cars/repository/cars_repository.dart';
|
|
||||||
import '../../../feature/cars/service/cars_service.dart';
|
|
||||||
import '../../operations/bloc/operation_bloc.dart';
|
|
||||||
|
|
||||||
class Home extends StatefulWidget {
|
class Home extends StatefulWidget {
|
||||||
const Home({super.key});
|
const Home({super.key});
|
||||||
@ -44,14 +41,11 @@ class _HomeState extends State<Home> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (index == 2) {
|
if (index == 2) {
|
||||||
return BlocProvider(
|
return CarManagementPage();
|
||||||
create:
|
}
|
||||||
(context) => CarsBloc(
|
|
||||||
repository: CarsRepository(service: CarService()),
|
if (index == 3) {
|
||||||
opBloc: context.read<OperationBloc>(),
|
return SettingsPage();
|
||||||
),
|
|
||||||
child: CarManagementPage(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container();
|
return Container();
|
||||||
@ -64,12 +58,14 @@ class _HomeState extends State<Home> {
|
|||||||
final currentState = state as NavigationInfo;
|
final currentState = state as NavigationInfo;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: PreferredSize(
|
|
||||||
preferredSize: Size.fromHeight(kToolbarHeight),
|
|
||||||
child: CustomAppBar(),
|
|
||||||
),
|
|
||||||
body: _buildPage(currentState.navigationIndex),
|
body: _buildPage(currentState.navigationIndex),
|
||||||
bottomNavigationBar: AppNavigationBar(),
|
bottomNavigationBar: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SelectedCarBar(),
|
||||||
|
AppNavigationBar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -32,6 +32,11 @@ class _AppNavigationBarState extends State<AppNavigationBar> {
|
|||||||
icon: Icon(Icons.local_shipping),
|
icon: Icon(Icons.local_shipping),
|
||||||
label: "Fahrzeuge",
|
label: "Fahrzeuge",
|
||||||
),
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.settings_outlined),
|
||||||
|
selectedIcon: Icon(Icons.settings),
|
||||||
|
label: "Einstellungen",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onDestinationSelected: (int index) {
|
onDestinationSelected: (int index) {
|
||||||
context.read<NavigationBloc>().add(NavigateToIndex(index: index));
|
context.read<NavigationBloc>().add(NavigateToIndex(index: index));
|
||||||
|
|||||||
@ -4,28 +4,19 @@ import 'package:hl_lieferservice/widget/operations/bloc/operation_state.dart';
|
|||||||
|
|
||||||
class OperationBloc extends Bloc<OperationEvent, OperationState> {
|
class OperationBloc extends Bloc<OperationEvent, OperationState> {
|
||||||
OperationBloc() : super(OperationIdle()) {
|
OperationBloc() : super(OperationIdle()) {
|
||||||
on<LoadOperation>(_loadOperation);
|
|
||||||
on<FailOperation>(_failOperation);
|
on<FailOperation>(_failOperation);
|
||||||
on<FinishOperation>(_finishOperation);
|
on<FinishOperation>(_finishOperation);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadOperation(LoadOperation event, Emitter<OperationState> emit) async {
|
|
||||||
emit(OperationLoading());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _failOperation(FailOperation event, Emitter<OperationState> emit) async {
|
Future<void> _failOperation(FailOperation event, Emitter<OperationState> emit) async {
|
||||||
emit(OperationFailed(message: event.message));
|
emit(OperationFailed(message: event.message));
|
||||||
|
await Future.delayed(const Duration(seconds: 5));
|
||||||
await Future.delayed(Duration(seconds: 5));
|
|
||||||
|
|
||||||
emit(OperationIdle());
|
emit(OperationIdle());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _finishOperation(FinishOperation event, Emitter<OperationState> emit) async {
|
Future<void> _finishOperation(FinishOperation event, Emitter<OperationState> emit) async {
|
||||||
emit(OperationFinished(message: event.message));
|
emit(OperationFinished(message: event.message));
|
||||||
|
await Future.delayed(const Duration(seconds: 5));
|
||||||
await Future.delayed(Duration(seconds: 5));
|
|
||||||
|
|
||||||
emit(OperationIdle());
|
emit(OperationIdle());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
abstract class OperationEvent {}
|
abstract class OperationEvent {}
|
||||||
|
|
||||||
class LoadOperation extends OperationEvent {}
|
|
||||||
|
|
||||||
class FailOperation extends OperationEvent {
|
class FailOperation extends OperationEvent {
|
||||||
String message;
|
String message;
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,6 @@ abstract class OperationState {}
|
|||||||
|
|
||||||
class OperationIdle extends OperationState {}
|
class OperationIdle extends OperationState {}
|
||||||
|
|
||||||
class OperationLoading extends OperationState {}
|
|
||||||
|
|
||||||
class OperationFailed extends OperationState {
|
class OperationFailed extends OperationState {
|
||||||
String message;
|
String message;
|
||||||
|
|
||||||
|
|||||||
@ -4,48 +4,22 @@ import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
|||||||
|
|
||||||
import '../bloc/operation_state.dart';
|
import '../bloc/operation_state.dart';
|
||||||
|
|
||||||
/// OperationViewEnforcer
|
/// Listens to [OperationBloc] and shows SnackBars for success and error
|
||||||
///
|
/// messages. Loading indicators are handled locally by each feature.
|
||||||
/// A view that encapsulates the functionality to react to asynchronous operations.
|
class OperationViewEnforcer extends StatelessWidget {
|
||||||
/// It is capable of showing a loading indicator while an operation is ongoing and it shows
|
|
||||||
/// a error message if the operation failed.
|
|
||||||
class OperationViewEnforcer extends StatefulWidget {
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const OperationViewEnforcer({super.key, required this.child});
|
const OperationViewEnforcer({super.key, required this.child});
|
||||||
@override
|
|
||||||
State<OperationViewEnforcer> createState() => _OperationViewEnforcerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OperationViewEnforcerState extends State<OperationViewEnforcer> {
|
|
||||||
OverlayEntry? _overlayEntry;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_overlayEntry?.remove();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<OperationBloc, OperationState>(
|
return BlocListener<OperationBloc, OperationState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is OperationLoading) {
|
if (state is OperationFinished && state.message != null) {
|
||||||
if (_overlayEntry == null) {
|
|
||||||
_overlayEntry = _createOverlayEntry(context);
|
|
||||||
Overlay.of(context).insert(_overlayEntry!);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_overlayEntry?.remove();
|
|
||||||
_overlayEntry = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state is OperationFinished) {
|
|
||||||
if (state.message != null) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(state.message!)),
|
SnackBar(content: Text(state.message!)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (state is OperationFailed) {
|
if (state is OperationFailed) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -53,20 +27,7 @@ class _OperationViewEnforcerState extends State<OperationViewEnforcer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: widget.child,
|
child: child,
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayEntry _createOverlayEntry(BuildContext context) {
|
|
||||||
return OverlayEntry(
|
|
||||||
builder: (context) => DecoratedBox(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color.fromRGBO(128, 128, 128, 0.8),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: CircularProgressIndicator(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
20
pubspec.lock
20
pubspec.lock
@ -165,10 +165,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -668,26 +668,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1033,10 +1033,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.10"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user