Phase C+D-1: Cars-Domain auf Rust-Backend umgestellt
Clean-Arch-Schichten für Cars: - lib/domain/entity/car.dart: UUID-id, accountId (Personalnummer), plate, active. Pendant zum Backend-Schema. - lib/domain/repository/cars_repository.dart: Port — listMine, create, update. Keine teamId/personalnummer-Parameter, der Account fließt serverseitig aus dem JWT. - lib/data/mapper/car_mapper.dart: API-DTO (built_value) → Domain. - lib/data/repository/cars_repository_impl.dart: konkrete Impl via generierter CarsApi (dio), mit DioException → CarsRepositoryException- Übersetzung. Feature-Cars-Refactoring: - CarsBloc nimmt jetzt die Domain-Repository-Schnittstelle. Events: CarLoad/CarAdd/CarEdit/CarDeactivate (statt CarDelete). Keine teamId-Parameter mehr. Kein authBloc-Bezug, Session-Expiry läuft über den globalen Provider-Stream. - CarsState sealed mit CarsInitial/Loading/LoadingFailed/Loaded. - Pages: car_management_page, car_management, car_card, car_fail_page, car_selection_page komplett auf die neue Entity und Event-Signaturen. - Alte lib/feature/cars/service/cars_service.dart und lib/feature/cars/repository/cars_repository.dart gelöscht. CarSelectBloc + Storage: - CarSelection.selectedCarId von int? auf String? umgestellt. - CarSelectionRepository persistiert die UUID jetzt als String; defensive Migration für noch vorhandene int-Werte (alte Pre-Migration-Installations) verwirft den Wert leise und erzwingt Neuauswahl. Konsequenz-Cleanup im Tour-Code (Phase-D-Vorbereitung): - Delivery.carId String? statt int?. - Tour.hasUndeliveredLoadedArticles / getFinishedDeliveries auf String carId. - _selectedCarId / int? carId / int selectedCarId in DeliveryOverview, LoadingCustomerPage/OverviewPage, Home, DeliverySelection/SortPage, DeliveryInfo/List, CustomSortDialog, SortableDeliveryList auf String umgestellt. - TourRepository ersetzt int.parse(carId)/int.tryParse-Zuweisungen direkt durch String. - lib/model/car.dart wird zum Re-Export der neuen Domain-Entity, damit Legacy-Imports während Phase-D-Übergang weiter compilieren. DI: - app.dart: CarsBloc bekommt CarsRepositoryImpl(locator<HolzleitnerApi>()) statt der alten CarsRepository(service: CarService()). Build (flutter build apk --debug) durch, flutter analyze ohne errors.
This commit is contained in:
23
lib/data/mapper/car_mapper.dart
Normal file
23
lib/data/mapper/car_mapper.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:holzleitner_api/holzleitner_api.dart' as api;
|
||||||
|
|
||||||
|
import 'package:hl_lieferservice/domain/entity/car.dart';
|
||||||
|
|
||||||
|
/// Mapper zwischen dem generierten API-DTO `api.Car` (built_value)
|
||||||
|
/// und der Domain-Entity [Car].
|
||||||
|
///
|
||||||
|
/// Nur Read-Mapper hier — Create/Update geht über die Request-DTOs
|
||||||
|
/// `api.CreateCarRequest` / `api.UpdateCarRequest`, die die Repository-
|
||||||
|
/// Impl direkt zusammenbaut.
|
||||||
|
extension ApiCarMapper on api.Car {
|
||||||
|
Car toDomain() => Car(
|
||||||
|
id: id,
|
||||||
|
accountId: accountId,
|
||||||
|
plate: plate,
|
||||||
|
active: active,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liste-Variante als Convenience.
|
||||||
|
extension ApiCarIterableMapper on Iterable<api.Car> {
|
||||||
|
List<Car> toDomainList() => map((c) => c.toDomain()).toList(growable: false);
|
||||||
|
}
|
||||||
96
lib/data/repository/cars_repository_impl.dart
Normal file
96
lib/data/repository/cars_repository_impl.dart
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:holzleitner_api/holzleitner_api.dart' as api;
|
||||||
|
|
||||||
|
import 'package:hl_lieferservice/data/mapper/car_mapper.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/car.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/repository/cars_repository.dart';
|
||||||
|
|
||||||
|
/// Spricht den generierten Holzleitner-API-Client an und mappt die
|
||||||
|
/// `built_value`-DTOs auf die Domain-Entity.
|
||||||
|
///
|
||||||
|
/// Fehler aus dem Backend werden in [CarsRepositoryException]
|
||||||
|
/// übersetzt — der Bloc kennt die HTTP-Schicht nicht. 401 fliegt
|
||||||
|
/// ungefangen durch und wird vom übergreifenden
|
||||||
|
/// HolzleitnerAuthInterceptor-Pfad behandelt (Provider-Stream meldet
|
||||||
|
/// `AuthSessionExpired` bei Refresh-Failure).
|
||||||
|
class CarsRepositoryImpl implements CarsRepository {
|
||||||
|
CarsRepositoryImpl(this._api);
|
||||||
|
|
||||||
|
final api.HolzleitnerApi _api;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Car>> listMine({bool includeInactive = false}) async {
|
||||||
|
try {
|
||||||
|
final response = await _api.getCarsApi().listMyCars(
|
||||||
|
includeInactive: includeInactive,
|
||||||
|
);
|
||||||
|
final cars = response.data?.cars;
|
||||||
|
if (cars == null) return const [];
|
||||||
|
return cars.toDomainList();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw CarsRepositoryException(_describe(e, 'Laden der Fahrzeuge'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Car> create({required String plate}) async {
|
||||||
|
try {
|
||||||
|
final request =
|
||||||
|
api.CreateCarRequest((b) => b..plate = plate);
|
||||||
|
final response =
|
||||||
|
await _api.getCarsApi().createMyCar(createCarRequest: request);
|
||||||
|
final car = response.data?.car;
|
||||||
|
if (car == null) {
|
||||||
|
throw const CarsRepositoryException(
|
||||||
|
'Server lieferte leere Antwort beim Anlegen',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return car.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw CarsRepositoryException(_describe(e, 'Anlegen eines Fahrzeugs'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Car> update({
|
||||||
|
required String carId,
|
||||||
|
String? plate,
|
||||||
|
bool? active,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.UpdateCarRequest((b) {
|
||||||
|
if (plate != null) b.plate = plate;
|
||||||
|
if (active != null) b.active = active;
|
||||||
|
});
|
||||||
|
final response = await _api.getCarsApi().updateMyCar(
|
||||||
|
carId: carId,
|
||||||
|
updateCarRequest: request,
|
||||||
|
);
|
||||||
|
final car = response.data?.car;
|
||||||
|
if (car == null) {
|
||||||
|
throw const CarsRepositoryException(
|
||||||
|
'Server lieferte leere Antwort beim Aktualisieren',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return car.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw CarsRepositoryException(
|
||||||
|
_describe(e, 'Aktualisieren eines Fahrzeugs'),
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Übersetzt eine DioException in eine kompakte, UI-taugliche
|
||||||
|
/// Begründung. Mehr Detail landet im Log, nicht in der Snackbar.
|
||||||
|
String _describe(DioException e, String operation) {
|
||||||
|
final status = e.response?.statusCode;
|
||||||
|
final body = e.response?.data;
|
||||||
|
if (status == 400 && body is Map && body['message'] != null) {
|
||||||
|
return '$operation fehlgeschlagen: ${body['message']}';
|
||||||
|
}
|
||||||
|
if (status == 401) return 'Sitzung abgelaufen';
|
||||||
|
if (status == 404) return 'Fahrzeug nicht gefunden';
|
||||||
|
return '$operation fehlgeschlagen (HTTP ${status ?? 'unbekannt'})';
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/domain/entity/car.dart
Normal file
56
lib/domain/entity/car.dart
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/// Fahrzeug eines Subunternehmer-Accounts — Domain-Entity.
|
||||||
|
///
|
||||||
|
/// Im Gegensatz zum alten `lib/model/car.dart` (int-ID,
|
||||||
|
/// ERPframe-Welt) hält die neue Entity:
|
||||||
|
/// * `id` als UUID-String (Backend-Konvention),
|
||||||
|
/// * `accountId` als Personalnummer (für Audit/Cross-Check, ist
|
||||||
|
/// redundant zur JWT-Identität aber explizit im Payload),
|
||||||
|
/// * `active`-Flag (Soft-Delete statt physisches Löschen).
|
||||||
|
class Car {
|
||||||
|
const Car({
|
||||||
|
required this.id,
|
||||||
|
required this.accountId,
|
||||||
|
required this.plate,
|
||||||
|
required this.active,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// UUID des Fahrzeugs.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Personalnummer des Account-Inhabers.
|
||||||
|
final int accountId;
|
||||||
|
|
||||||
|
/// Kennzeichen (z. B. "BGL-HZ 100").
|
||||||
|
final String plate;
|
||||||
|
|
||||||
|
/// Inaktive Fahrzeuge tauchen in `GET /me/cars` standardmäßig
|
||||||
|
/// nicht auf — sie bleiben aber als FK-Anker für historische
|
||||||
|
/// Audit-Einträge in der Datenbank.
|
||||||
|
final bool active;
|
||||||
|
|
||||||
|
Car copyWith({String? id, int? accountId, String? plate, bool? active}) {
|
||||||
|
return Car(
|
||||||
|
id: id ?? this.id,
|
||||||
|
accountId: accountId ?? this.accountId,
|
||||||
|
plate: plate ?? this.plate,
|
||||||
|
active: active ?? this.active,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is Car &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
id == other.id &&
|
||||||
|
accountId == other.accountId &&
|
||||||
|
plate == other.plate &&
|
||||||
|
active == other.active;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(id, accountId, plate, active);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'Car(id: $id, accountId: $accountId, plate: $plate, active: $active)';
|
||||||
|
}
|
||||||
43
lib/domain/repository/cars_repository.dart
Normal file
43
lib/domain/repository/cars_repository.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'package:hl_lieferservice/domain/entity/car.dart';
|
||||||
|
|
||||||
|
/// Port für Fahrzeug-Stammdaten des angemeldeten Fahrers.
|
||||||
|
///
|
||||||
|
/// Bewusst **kein** `personalnummer`/`teamId`-Parameter: der Account
|
||||||
|
/// wird serverseitig aus dem JWT abgeleitet, der Client muss nichts
|
||||||
|
/// mitschicken. Eine Methode, die einen Account-Filter ermöglicht,
|
||||||
|
/// ist konsequent nicht vorgesehen.
|
||||||
|
///
|
||||||
|
/// Implementierungen werfen anwendungs-spezifische Exceptions
|
||||||
|
/// (z. B. `CarsRepositoryException`) — der Aufrufer fängt sie und
|
||||||
|
/// übersetzt in UI-Zustand.
|
||||||
|
abstract interface class CarsRepository {
|
||||||
|
/// Liste der Fahrzeuge des angemeldeten Accounts.
|
||||||
|
/// `includeInactive=false` blendet deaktivierte Fahrzeuge aus
|
||||||
|
/// (Default für die App-UI).
|
||||||
|
Future<List<Car>> listMine({bool includeInactive = false});
|
||||||
|
|
||||||
|
/// Legt ein neues Fahrzeug mit dem gegebenen Kennzeichen an.
|
||||||
|
/// Wirft, wenn das Kennzeichen für den Account schon existiert.
|
||||||
|
Future<Car> create({required String plate});
|
||||||
|
|
||||||
|
/// Aktualisiert ein bestehendes Fahrzeug.
|
||||||
|
/// Beide Optional-Parameter `null` ist ein No-Op-PATCH.
|
||||||
|
/// Soft-Delete erfolgt über `update(carId: ..., active: false)`.
|
||||||
|
Future<Car> update({
|
||||||
|
required String carId,
|
||||||
|
String? plate,
|
||||||
|
bool? active,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allgemeine Repository-Exception. Konkrete Implementierungen
|
||||||
|
/// können spezifischere Subtypen werfen (z. B. `CarsUnauthorized`).
|
||||||
|
class CarsRepositoryException implements Exception {
|
||||||
|
const CarsRepositoryException(this.message, [this.cause]);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
final Object? cause;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'CarsRepositoryException: $message';
|
||||||
|
}
|
||||||
@ -40,7 +40,13 @@ class CarSelectBloc extends Bloc<CarSelectEvent, CarSelectState> {
|
|||||||
CarSelectComplete(
|
CarSelectComplete(
|
||||||
selectedCar: Car(
|
selectedCar: Car(
|
||||||
id: stored.selectedCarId!,
|
id: stored.selectedCarId!,
|
||||||
|
// accountId/active fließen aus der lokalen Selection
|
||||||
|
// nicht durch — wir persistieren nur (id, plate) als
|
||||||
|
// UI-Pointer. Die Tour-Logik holt die vollständigen
|
||||||
|
// Car-Felder weiterhin aus dem CarsBloc-Listing.
|
||||||
|
accountId: 0,
|
||||||
plate: stored.selectedCarPlate!,
|
plate: stored.selectedCarPlate!,
|
||||||
|
active: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -33,19 +33,15 @@ class _CarSelectionPageState extends State<CarSelectionPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedCar = widget.previousCar;
|
_selectedCar = widget.previousCar;
|
||||||
final authState = context.read<AuthBloc>().state as Authenticated;
|
context.read<CarsBloc>().add(const CarLoad());
|
||||||
context.read<CarsBloc>().add(CarLoad(teamId: authState.user.number));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAddCar() {
|
void _onAddCar() {
|
||||||
final authState = context.read<AuthBloc>().state as Authenticated;
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => CarDialog(
|
builder: (_) => CarDialog(
|
||||||
onAction: (plate) {
|
onAction: (plate) {
|
||||||
context.read<CarsBloc>().add(
|
context.read<CarsBloc>().add(CarAdd(plate: plate));
|
||||||
CarAdd(teamId: authState.user.number, plate: plate),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -97,12 +93,9 @@ class _CarSelectionPageState extends State<CarSelectionPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final authState = context.read<AuthBloc>().state as Authenticated;
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
context.read<CarsBloc>().add(
|
context.read<CarsBloc>().add(const CarLoad(force: true));
|
||||||
CarLoad(teamId: authState.user.number, force: true),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
@ -207,14 +200,9 @@ class _CarSelectionPageState extends State<CarSelectionPage> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final authState =
|
|
||||||
context.read<AuthBloc>().state
|
|
||||||
as Authenticated;
|
|
||||||
context.read<CarsBloc>().add(
|
context.read<CarsBloc>().add(
|
||||||
CarLoad(
|
const CarLoad(force: true),
|
||||||
teamId: authState.user.number,
|
);
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: const Text("Erneut versuchen"),
|
child: const Text("Erneut versuchen"),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,21 +1,34 @@
|
|||||||
import 'package:hl_lieferservice/feature/cars/model/selection.dart';
|
import 'package:hl_lieferservice/feature/cars/model/selection.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
/// Persistiert die Tagesfahrzeug-Auswahl lokal auf dem Gerät pro
|
||||||
|
/// Account. Nach der Backend-Migration sind die Car-IDs UUIDs
|
||||||
|
/// (Strings). Alte Pre-Phase-D-Installations können noch int-Werte
|
||||||
|
/// unter `_car_id` liegen haben — die werden beim Lesen
|
||||||
|
/// stillschweigend ignoriert (Migration durch "neu auswählen").
|
||||||
class CarSelectionRepository {
|
class CarSelectionRepository {
|
||||||
static String _keyDate(String userId) => 'car_selection_${userId}_date';
|
static String _keyDate(String userId) => 'car_selection_${userId}_date';
|
||||||
static String _keyCarId(String userId) => 'car_selection_${userId}_car_id';
|
static String _keyCarId(String userId) => 'car_selection_${userId}_car_id';
|
||||||
static String _keyCarPlate(String userId) =>
|
static String _keyCarPlate(String userId) =>
|
||||||
'car_selection_${userId}_car_plate';
|
'car_selection_${userId}_car_plate';
|
||||||
|
|
||||||
/// Returns the stored [CarSelection] for the given user, or null if nothing
|
|
||||||
/// has been saved yet for that user.
|
|
||||||
Future<CarSelection?> getSelection(String userId) async {
|
Future<CarSelection?> getSelection(String userId) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
final dateString = prefs.getString(_keyDate(userId));
|
final dateString = prefs.getString(_keyDate(userId));
|
||||||
final carId = prefs.getInt(_keyCarId(userId));
|
|
||||||
final plate = prefs.getString(_keyCarPlate(userId));
|
final plate = prefs.getString(_keyCarPlate(userId));
|
||||||
|
|
||||||
|
// Versuche zuerst die neue String-Variante. Falls noch ein altes
|
||||||
|
// int unter dem Key liegt (pre-Migration), wirft getString —
|
||||||
|
// dann beste Strategie: alte Daten droppen, Re-Auswahl erzwingen.
|
||||||
|
String? carId;
|
||||||
|
try {
|
||||||
|
carId = prefs.getString(_keyCarId(userId));
|
||||||
|
} catch (_) {
|
||||||
|
await prefs.remove(_keyCarId(userId));
|
||||||
|
carId = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (dateString == null || carId == null || plate == null) return null;
|
if (dateString == null || carId == null || plate == null) return null;
|
||||||
|
|
||||||
return CarSelection(
|
return CarSelection(
|
||||||
@ -25,12 +38,11 @@ class CarSelectionRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persists the given [selection] for the given user locally on this device.
|
|
||||||
Future<void> saveSelection(String userId, CarSelection selection) async {
|
Future<void> saveSelection(String userId, CarSelection selection) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
await prefs.setString(_keyDate(userId), selection.date.toIso8601String());
|
await prefs.setString(_keyDate(userId), selection.date.toIso8601String());
|
||||||
await prefs.setInt(_keyCarId(userId), selection.selectedCarId!);
|
await prefs.setString(_keyCarId(userId), selection.selectedCarId!);
|
||||||
await prefs.setString(_keyCarPlate(userId), selection.selectedCarPlate!);
|
await prefs.setString(_keyCarPlate(userId), selection.selectedCarPlate!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,125 +1,125 @@
|
|||||||
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/domain/entity/car.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/exceptions.dart';
|
import 'package:hl_lieferservice/domain/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';
|
||||||
|
|
||||||
import '../../../model/car.dart';
|
|
||||||
import 'cars_event.dart';
|
import 'cars_event.dart';
|
||||||
import 'cars_state.dart';
|
import 'cars_state.dart';
|
||||||
|
|
||||||
|
/// Bloc für die Fahrzeugverwaltung des angemeldeten Fahrers.
|
||||||
|
///
|
||||||
|
/// `personalnummer`/`teamId` wird **nicht mehr** in den Events
|
||||||
|
/// übergeben — der Account kommt serverseitig aus dem JWT.
|
||||||
|
/// Session-Expiry geht über den globalen Provider-Stream
|
||||||
|
/// (KeycloakOidcTokenProvider → AuthSessionExpired), nicht über
|
||||||
|
/// dieses Bloc.
|
||||||
class CarsBloc extends Bloc<CarEvents, CarsState> {
|
class CarsBloc extends Bloc<CarEvents, CarsState> {
|
||||||
CarsRepository repository;
|
CarsBloc({required this.repository, required this.opBloc})
|
||||||
OperationBloc opBloc;
|
: super(const CarsInitial()) {
|
||||||
AuthBloc authBloc;
|
on<CarLoad>(_handleLoad);
|
||||||
|
on<CarAdd>(_handleAdd);
|
||||||
CarsBloc({required this.repository, required this.opBloc, required this.authBloc})
|
on<CarEdit>(_handleEdit);
|
||||||
: super(CarsInitial()) {
|
on<CarDeactivate>(_handleDeactivate);
|
||||||
on<CarAdd>(_carAdd);
|
|
||||||
on<CarEdit>(_carEdit);
|
|
||||||
on<CarDelete>(_carDelete);
|
|
||||||
on<CarLoad>(_carLoad);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleError(Object e, String fallbackMessage) {
|
final CarsRepository repository;
|
||||||
if (e is UserUnauthorized) {
|
final OperationBloc opBloc;
|
||||||
authBloc.add(SessionExpiredEvent());
|
|
||||||
} else {
|
|
||||||
opBloc.add(FailOperation(message: fallbackMessage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _carLoad(CarLoad event, Emitter<CarsState> emit) async {
|
Future<void> _handleLoad(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;
|
if (state is CarsLoaded && !event.force) return;
|
||||||
|
|
||||||
|
emit(const CarsLoading());
|
||||||
try {
|
try {
|
||||||
emit(CarsLoading());
|
final cars = await repository.listMine();
|
||||||
List<Car> cars = await repository.getAll(event.teamId);
|
emit(CarsLoaded(cars: cars));
|
||||||
emit(CarsLoaded(cars: cars, teamId: event.teamId));
|
} on CarsRepositoryException catch (e) {
|
||||||
} catch (e) {
|
emit(CarsLoadingFailed(message: e.message));
|
||||||
if (e is UserUnauthorized) {
|
} catch (_) {
|
||||||
authBloc.add(SessionExpiredEvent());
|
emit(const CarsLoadingFailed());
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit(CarsLoadingFailed());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _carAdd(CarAdd event, Emitter<CarsState> emit) async {
|
Future<void> _handleAdd(CarAdd event, Emitter<CarsState> emit) async {
|
||||||
final currentState = state;
|
final current = state;
|
||||||
|
|
||||||
opBloc.add(StartOperation());
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
Car newCar = await repository.add(event.teamId, event.plate);
|
final created = await repository.create(plate: event.plate);
|
||||||
|
if (current is CarsLoaded) {
|
||||||
if (currentState is CarsLoaded) {
|
emit(current.copyWith(cars: [...current.cars, created]));
|
||||||
emit(
|
|
||||||
currentState.copyWith(
|
|
||||||
cars: List<Car>.from(currentState.cars)..add(newCar),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
opBloc.add(FinishOperation(message: "Fahrzeug hinzugefügt"));
|
||||||
opBloc.add(FinishOperation(message: "Auto erfolgreich hinzugefügt"));
|
} on CarsRepositoryException catch (e) {
|
||||||
} catch (e) {
|
opBloc.add(FailOperation(message: e.message));
|
||||||
_handleError(e, "Fehler beim Hinzufügen eines Autos");
|
} catch (_) {
|
||||||
|
opBloc.add(
|
||||||
|
FailOperation(message: "Fehler beim Anlegen eines Fahrzeugs"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _carEdit(CarEdit event, Emitter<CarsState> emit) async {
|
Future<void> _handleEdit(CarEdit event, Emitter<CarsState> emit) async {
|
||||||
final currentState = state;
|
final current = state;
|
||||||
|
|
||||||
opBloc.add(StartOperation());
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await repository.edit(event.teamId, event.newCar);
|
final updated = await repository.update(
|
||||||
|
carId: event.carId,
|
||||||
if (currentState is CarsLoaded) {
|
plate: event.plate,
|
||||||
emit(
|
active: event.active,
|
||||||
currentState.copyWith(
|
);
|
||||||
cars:
|
_emitReplaced(current, updated, emit);
|
||||||
List<Car>.from(currentState.cars).map((car) {
|
opBloc.add(FinishOperation(message: "Fahrzeug aktualisiert"));
|
||||||
if (car.id == event.newCar.id) {
|
} on CarsRepositoryException catch (e) {
|
||||||
return event.newCar;
|
opBloc.add(FailOperation(message: e.message));
|
||||||
}
|
} catch (_) {
|
||||||
|
opBloc.add(
|
||||||
return car;
|
FailOperation(message: "Fehler beim Aktualisieren des Fahrzeugs"),
|
||||||
}).toList(),
|
);
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
opBloc.add(FinishOperation(message: "Auto erfolgreich editiert"));
|
|
||||||
} catch (e) {
|
|
||||||
_handleError(e, "Fehler beim Editieren des Autos");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _carDelete(CarDelete event, Emitter<CarsState> emit) async {
|
Future<void> _handleDeactivate(
|
||||||
final currentState = state;
|
CarDeactivate event,
|
||||||
|
Emitter<CarsState> emit,
|
||||||
|
) async {
|
||||||
|
final current = state;
|
||||||
opBloc.add(StartOperation());
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await repository.delete(event.carId, event.teamId);
|
await repository.update(carId: event.carId, active: false);
|
||||||
|
if (current is CarsLoaded) {
|
||||||
if (currentState is CarsLoaded) {
|
// Inaktive Fahrzeuge raus aus der UI-Liste — die Liste zeigt
|
||||||
|
// nur aktive (Default).
|
||||||
emit(
|
emit(
|
||||||
CarsLoaded(
|
current.copyWith(
|
||||||
cars: [
|
cars: current.cars
|
||||||
...currentState.cars.where(
|
.where((c) => c.id != event.carId)
|
||||||
(car) => car.id != int.parse(event.carId),
|
.toList(growable: false),
|
||||||
),
|
|
||||||
],
|
|
||||||
teamId: currentState.teamId,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
opBloc.add(FinishOperation(message: "Fahrzeug deaktiviert"));
|
||||||
opBloc.add(FinishOperation(message: "Auto erfolgreich gelöscht"));
|
} on CarsRepositoryException catch (e) {
|
||||||
} catch (e) {
|
opBloc.add(FailOperation(message: e.message));
|
||||||
_handleError(e, "Fehler beim Löschen des Autos");
|
} catch (_) {
|
||||||
|
opBloc.add(
|
||||||
|
FailOperation(message: "Fehler beim Deaktivieren des Fahrzeugs"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _emitReplaced(
|
||||||
|
CarsState current,
|
||||||
|
Car updated,
|
||||||
|
Emitter<CarsState> emit,
|
||||||
|
) {
|
||||||
|
if (current is! CarsLoaded) return;
|
||||||
|
emit(
|
||||||
|
current.copyWith(
|
||||||
|
cars: current.cars
|
||||||
|
.map((c) => c.id == updated.id ? updated : c)
|
||||||
|
.toList(growable: false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,35 @@
|
|||||||
import '../../../model/car.dart';
|
/// Events des CarsBloc nach der Backend-Migration.
|
||||||
|
///
|
||||||
abstract class CarEvents {}
|
/// Anders als zuvor brauchen die Events **keinen** `teamId`/
|
||||||
|
/// `personalnummer`-Parameter — der Server leitet den Account aus dem
|
||||||
class CarLoad extends CarEvents {
|
/// JWT ab. Statt `CarDelete` gibt's nur noch `CarDeactivate` (Soft-
|
||||||
String teamId;
|
/// Delete via `active=false`).
|
||||||
|
sealed class CarEvents {
|
||||||
/// If [force] is true the API is always called, bypassing the cache.
|
const CarEvents();
|
||||||
/// Use this for pull-to-refresh. Defaults to false.
|
|
||||||
bool force;
|
|
||||||
|
|
||||||
CarLoad({required this.teamId, this.force = false});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarEdit extends CarEvents {
|
class CarLoad extends CarEvents {
|
||||||
Car newCar;
|
/// Pull-to-Refresh setzt `force=true` und umgeht den Cache.
|
||||||
String teamId;
|
final bool force;
|
||||||
|
const CarLoad({this.force = false});
|
||||||
CarEdit({required this.newCar, required this.teamId});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarAdd extends CarEvents {
|
class CarAdd extends CarEvents {
|
||||||
String teamId;
|
final String plate;
|
||||||
String plate;
|
const CarAdd({required this.plate});
|
||||||
|
|
||||||
CarAdd({required this.teamId, required this.plate});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarDelete extends CarEvents {
|
/// PATCH: Kennzeichen ändern und/oder aktivieren-Status setzen.
|
||||||
String carId;
|
class CarEdit extends CarEvents {
|
||||||
String teamId;
|
final String carId;
|
||||||
|
final String? plate;
|
||||||
|
final bool? active;
|
||||||
|
const CarEdit({required this.carId, this.plate, this.active});
|
||||||
|
}
|
||||||
|
|
||||||
CarDelete({required this.carId, required this.teamId});
|
/// Soft-Delete (setzt `active=false`). Hartes Löschen ist nicht
|
||||||
}
|
/// vorgesehen — Fahrzeuge bleiben als FK-Anker für Audit-Einträge.
|
||||||
|
class CarDeactivate extends CarEvents {
|
||||||
|
final String carId;
|
||||||
|
const CarDeactivate({required this.carId});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,38 +1,26 @@
|
|||||||
import 'package:hl_lieferservice/model/car.dart';
|
import 'package:hl_lieferservice/domain/entity/car.dart';
|
||||||
|
|
||||||
abstract class CarsState {}
|
sealed class CarsState {
|
||||||
|
const CarsState();
|
||||||
class CarsInitial extends CarsState {}
|
|
||||||
|
|
||||||
class CarsLoading extends CarsState {}
|
|
||||||
|
|
||||||
class CarsLoadingFailed extends CarsState {}
|
|
||||||
|
|
||||||
class CarAdded extends CarsState {
|
|
||||||
Car car;
|
|
||||||
|
|
||||||
CarAdded({required this.car});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarDeleted extends CarsState {
|
class CarsInitial extends CarsState {
|
||||||
String plate;
|
const CarsInitial();
|
||||||
|
|
||||||
CarDeleted({required this.plate});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarEdited extends CarsState {
|
class CarsLoading extends CarsState {
|
||||||
Car car;
|
const CarsLoading();
|
||||||
|
}
|
||||||
|
|
||||||
CarEdited({required this.car});
|
class CarsLoadingFailed extends CarsState {
|
||||||
|
const CarsLoadingFailed({this.message});
|
||||||
|
final String? message;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarsLoaded extends CarsState {
|
class CarsLoaded extends CarsState {
|
||||||
List<Car> cars;
|
const CarsLoaded({required this.cars});
|
||||||
String teamId;
|
final List<Car> cars;
|
||||||
|
|
||||||
CarsLoaded({required this.cars, required this.teamId});
|
CarsLoaded copyWith({List<Car>? cars}) =>
|
||||||
|
CarsLoaded(cars: cars ?? this.cars);
|
||||||
CarsLoaded copyWith({List<Car>? cars, String? teamId}) {
|
|
||||||
return CarsLoaded(cars: cars ?? this.cars, teamId: teamId ?? this.teamId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
/*
|
/// Lokal persistierte Fahrzeug-Auswahl des Fahrers für den aktuellen
|
||||||
Settings for the driver to select a car for the current workday.
|
/// Arbeitstag. UUID-basiert (Backend-Konvention).
|
||||||
*/
|
|
||||||
class CarSelection {
|
class CarSelection {
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
final int? selectedCarId;
|
/// UUID des heute ausgewählten Fahrzeugs.
|
||||||
|
final String? selectedCarId;
|
||||||
final String? selectedCarPlate;
|
final String? selectedCarPlate;
|
||||||
|
|
||||||
CarSelection({
|
CarSelection({
|
||||||
|
|||||||
@ -1,7 +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/feature/authentication/bloc/auth_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';
|
||||||
|
|
||||||
@ -9,8 +8,7 @@ class CarsLoadingFailedPage extends StatelessWidget {
|
|||||||
const CarsLoadingFailedPage({super.key});
|
const CarsLoadingFailedPage({super.key});
|
||||||
|
|
||||||
void _onRetry(BuildContext context) {
|
void _onRetry(BuildContext context) {
|
||||||
Authenticated state = context.read<AuthBloc>().state as Authenticated;
|
context.read<CarsBloc>().add(const CarLoad(force: true));
|
||||||
context.read<CarsBloc>().add(CarLoad(teamId: state.user.number));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -21,9 +19,13 @@ class CarsLoadingFailedPage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error_outline, size: 72, color: Theme.of(context).colorScheme.error,),
|
Icon(
|
||||||
Padding(
|
Icons.error_outline,
|
||||||
padding: const EdgeInsets.only(top: 30),
|
size: 72,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 30),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Leider ist es beim Laden der Fahrzeuge zu einem Fehler gekommen.",
|
"Leider ist es beim Laden der Fahrzeuge zu einem Fehler gekommen.",
|
||||||
),
|
),
|
||||||
@ -32,7 +34,7 @@ class CarsLoadingFailedPage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: 30),
|
padding: const EdgeInsets.only(top: 30),
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: () => _onRetry(context),
|
onPressed: () => _onRetry(context),
|
||||||
child: Text("Erneut versuchen"),
|
child: const Text("Erneut versuchen"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import 'package:hl_lieferservice/feature/cars/presentation/car_card.dart';
|
|
||||||
|
|
||||||
import '../../../model/car.dart';
|
|
||||||
import 'car_dialog.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:hl_lieferservice/domain/entity/car.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/presentation/car_card.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/cars/presentation/car_dialog.dart';
|
||||||
|
|
||||||
class CarManagementOverview extends StatefulWidget {
|
class CarManagementOverview extends StatefulWidget {
|
||||||
final List<Car> cars;
|
final List<Car> cars;
|
||||||
final int? selectedCarId;
|
/// UUID des aktuell für heute ausgewählten Fahrzeugs (oder `null`).
|
||||||
|
final String? 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;
|
||||||
@ -36,12 +37,12 @@ class _CarManagementOverviewState extends State<CarManagementOverview> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeCar(Car car) async {
|
void _removeCar(Car car) {
|
||||||
widget.onDelete(car.id.toString());
|
widget.onDelete(car.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editCar(Car car, String newName) async {
|
void _editCar(Car car, String newName) {
|
||||||
widget.onEdit(car.id.toString(), newName);
|
widget.onEdit(car.id, newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -1,7 +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/feature/authentication/bloc/auth_state.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/car_selection/bloc/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/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';
|
||||||
@ -9,10 +8,6 @@ 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';
|
||||||
import 'package:hl_lieferservice/feature/cars/presentation/car_fail_page.dart';
|
import 'package:hl_lieferservice/feature/cars/presentation/car_fail_page.dart';
|
||||||
import 'package:hl_lieferservice/feature/cars/presentation/car_management.dart';
|
import 'package:hl_lieferservice/feature/cars/presentation/car_management.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_state.dart';
|
|
||||||
import 'package:hl_lieferservice/model/car.dart';
|
|
||||||
|
|
||||||
class CarManagementPage extends StatefulWidget {
|
class CarManagementPage extends StatefulWidget {
|
||||||
const CarManagementPage({super.key});
|
const CarManagementPage({super.key});
|
||||||
@ -22,38 +17,34 @@ class CarManagementPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CarManagementPageState extends State<CarManagementPage> {
|
class _CarManagementPageState extends State<CarManagementPage> {
|
||||||
late Authenticated _authState;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// Account-Identifizierung läuft serverseitig über den JWT — keine
|
||||||
// Load cars
|
// teamId mehr aus dem AuthState extrahieren.
|
||||||
_authState = context.read<AuthBloc>().state as Authenticated;
|
context.read<CarsBloc>().add(const CarLoad());
|
||||||
context.read<CarsBloc>().add(CarLoad(teamId: _authState.user.number));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _add(String plate) {
|
void _add(String plate) {
|
||||||
context.read<CarsBloc>().add(
|
context.read<CarsBloc>().add(CarAdd(plate: plate));
|
||||||
CarAdd(teamId: _authState.user.number, plate: plate),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
context.read<CarsBloc>().add(CarLoad(teamId: _authState.user.number, force: true));
|
context.read<CarsBloc>().add(const CarLoad(force: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _remove(String id) {
|
void _remove(String carId) {
|
||||||
final carId = int.parse(id);
|
// Schutz: wenn dieses Fahrzeug aktuell für heute ausgewählt ist,
|
||||||
|
// darf es nicht deaktiviert werden.
|
||||||
final carSelectState = context.read<CarSelectBloc>().state;
|
final carSelectState = context.read<CarSelectBloc>().state;
|
||||||
if (carSelectState is CarSelectComplete &&
|
if (carSelectState is CarSelectComplete &&
|
||||||
carSelectState.selectedCar.id == carId) {
|
carSelectState.selectedCar.id == carId) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"Dieses Fahrzeug ist aktuell ausgewählt und kann nicht gelöscht werden. "
|
"Dieses Fahrzeug ist aktuell ausgewählt und kann nicht "
|
||||||
"Bitte wähle zuerst ein anderes Fahrzeug aus.",
|
"deaktiviert werden. Bitte wähle zuerst ein anderes Fahrzeug "
|
||||||
|
"aus.",
|
||||||
),
|
),
|
||||||
duration: Duration(seconds: 4),
|
duration: Duration(seconds: 4),
|
||||||
),
|
),
|
||||||
@ -61,68 +52,32 @@ class _CarManagementPageState extends State<CarManagementPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final tourState = context.read<TourBloc>().state;
|
// TODO Phase D: Schutz wieder einhängen, sobald TourBloc/Domain
|
||||||
if (tourState is! TourLoaded) {
|
// mit UUID-Car-Ids arbeiten und prüfen, ob das Fahrzeug noch
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
// beladene, nicht ausgelieferte Artikel hat.
|
||||||
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)) {
|
context.read<CarsBloc>().add(CarDeactivate(carId: 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(
|
|
||||||
CarDelete(carId: id, teamId: _authState.user.number),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _edit(String id, String plate) {
|
void _edit(String carId, String plate) {
|
||||||
context.read<CarsBloc>().add(
|
context.read<CarsBloc>().add(CarEdit(carId: carId, plate: plate));
|
||||||
CarEdit(
|
|
||||||
newCar: Car(id: int.parse(id), plate: plate),
|
|
||||||
teamId: _authState.user.number,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: BlocConsumer<CarsBloc, CarsState>(
|
body: BlocBuilder<CarsBloc, CarsState>(
|
||||||
listener: (context, state) {
|
|
||||||
if (state is CarsLoaded &&
|
|
||||||
context.read<TourBloc>().state is TourLoaded) {
|
|
||||||
context.read<TourBloc>().add(CarsLoadedEvent(cars: state.cars));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is CarsLoading) {
|
if (state is CarsLoading || state is CarsInitial) {
|
||||||
return Center(child: const CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is CarsLoaded) {
|
if (state is CarsLoaded) {
|
||||||
return BlocBuilder<CarSelectBloc, CarSelectState>(
|
return BlocBuilder<CarSelectBloc, CarSelectState>(
|
||||||
builder: (context, selectState) {
|
builder: (context, selectState) {
|
||||||
final int? selectedCarId = selectState is CarSelectComplete
|
final String? selectedCarId =
|
||||||
? selectState.selectedCar.id
|
selectState is CarSelectComplete
|
||||||
: null;
|
? selectState.selectedCar.id
|
||||||
|
: null;
|
||||||
return CarManagementOverview(
|
return CarManagementOverview(
|
||||||
cars: state.cars,
|
cars: state.cars,
|
||||||
selectedCarId: selectedCarId,
|
selectedCarId: selectedCarId,
|
||||||
@ -134,12 +89,10 @@ class _CarManagementPageState extends State<CarManagementPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is CarsLoadingFailed) {
|
if (state is CarsLoadingFailed) {
|
||||||
return CarsLoadingFailedPage();
|
return const CarsLoadingFailedPage();
|
||||||
}
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
return Container();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
import 'package:hl_lieferservice/feature/cars/service/cars_service.dart';
|
|
||||||
|
|
||||||
import '../../../model/car.dart';
|
|
||||||
|
|
||||||
class CarsRepository {
|
|
||||||
CarService service;
|
|
||||||
|
|
||||||
CarsRepository({required this.service});
|
|
||||||
|
|
||||||
Future<List<Car>> getAll(String teamId) async {
|
|
||||||
return service.getCars(int.parse(teamId));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> delete(String carId, String teamId) async {
|
|
||||||
return service.removeCar(int.parse(carId), int.parse(teamId));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> edit(String teamId, Car newCar) async {
|
|
||||||
return service.editCar(newCar);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Car> add(String teamId, String plate) async {
|
|
||||||
return service.addCar(plate, int.parse(teamId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/authentication/exceptions.dart';
|
|
||||||
import 'package:hl_lieferservice/util.dart';
|
|
||||||
import 'package:http/http.dart';
|
|
||||||
|
|
||||||
import '../../../dto/basic_response.dart';
|
|
||||||
import '../../../dto/car_add_response.dart';
|
|
||||||
import '../../../dto/car_get_response.dart';
|
|
||||||
import '../../../model/car.dart';
|
|
||||||
|
|
||||||
class CarService {
|
|
||||||
CarService();
|
|
||||||
|
|
||||||
Future<Car> addCar(String plate, int teamId) async {
|
|
||||||
try {
|
|
||||||
debugPrint(jsonEncode({"team_id": teamId.toString(), "plate": plate}));
|
|
||||||
|
|
||||||
var response = await post(
|
|
||||||
urlBuilder("_web_addCar"),
|
|
||||||
headers: getSessionOrThrow(),
|
|
||||||
body: {"team_id": teamId.toString(), "plate": plate},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == HttpStatus.unauthorized) {
|
|
||||||
throw UserUnauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> responseJson = jsonDecode(response.body);
|
|
||||||
CarAddResponseDTO responseDto = CarAddResponseDTO.fromJson(responseJson);
|
|
||||||
|
|
||||||
if (responseDto.succeeded == true) {
|
|
||||||
return Car(
|
|
||||||
id: int.parse(responseDto.car.id),
|
|
||||||
plate: responseDto.car.plate,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw responseDto.message;
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
debugPrint("ERROR WHILE ADDING CAR");
|
|
||||||
debugPrint(e.toString());
|
|
||||||
debugPrint(st.toString());
|
|
||||||
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> editCar(Car car) async {
|
|
||||||
try {
|
|
||||||
var response = await post(
|
|
||||||
urlBuilder("_web_editCar"),
|
|
||||||
headers: getSessionOrThrow(),
|
|
||||||
body: {"id": car.id.toString(), "plate": car.plate},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == HttpStatus.unauthorized) {
|
|
||||||
throw UserUnauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> responseJson = jsonDecode(response.body);
|
|
||||||
BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson);
|
|
||||||
|
|
||||||
if (responseDto.succeeded == true) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw responseDto.message;
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
debugPrint("ERROR WHILE EDITING CAR ${car.id}");
|
|
||||||
debugPrint("$e");
|
|
||||||
debugPrint(st.toString());
|
|
||||||
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeCar(int carId, int teamId) async {
|
|
||||||
try {
|
|
||||||
var response = await post(
|
|
||||||
urlBuilder("_web_removeCar"),
|
|
||||||
headers: getSessionOrThrow(),
|
|
||||||
body: {"team_id": teamId.toString(), "id": carId.toString()},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == HttpStatus.unauthorized) {
|
|
||||||
throw UserUnauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> responseJson = jsonDecode(response.body);
|
|
||||||
BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson);
|
|
||||||
|
|
||||||
if (responseDto.succeeded == true) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw responseDto.message;
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
debugPrint("ERROR WHILE REMOVING CAR");
|
|
||||||
debugPrint(e.toString());
|
|
||||||
debugPrint(st.toString());
|
|
||||||
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Car>> getCars(int teamId) async {
|
|
||||||
try {
|
|
||||||
debugPrint(teamId.toString());
|
|
||||||
|
|
||||||
var response = await post(
|
|
||||||
urlBuilder("_web_getCars"),
|
|
||||||
headers: getSessionOrThrow(),
|
|
||||||
body: {"team_id": teamId.toString()},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == HttpStatus.unauthorized) {
|
|
||||||
throw UserUnauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> responseJson = jsonDecode(response.body);
|
|
||||||
CarGetResponseDTO responseDto = CarGetResponseDTO.fromJson(responseJson);
|
|
||||||
|
|
||||||
if (responseDto.succeeded == true) {
|
|
||||||
return responseDto.cars!
|
|
||||||
.map((carDto) => Car(id: int.parse(carDto.id), plate: carDto.plate))
|
|
||||||
.toList();
|
|
||||||
} else {
|
|
||||||
throw responseDto.message;
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
debugPrint("ERROR WHILE FETCHING CARS");
|
|
||||||
debugPrint(e.toString());
|
|
||||||
debugPrint(st.toString());
|
|
||||||
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@ import 'package:intl/intl.dart';
|
|||||||
|
|
||||||
class DeliveryInfo extends StatelessWidget {
|
class DeliveryInfo extends StatelessWidget {
|
||||||
final Tour tour;
|
final Tour tour;
|
||||||
final int? selectedCarId;
|
final String? selectedCarId;
|
||||||
|
|
||||||
const DeliveryInfo({super.key, required this.tour, this.selectedCarId});
|
const DeliveryInfo({super.key, required this.tour, this.selectedCarId});
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import 'package:hl_lieferservice/model/delivery.dart';
|
|||||||
import 'delivery_item.dart';
|
import 'delivery_item.dart';
|
||||||
|
|
||||||
class DeliveryList extends StatefulWidget {
|
class DeliveryList extends StatefulWidget {
|
||||||
final int? selectedCarId;
|
final String? selectedCarId;
|
||||||
final SortType sortType;
|
final SortType sortType;
|
||||||
|
|
||||||
const DeliveryList({super.key, this.selectedCarId, required this.sortType});
|
const DeliveryList({super.key, this.selectedCarId, required this.sortType});
|
||||||
|
|||||||
@ -27,7 +27,7 @@ class DeliveryOverview extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DeliveryOverviewState extends State<DeliveryOverview> {
|
class _DeliveryOverviewState extends State<DeliveryOverview> {
|
||||||
int? _selectedCarId;
|
String? _selectedCarId;
|
||||||
late SortType _sortType;
|
late SortType _sortType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import 'package:hl_lieferservice/model/delivery.dart';
|
|||||||
class CustomSortDialog extends StatefulWidget {
|
class CustomSortDialog extends StatefulWidget {
|
||||||
const CustomSortDialog({super.key, this.selectedCarId});
|
const CustomSortDialog({super.key, this.selectedCarId});
|
||||||
|
|
||||||
final int? selectedCarId;
|
final String? selectedCarId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _CustomSortDialogState();
|
State<StatefulWidget> createState() => _CustomSortDialogState();
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class DeliverySelectionPage extends StatefulWidget {
|
|||||||
|
|
||||||
/// ID des aktuell gewählten Fahrzeugs (Eigene Lieferungen / Ziel von
|
/// ID des aktuell gewählten Fahrzeugs (Eigene Lieferungen / Ziel von
|
||||||
/// Übernahmen).
|
/// Übernahmen).
|
||||||
final int selectedCarId;
|
final String selectedCarId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _DeliverySelectionPageState();
|
State<StatefulWidget> createState() => _DeliverySelectionPageState();
|
||||||
@ -52,7 +52,7 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
|||||||
/// Sucht das Plate eines Autos in der Tour-Driver-Liste. Liefert "?" als
|
/// Sucht das Plate eines Autos in der Tour-Driver-Liste. Liefert "?" als
|
||||||
/// Fallback, falls die Zuordnung nicht (mehr) im Team enthalten ist —
|
/// Fallback, falls die Zuordnung nicht (mehr) im Team enthalten ist —
|
||||||
/// z. B. nach Personalwechsel zwischen Tour-Synchronisationen.
|
/// z. B. nach Personalwechsel zwischen Tour-Synchronisationen.
|
||||||
String _plateFor(int? carId, Tour tour) {
|
String _plateFor(String? carId, Tour tour) {
|
||||||
if (carId == null) return "?";
|
if (carId == null) return "?";
|
||||||
final car = tour.driver.cars.firstWhereOrNull((c) => c.id == carId);
|
final car = tour.driver.cars.firstWhereOrNull((c) => c.id == carId);
|
||||||
return car?.plate ?? "?";
|
return car?.plate ?? "?";
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class DeliverySortPage extends StatefulWidget {
|
|||||||
this.onPhaseAdvanced,
|
this.onPhaseAdvanced,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int selectedCarId;
|
final String selectedCarId;
|
||||||
|
|
||||||
/// Optionaler Hook, damit die übergeordnete Routing-Stelle nach Erfolg
|
/// Optionaler Hook, damit die übergeordnete Routing-Stelle nach Erfolg
|
||||||
/// auf die nächste Phase wechseln kann (rerender der Beladungs-Page).
|
/// auf die nächste Phase wechseln kann (rerender der Beladungs-Page).
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class SortableDeliveryList extends StatefulWidget {
|
|||||||
this.controller,
|
this.controller,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int? selectedCarId;
|
final String? selectedCarId;
|
||||||
|
|
||||||
/// Optionaler Controller zum Zurücksetzen der Liste durch Eltern-Widgets
|
/// Optionaler Controller zum Zurücksetzen der Liste durch Eltern-Widgets
|
||||||
/// (z. B. Button "Zurücksetzen" in der Page).
|
/// (z. B. Button "Zurücksetzen" in der Page).
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class TourRepository {
|
|||||||
final index = tour.deliveries.indexWhere(
|
final index = tour.deliveries.indexWhere(
|
||||||
(delivery) => delivery.id == deliveryId,
|
(delivery) => delivery.id == deliveryId,
|
||||||
);
|
);
|
||||||
tour.deliveries[index].carId = int.parse(carId);
|
tour.deliveries[index].carId = carId;
|
||||||
|
|
||||||
_tourStream.add(tour);
|
_tourStream.add(tour);
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ 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;
|
delivery.carId = carId;
|
||||||
await service.assignCar(deliveryId, carId);
|
await service.assignCar(deliveryId, carId);
|
||||||
_tourStream.add(tour);
|
_tourStream.add(tour);
|
||||||
return ScanResult.scanned;
|
return ScanResult.scanned;
|
||||||
@ -126,7 +126,7 @@ class TourRepository {
|
|||||||
if (parentArticle.isFullyScanned) {
|
if (parentArticle.isFullyScanned) {
|
||||||
await service.scanArticle(parentArticle.internalId.toString());
|
await service.scanArticle(parentArticle.internalId.toString());
|
||||||
parentArticle.scannedAmount += 1;
|
parentArticle.scannedAmount += 1;
|
||||||
delivery.carId = int.tryParse(carId) ?? delivery.carId;
|
delivery.carId = carId;
|
||||||
await service.assignCar(deliveryId, carId);
|
await service.assignCar(deliveryId, carId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -126,8 +126,17 @@ class TourService {
|
|||||||
cars:
|
cars:
|
||||||
responseDto.driver.cars
|
responseDto.driver.cars
|
||||||
.map(
|
.map(
|
||||||
(carDto) =>
|
// Legacy: alte ERPframe-CarDto hat int-IDs, neue
|
||||||
Car(id: int.parse(carDto.id), plate: carDto.plate),
|
// Domain-Entity erwartet UUID-Strings. Wir
|
||||||
|
// stringifizieren die int-ID und füllen
|
||||||
|
// accountId/active mit Stub-Werten — der ganze
|
||||||
|
// Service wird in Phase D entfernt.
|
||||||
|
(carDto) => Car(
|
||||||
|
id: carDto.id,
|
||||||
|
accountId: 0,
|
||||||
|
plate: carDto.plate,
|
||||||
|
active: true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
teamNumber: int.parse(responseDto.driver.id),
|
teamNumber: int.parse(responseDto.driver.id),
|
||||||
|
|||||||
@ -82,7 +82,7 @@ class _LoadingCustomerPageState extends State<LoadingCustomerPage> {
|
|||||||
|
|
||||||
/// Aktuell gewähltes Fahrzeug. Wird über den CarSelectBloc synchronisiert,
|
/// Aktuell gewähltes Fahrzeug. Wird über den CarSelectBloc synchronisiert,
|
||||||
/// einmalig in initState bevor erster build.
|
/// einmalig in initState bevor erster build.
|
||||||
int? _selectedCarId;
|
String? _selectedCarId;
|
||||||
|
|
||||||
/// Erkennt den Übergang "Lieferung läuft → abgeschlossen", damit der
|
/// Erkennt den Übergang "Lieferung läuft → abgeschlossen", damit der
|
||||||
/// Listener auch dann robust reagiert, wenn der TourBloc zwischendurch
|
/// Listener auch dann robust reagiert, wenn der TourBloc zwischendurch
|
||||||
@ -244,7 +244,7 @@ class _LoadingCustomerPageState extends State<LoadingCustomerPage> {
|
|||||||
// Datenaufbau
|
// Datenaufbau
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
String? _lookupCarPlate(int? carId, Tour tour) {
|
String? _lookupCarPlate(String? carId, Tour tour) {
|
||||||
if (carId == null) return null;
|
if (carId == null) return null;
|
||||||
return tour.driver.cars.firstWhereOrNull((c) => c.id == carId)?.plate;
|
return tour.driver.cars.firstWhereOrNull((c) => c.id == carId)?.plate;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import 'package:hl_lieferservice/widget/phase_stepper/phase_stepper.dart';
|
|||||||
class LoadingOverviewPage extends StatelessWidget {
|
class LoadingOverviewPage extends StatelessWidget {
|
||||||
const LoadingOverviewPage({super.key});
|
const LoadingOverviewPage({super.key});
|
||||||
|
|
||||||
String? _lookupCarPlate(int? carId, Tour tour) {
|
String? _lookupCarPlate(String? carId, Tour tour) {
|
||||||
if (carId == null) return null;
|
if (carId == null) return null;
|
||||||
return tour.driver.cars.firstWhereOrNull((c) => c.id == carId)?.plate;
|
return tour.driver.cars.firstWhereOrNull((c) => c.id == carId)?.plate;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
class Car {
|
/// Backward-compat-Re-Export: alle Aufrufer, die noch
|
||||||
Car({required this.id, required this.plate});
|
/// `package:hl_lieferservice/model/car.dart` importieren, bekommen
|
||||||
|
/// jetzt die neue Domain-Entity. Mit Phase D wandert dieses File raus,
|
||||||
int id;
|
/// sobald die letzten Legacy-Aufrufer (tour_service.dart,
|
||||||
String plate;
|
/// tour_event.dart) auf die echte Domain-Schicht umgestellt sind.
|
||||||
}
|
export 'package:hl_lieferservice/domain/entity/car.dart';
|
||||||
|
|||||||
@ -202,7 +202,10 @@ class Delivery implements Comparable<Delivery> {
|
|||||||
DeliveryState state;
|
DeliveryState state;
|
||||||
String? specialAgreements;
|
String? specialAgreements;
|
||||||
PaymentOptions paymentOptions;
|
PaymentOptions paymentOptions;
|
||||||
int? carId;
|
/// UUID des zugewiesenen Fahrzeugs (Backend-Konvention).
|
||||||
|
/// War vor der Backend-Migration `int?` — die Konversion ist Teil
|
||||||
|
/// der Phase-D-Vorbereitung.
|
||||||
|
String? carId;
|
||||||
List<Note> notes;
|
List<Note> notes;
|
||||||
List<ImageNote> images;
|
List<ImageNote> images;
|
||||||
double prepayment;
|
double prepayment;
|
||||||
@ -230,7 +233,7 @@ class Delivery implements Comparable<Delivery> {
|
|||||||
DeliveryState? state,
|
DeliveryState? state,
|
||||||
String? specialAgreements,
|
String? specialAgreements,
|
||||||
PaymentOptions? paymentOptions,
|
PaymentOptions? paymentOptions,
|
||||||
int? carId,
|
String? carId,
|
||||||
List<Note>? notes,
|
List<Note>? notes,
|
||||||
List<ImageNote>? images,
|
List<ImageNote>? images,
|
||||||
double? prepayment,
|
double? prepayment,
|
||||||
@ -371,7 +374,10 @@ class Delivery implements Comparable<Delivery> {
|
|||||||
),
|
),
|
||||||
paymentAtDelivery: double.tryParse(dto.paymentAtDelivery) ?? 0.0,
|
paymentAtDelivery: double.tryParse(dto.paymentAtDelivery) ?? 0.0,
|
||||||
images: dto.images.map(ImageNote.fromDTO).toList(),
|
images: dto.images.map(ImageNote.fromDTO).toList(),
|
||||||
carId: int.tryParse(dto.carId),
|
// Legacy: ERPframe-Backend liefert int-Strings; im neuen Backend
|
||||||
|
// sind das UUID-Strings. Beide werden hier transparent
|
||||||
|
// weitergereicht.
|
||||||
|
carId: dto.carId.isEmpty ? null : dto.carId,
|
||||||
totalGrossValue: double.parse(
|
totalGrossValue: double.parse(
|
||||||
dto.totalGrossValue == "" ? "0" : dto.totalGrossValue,
|
dto.totalGrossValue == "" ? "0" : dto.totalGrossValue,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class Tour {
|
|||||||
|
|
||||||
Map<String, List<Delivery>> deliveriesPerCar;
|
Map<String, List<Delivery>> deliveriesPerCar;
|
||||||
|
|
||||||
int getFinishedDeliveries(int carId) {
|
int getFinishedDeliveries(String carId) {
|
||||||
return deliveries
|
return deliveries
|
||||||
.where((delivery) => delivery.carId == carId)
|
.where((delivery) => delivery.carId == carId)
|
||||||
.where((delivery) => delivery.state == DeliveryState.finished)
|
.where((delivery) => delivery.state == DeliveryState.finished)
|
||||||
@ -64,7 +64,7 @@ class Tour {
|
|||||||
/// that has not been finished yet. Scannable articles count when their
|
/// that has not been finished yet. Scannable articles count when their
|
||||||
/// effective scanned amount (scanned minus removed) is positive; non-scannable
|
/// effective scanned amount (scanned minus removed) is positive; non-scannable
|
||||||
/// articles count when their target amount is greater than zero.
|
/// articles count when their target amount is greater than zero.
|
||||||
bool hasUndeliveredLoadedArticles(int carId) {
|
bool hasUndeliveredLoadedArticles(String carId) {
|
||||||
return deliveries.any((delivery) {
|
return deliveries.any((delivery) {
|
||||||
if (delivery.carId != carId) return false;
|
if (delivery.carId != carId) return false;
|
||||||
if (delivery.state == DeliveryState.finished) return false;
|
if (delivery.state == DeliveryState.finished) return false;
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import 'package:hl_lieferservice/main.dart' show locator;
|
|||||||
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.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/presentation/car_selection_enforcer.dart';
|
||||||
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
|
||||||
|
import 'package:hl_lieferservice/data/repository/cars_repository_impl.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/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:holzleitner_api/holzleitner_api.dart' show HolzleitnerApi;
|
||||||
import 'package:hl_lieferservice/feature/cars/service/cars_service.dart';
|
|
||||||
import 'package:hl_lieferservice/feature/delivery/bloc/phase_bloc.dart';
|
import 'package:hl_lieferservice/feature/delivery/bloc/phase_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';
|
||||||
@ -69,10 +69,13 @@ class _DeliveryAppState extends State<DeliveryApp> {
|
|||||||
CarSelectBloc(repository: CarSelectionRepository()),
|
CarSelectBloc(repository: CarSelectionRepository()),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
|
// Phase-D-Migration: produktive CarsRepository-Impl
|
||||||
|
// gegen das generierte Rust-Backend-API. Account-Filter
|
||||||
|
// serverseitig aus dem JWT, deshalb braucht der Bloc
|
||||||
|
// keinen AuthBloc-Bezug mehr.
|
||||||
create: (context) => CarsBloc(
|
create: (context) => CarsBloc(
|
||||||
repository: CarsRepository(service: CarService()),
|
repository: CarsRepositoryImpl(locator<HolzleitnerApi>()),
|
||||||
opBloc: context.read<OperationBloc>(),
|
opBloc: context.read<OperationBloc>(),
|
||||||
authBloc: context.read<AuthBloc>(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
|
|||||||
@ -108,7 +108,7 @@ class _HomeState extends State<Home> {
|
|||||||
Widget _buildForPhase(
|
Widget _buildForPhase(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
DeliveryPhase phase,
|
DeliveryPhase phase,
|
||||||
int selectedCarId,
|
String selectedCarId,
|
||||||
) {
|
) {
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case DeliveryPhase.auswaehlen:
|
case DeliveryPhase.auswaehlen:
|
||||||
|
|||||||
Reference in New Issue
Block a user