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(
|
||||
selectedCar: Car(
|
||||
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!,
|
||||
active: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -33,19 +33,15 @@ class _CarSelectionPageState extends State<CarSelectionPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedCar = widget.previousCar;
|
||||
final authState = context.read<AuthBloc>().state as Authenticated;
|
||||
context.read<CarsBloc>().add(CarLoad(teamId: authState.user.number));
|
||||
context.read<CarsBloc>().add(const CarLoad());
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
context.read<CarsBloc>().add(CarAdd(plate: plate));
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -97,12 +93,9 @@ class _CarSelectionPageState extends State<CarSelectionPage> {
|
||||
);
|
||||
}
|
||||
|
||||
final authState = context.read<AuthBloc>().state as Authenticated;
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<CarsBloc>().add(
|
||||
CarLoad(teamId: authState.user.number, force: true),
|
||||
);
|
||||
context.read<CarsBloc>().add(const CarLoad(force: true));
|
||||
},
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
@ -207,14 +200,9 @@ class _CarSelectionPageState extends State<CarSelectionPage> {
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final authState =
|
||||
context.read<AuthBloc>().state
|
||||
as Authenticated;
|
||||
context.read<CarsBloc>().add(
|
||||
CarLoad(
|
||||
teamId: authState.user.number,
|
||||
),
|
||||
);
|
||||
const CarLoad(force: true),
|
||||
);
|
||||
},
|
||||
child: const Text("Erneut versuchen"),
|
||||
),
|
||||
|
||||
@ -1,21 +1,34 @@
|
||||
import 'package:hl_lieferservice/feature/cars/model/selection.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 {
|
||||
static String _keyDate(String userId) => 'car_selection_${userId}_date';
|
||||
static String _keyCarId(String userId) => 'car_selection_${userId}_car_id';
|
||||
static String _keyCarPlate(String userId) =>
|
||||
'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 {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final dateString = prefs.getString(_keyDate(userId));
|
||||
final carId = prefs.getInt(_keyCarId(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;
|
||||
|
||||
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 {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
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!);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,125 +1,125 @@
|
||||
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/domain/entity/car.dart';
|
||||
import 'package:hl_lieferservice/domain/repository/cars_repository.dart';
|
||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
||||
|
||||
import '../../../model/car.dart';
|
||||
import 'cars_event.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> {
|
||||
CarsRepository repository;
|
||||
OperationBloc opBloc;
|
||||
AuthBloc authBloc;
|
||||
|
||||
CarsBloc({required this.repository, required this.opBloc, required this.authBloc})
|
||||
: super(CarsInitial()) {
|
||||
on<CarAdd>(_carAdd);
|
||||
on<CarEdit>(_carEdit);
|
||||
on<CarDelete>(_carDelete);
|
||||
on<CarLoad>(_carLoad);
|
||||
CarsBloc({required this.repository, required this.opBloc})
|
||||
: super(const CarsInitial()) {
|
||||
on<CarLoad>(_handleLoad);
|
||||
on<CarAdd>(_handleAdd);
|
||||
on<CarEdit>(_handleEdit);
|
||||
on<CarDeactivate>(_handleDeactivate);
|
||||
}
|
||||
|
||||
void _handleError(Object e, String fallbackMessage) {
|
||||
if (e is UserUnauthorized) {
|
||||
authBloc.add(SessionExpiredEvent());
|
||||
} else {
|
||||
opBloc.add(FailOperation(message: fallbackMessage));
|
||||
}
|
||||
}
|
||||
final CarsRepository repository;
|
||||
final OperationBloc opBloc;
|
||||
|
||||
Future<void> _carLoad(CarLoad event, Emitter<CarsState> emit) async {
|
||||
// Skip the API call if cars are already loaded and no force-refresh requested.
|
||||
Future<void> _handleLoad(CarLoad event, Emitter<CarsState> emit) async {
|
||||
if (state is CarsLoaded && !event.force) return;
|
||||
|
||||
emit(const CarsLoading());
|
||||
try {
|
||||
emit(CarsLoading());
|
||||
List<Car> cars = await repository.getAll(event.teamId);
|
||||
emit(CarsLoaded(cars: cars, teamId: event.teamId));
|
||||
} catch (e) {
|
||||
if (e is UserUnauthorized) {
|
||||
authBloc.add(SessionExpiredEvent());
|
||||
return;
|
||||
}
|
||||
emit(CarsLoadingFailed());
|
||||
final cars = await repository.listMine();
|
||||
emit(CarsLoaded(cars: cars));
|
||||
} on CarsRepositoryException catch (e) {
|
||||
emit(CarsLoadingFailed(message: e.message));
|
||||
} catch (_) {
|
||||
emit(const CarsLoadingFailed());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _carAdd(CarAdd event, Emitter<CarsState> emit) async {
|
||||
final currentState = state;
|
||||
|
||||
Future<void> _handleAdd(CarAdd event, Emitter<CarsState> emit) async {
|
||||
final current = state;
|
||||
opBloc.add(StartOperation());
|
||||
try {
|
||||
Car newCar = await repository.add(event.teamId, event.plate);
|
||||
|
||||
if (currentState is CarsLoaded) {
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
cars: List<Car>.from(currentState.cars)..add(newCar),
|
||||
),
|
||||
);
|
||||
final created = await repository.create(plate: event.plate);
|
||||
if (current is CarsLoaded) {
|
||||
emit(current.copyWith(cars: [...current.cars, created]));
|
||||
}
|
||||
|
||||
opBloc.add(FinishOperation(message: "Auto erfolgreich hinzugefügt"));
|
||||
} catch (e) {
|
||||
_handleError(e, "Fehler beim Hinzufügen eines Autos");
|
||||
opBloc.add(FinishOperation(message: "Fahrzeug hinzugefügt"));
|
||||
} on CarsRepositoryException catch (e) {
|
||||
opBloc.add(FailOperation(message: e.message));
|
||||
} catch (_) {
|
||||
opBloc.add(
|
||||
FailOperation(message: "Fehler beim Anlegen eines Fahrzeugs"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _carEdit(CarEdit event, Emitter<CarsState> emit) async {
|
||||
final currentState = state;
|
||||
|
||||
Future<void> _handleEdit(CarEdit event, Emitter<CarsState> emit) async {
|
||||
final current = state;
|
||||
opBloc.add(StartOperation());
|
||||
try {
|
||||
await repository.edit(event.teamId, event.newCar);
|
||||
|
||||
if (currentState is CarsLoaded) {
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
cars:
|
||||
List<Car>.from(currentState.cars).map((car) {
|
||||
if (car.id == event.newCar.id) {
|
||||
return event.newCar;
|
||||
}
|
||||
|
||||
return car;
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
opBloc.add(FinishOperation(message: "Auto erfolgreich editiert"));
|
||||
} catch (e) {
|
||||
_handleError(e, "Fehler beim Editieren des Autos");
|
||||
final updated = await repository.update(
|
||||
carId: event.carId,
|
||||
plate: event.plate,
|
||||
active: event.active,
|
||||
);
|
||||
_emitReplaced(current, updated, emit);
|
||||
opBloc.add(FinishOperation(message: "Fahrzeug aktualisiert"));
|
||||
} on CarsRepositoryException catch (e) {
|
||||
opBloc.add(FailOperation(message: e.message));
|
||||
} catch (_) {
|
||||
opBloc.add(
|
||||
FailOperation(message: "Fehler beim Aktualisieren des Fahrzeugs"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _carDelete(CarDelete event, Emitter<CarsState> emit) async {
|
||||
final currentState = state;
|
||||
|
||||
Future<void> _handleDeactivate(
|
||||
CarDeactivate event,
|
||||
Emitter<CarsState> emit,
|
||||
) async {
|
||||
final current = state;
|
||||
opBloc.add(StartOperation());
|
||||
try {
|
||||
await repository.delete(event.carId, event.teamId);
|
||||
|
||||
if (currentState is CarsLoaded) {
|
||||
await repository.update(carId: event.carId, active: false);
|
||||
if (current is CarsLoaded) {
|
||||
// Inaktive Fahrzeuge raus aus der UI-Liste — die Liste zeigt
|
||||
// nur aktive (Default).
|
||||
emit(
|
||||
CarsLoaded(
|
||||
cars: [
|
||||
...currentState.cars.where(
|
||||
(car) => car.id != int.parse(event.carId),
|
||||
),
|
||||
],
|
||||
teamId: currentState.teamId,
|
||||
current.copyWith(
|
||||
cars: current.cars
|
||||
.where((c) => c.id != event.carId)
|
||||
.toList(growable: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
opBloc.add(FinishOperation(message: "Auto erfolgreich gelöscht"));
|
||||
} catch (e) {
|
||||
_handleError(e, "Fehler beim Löschen des Autos");
|
||||
opBloc.add(FinishOperation(message: "Fahrzeug deaktiviert"));
|
||||
} on CarsRepositoryException catch (e) {
|
||||
opBloc.add(FailOperation(message: e.message));
|
||||
} 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';
|
||||
|
||||
abstract class CarEvents {}
|
||||
|
||||
class CarLoad extends CarEvents {
|
||||
String 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});
|
||||
/// Events des CarsBloc nach der Backend-Migration.
|
||||
///
|
||||
/// Anders als zuvor brauchen die Events **keinen** `teamId`/
|
||||
/// `personalnummer`-Parameter — der Server leitet den Account aus dem
|
||||
/// JWT ab. Statt `CarDelete` gibt's nur noch `CarDeactivate` (Soft-
|
||||
/// Delete via `active=false`).
|
||||
sealed class CarEvents {
|
||||
const CarEvents();
|
||||
}
|
||||
|
||||
class CarEdit extends CarEvents {
|
||||
Car newCar;
|
||||
String teamId;
|
||||
|
||||
CarEdit({required this.newCar, required this.teamId});
|
||||
class CarLoad extends CarEvents {
|
||||
/// Pull-to-Refresh setzt `force=true` und umgeht den Cache.
|
||||
final bool force;
|
||||
const CarLoad({this.force = false});
|
||||
}
|
||||
|
||||
class CarAdd extends CarEvents {
|
||||
String teamId;
|
||||
String plate;
|
||||
|
||||
CarAdd({required this.teamId, required this.plate});
|
||||
final String plate;
|
||||
const CarAdd({required this.plate});
|
||||
}
|
||||
|
||||
class CarDelete extends CarEvents {
|
||||
String carId;
|
||||
String teamId;
|
||||
/// PATCH: Kennzeichen ändern und/oder aktivieren-Status setzen.
|
||||
class CarEdit extends CarEvents {
|
||||
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 {}
|
||||
|
||||
class CarsInitial extends CarsState {}
|
||||
|
||||
class CarsLoading extends CarsState {}
|
||||
|
||||
class CarsLoadingFailed extends CarsState {}
|
||||
|
||||
class CarAdded extends CarsState {
|
||||
Car car;
|
||||
|
||||
CarAdded({required this.car});
|
||||
sealed class CarsState {
|
||||
const CarsState();
|
||||
}
|
||||
|
||||
class CarDeleted extends CarsState {
|
||||
String plate;
|
||||
|
||||
CarDeleted({required this.plate});
|
||||
class CarsInitial extends CarsState {
|
||||
const CarsInitial();
|
||||
}
|
||||
|
||||
class CarEdited extends CarsState {
|
||||
Car car;
|
||||
class CarsLoading extends CarsState {
|
||||
const CarsLoading();
|
||||
}
|
||||
|
||||
CarEdited({required this.car});
|
||||
class CarsLoadingFailed extends CarsState {
|
||||
const CarsLoadingFailed({this.message});
|
||||
final String? message;
|
||||
}
|
||||
|
||||
class CarsLoaded extends CarsState {
|
||||
List<Car> cars;
|
||||
String teamId;
|
||||
const CarsLoaded({required this.cars});
|
||||
final List<Car> cars;
|
||||
|
||||
CarsLoaded({required this.cars, required this.teamId});
|
||||
|
||||
CarsLoaded copyWith({List<Car>? cars, String? teamId}) {
|
||||
return CarsLoaded(cars: cars ?? this.cars, teamId: teamId ?? this.teamId);
|
||||
}
|
||||
CarsLoaded copyWith({List<Car>? cars}) =>
|
||||
CarsLoaded(cars: cars ?? this.cars);
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
/*
|
||||
Settings for the driver to select a car for the current workday.
|
||||
*/
|
||||
/// Lokal persistierte Fahrzeug-Auswahl des Fahrers für den aktuellen
|
||||
/// Arbeitstag. UUID-basiert (Backend-Konvention).
|
||||
class CarSelection {
|
||||
final DateTime date;
|
||||
final int? selectedCarId;
|
||||
/// UUID des heute ausgewählten Fahrzeugs.
|
||||
final String? selectedCarId;
|
||||
final String? selectedCarPlate;
|
||||
|
||||
CarSelection({
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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/cars/bloc/cars_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_event.dart';
|
||||
|
||||
@ -9,8 +8,7 @@ class CarsLoadingFailedPage extends StatelessWidget {
|
||||
const CarsLoadingFailedPage({super.key});
|
||||
|
||||
void _onRetry(BuildContext context) {
|
||||
Authenticated state = context.read<AuthBloc>().state as Authenticated;
|
||||
context.read<CarsBloc>().add(CarLoad(teamId: state.user.number));
|
||||
context.read<CarsBloc>().add(const CarLoad(force: true));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -21,9 +19,13 @@ class CarsLoadingFailedPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 72, color: Theme.of(context).colorScheme.error,),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 30),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 72,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 30),
|
||||
child: Text(
|
||||
"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),
|
||||
child: FilledButton(
|
||||
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: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 {
|
||||
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 id) onDelete;
|
||||
final Function(String id, String plate) onEdit;
|
||||
@ -36,12 +37,12 @@ class _CarManagementOverviewState extends State<CarManagementOverview> {
|
||||
);
|
||||
}
|
||||
|
||||
void _removeCar(Car car) async {
|
||||
widget.onDelete(car.id.toString());
|
||||
void _removeCar(Car car) {
|
||||
widget.onDelete(car.id);
|
||||
}
|
||||
|
||||
void _editCar(Car car, String newName) async {
|
||||
widget.onEdit(car.id.toString(), newName);
|
||||
void _editCar(Car car, String newName) {
|
||||
widget.onEdit(car.id, newName);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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/state.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/presentation/car_fail_page.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 {
|
||||
const CarManagementPage({super.key});
|
||||
@ -22,38 +17,34 @@ class CarManagementPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CarManagementPageState extends State<CarManagementPage> {
|
||||
late Authenticated _authState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Load cars
|
||||
_authState = context.read<AuthBloc>().state as Authenticated;
|
||||
context.read<CarsBloc>().add(CarLoad(teamId: _authState.user.number));
|
||||
// Account-Identifizierung läuft serverseitig über den JWT — keine
|
||||
// teamId mehr aus dem AuthState extrahieren.
|
||||
context.read<CarsBloc>().add(const CarLoad());
|
||||
}
|
||||
|
||||
void _add(String plate) {
|
||||
context.read<CarsBloc>().add(
|
||||
CarAdd(teamId: _authState.user.number, plate: plate),
|
||||
);
|
||||
context.read<CarsBloc>().add(CarAdd(plate: plate));
|
||||
}
|
||||
|
||||
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) {
|
||||
final carId = int.parse(id);
|
||||
|
||||
void _remove(String carId) {
|
||||
// Schutz: wenn dieses Fahrzeug aktuell für heute ausgewählt ist,
|
||||
// darf es nicht deaktiviert werden.
|
||||
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.",
|
||||
"Dieses Fahrzeug ist aktuell ausgewählt und kann nicht "
|
||||
"deaktiviert werden. Bitte wähle zuerst ein anderes Fahrzeug "
|
||||
"aus.",
|
||||
),
|
||||
duration: Duration(seconds: 4),
|
||||
),
|
||||
@ -61,68 +52,32 @@ class _CarManagementPageState extends State<CarManagementPage> {
|
||||
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;
|
||||
}
|
||||
// TODO Phase D: Schutz wieder einhängen, sobald TourBloc/Domain
|
||||
// mit UUID-Car-Ids arbeiten und prüfen, ob das Fahrzeug noch
|
||||
// beladene, nicht ausgelieferte Artikel hat.
|
||||
|
||||
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(
|
||||
CarDelete(carId: id, teamId: _authState.user.number),
|
||||
);
|
||||
context.read<CarsBloc>().add(CarDeactivate(carId: carId));
|
||||
}
|
||||
|
||||
void _edit(String id, String plate) {
|
||||
context.read<CarsBloc>().add(
|
||||
CarEdit(
|
||||
newCar: Car(id: int.parse(id), plate: plate),
|
||||
teamId: _authState.user.number,
|
||||
),
|
||||
);
|
||||
void _edit(String carId, String plate) {
|
||||
context.read<CarsBloc>().add(CarEdit(carId: carId, plate: plate));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: BlocConsumer<CarsBloc, CarsState>(
|
||||
listener: (context, state) {
|
||||
if (state is CarsLoaded &&
|
||||
context.read<TourBloc>().state is TourLoaded) {
|
||||
context.read<TourBloc>().add(CarsLoadedEvent(cars: state.cars));
|
||||
}
|
||||
},
|
||||
body: BlocBuilder<CarsBloc, CarsState>(
|
||||
builder: (context, state) {
|
||||
if (state is CarsLoading) {
|
||||
return Center(child: const CircularProgressIndicator());
|
||||
if (state is CarsLoading || state is CarsInitial) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state is CarsLoaded) {
|
||||
return BlocBuilder<CarSelectBloc, CarSelectState>(
|
||||
builder: (context, selectState) {
|
||||
final int? selectedCarId = selectState is CarSelectComplete
|
||||
? selectState.selectedCar.id
|
||||
: null;
|
||||
final String? selectedCarId =
|
||||
selectState is CarSelectComplete
|
||||
? selectState.selectedCar.id
|
||||
: null;
|
||||
return CarManagementOverview(
|
||||
cars: state.cars,
|
||||
selectedCarId: selectedCarId,
|
||||
@ -134,12 +89,10 @@ class _CarManagementPageState extends State<CarManagementPage> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (state is CarsLoadingFailed) {
|
||||
return CarsLoadingFailedPage();
|
||||
return const CarsLoadingFailedPage();
|
||||
}
|
||||
|
||||
return Container();
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
final Tour tour;
|
||||
final int? selectedCarId;
|
||||
final String? 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';
|
||||
|
||||
class DeliveryList extends StatefulWidget {
|
||||
final int? selectedCarId;
|
||||
final String? selectedCarId;
|
||||
final SortType sortType;
|
||||
|
||||
const DeliveryList({super.key, this.selectedCarId, required this.sortType});
|
||||
|
||||
@ -27,7 +27,7 @@ class DeliveryOverview extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DeliveryOverviewState extends State<DeliveryOverview> {
|
||||
int? _selectedCarId;
|
||||
String? _selectedCarId;
|
||||
late SortType _sortType;
|
||||
|
||||
@override
|
||||
|
||||
@ -9,7 +9,7 @@ import 'package:hl_lieferservice/model/delivery.dart';
|
||||
class CustomSortDialog extends StatefulWidget {
|
||||
const CustomSortDialog({super.key, this.selectedCarId});
|
||||
|
||||
final int? selectedCarId;
|
||||
final String? selectedCarId;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CustomSortDialogState();
|
||||
|
||||
@ -32,7 +32,7 @@ class DeliverySelectionPage extends StatefulWidget {
|
||||
|
||||
/// ID des aktuell gewählten Fahrzeugs (Eigene Lieferungen / Ziel von
|
||||
/// Übernahmen).
|
||||
final int selectedCarId;
|
||||
final String selectedCarId;
|
||||
|
||||
@override
|
||||
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
|
||||
/// Fallback, falls die Zuordnung nicht (mehr) im Team enthalten ist —
|
||||
/// z. B. nach Personalwechsel zwischen Tour-Synchronisationen.
|
||||
String _plateFor(int? carId, Tour tour) {
|
||||
String _plateFor(String? carId, Tour tour) {
|
||||
if (carId == null) return "?";
|
||||
final car = tour.driver.cars.firstWhereOrNull((c) => c.id == carId);
|
||||
return car?.plate ?? "?";
|
||||
|
||||
@ -22,7 +22,7 @@ class DeliverySortPage extends StatefulWidget {
|
||||
this.onPhaseAdvanced,
|
||||
});
|
||||
|
||||
final int selectedCarId;
|
||||
final String selectedCarId;
|
||||
|
||||
/// Optionaler Hook, damit die übergeordnete Routing-Stelle nach Erfolg
|
||||
/// auf die nächste Phase wechseln kann (rerender der Beladungs-Page).
|
||||
|
||||
@ -21,7 +21,7 @@ class SortableDeliveryList extends StatefulWidget {
|
||||
this.controller,
|
||||
});
|
||||
|
||||
final int? selectedCarId;
|
||||
final String? selectedCarId;
|
||||
|
||||
/// Optionaler Controller zum Zurücksetzen der Liste durch Eltern-Widgets
|
||||
/// (z. B. Button "Zurücksetzen" in der Page).
|
||||
|
||||
@ -48,7 +48,7 @@ class TourRepository {
|
||||
final index = tour.deliveries.indexWhere(
|
||||
(delivery) => delivery.id == deliveryId,
|
||||
);
|
||||
tour.deliveries[index].carId = int.parse(carId);
|
||||
tour.deliveries[index].carId = carId;
|
||||
|
||||
_tourStream.add(tour);
|
||||
}
|
||||
@ -80,7 +80,7 @@ class TourRepository {
|
||||
|
||||
if (article.scannedAmount < article.amount) {
|
||||
article.scannedAmount += 1;
|
||||
delivery.carId = int.tryParse(carId) ?? delivery.carId;
|
||||
delivery.carId = carId;
|
||||
await service.assignCar(deliveryId, carId);
|
||||
_tourStream.add(tour);
|
||||
return ScanResult.scanned;
|
||||
@ -126,7 +126,7 @@ class TourRepository {
|
||||
if (parentArticle.isFullyScanned) {
|
||||
await service.scanArticle(parentArticle.internalId.toString());
|
||||
parentArticle.scannedAmount += 1;
|
||||
delivery.carId = int.tryParse(carId) ?? delivery.carId;
|
||||
delivery.carId = carId;
|
||||
await service.assignCar(deliveryId, carId);
|
||||
}
|
||||
|
||||
|
||||
@ -126,8 +126,17 @@ class TourService {
|
||||
cars:
|
||||
responseDto.driver.cars
|
||||
.map(
|
||||
(carDto) =>
|
||||
Car(id: int.parse(carDto.id), plate: carDto.plate),
|
||||
// Legacy: alte ERPframe-CarDto hat int-IDs, neue
|
||||
// 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(),
|
||||
teamNumber: int.parse(responseDto.driver.id),
|
||||
|
||||
@ -82,7 +82,7 @@ class _LoadingCustomerPageState extends State<LoadingCustomerPage> {
|
||||
|
||||
/// Aktuell gewähltes Fahrzeug. Wird über den CarSelectBloc synchronisiert,
|
||||
/// einmalig in initState bevor erster build.
|
||||
int? _selectedCarId;
|
||||
String? _selectedCarId;
|
||||
|
||||
/// Erkennt den Übergang "Lieferung läuft → abgeschlossen", damit der
|
||||
/// Listener auch dann robust reagiert, wenn der TourBloc zwischendurch
|
||||
@ -244,7 +244,7 @@ class _LoadingCustomerPageState extends State<LoadingCustomerPage> {
|
||||
// Datenaufbau
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
String? _lookupCarPlate(int? carId, Tour tour) {
|
||||
String? _lookupCarPlate(String? carId, Tour tour) {
|
||||
if (carId == null) return null;
|
||||
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 {
|
||||
const LoadingOverviewPage({super.key});
|
||||
|
||||
String? _lookupCarPlate(int? carId, Tour tour) {
|
||||
String? _lookupCarPlate(String? carId, Tour tour) {
|
||||
if (carId == null) return null;
|
||||
return tour.driver.cars.firstWhereOrNull((c) => c.id == carId)?.plate;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
class Car {
|
||||
Car({required this.id, required this.plate});
|
||||
|
||||
int id;
|
||||
String plate;
|
||||
}
|
||||
/// Backward-compat-Re-Export: alle Aufrufer, die noch
|
||||
/// `package:hl_lieferservice/model/car.dart` importieren, bekommen
|
||||
/// jetzt die neue Domain-Entity. Mit Phase D wandert dieses File raus,
|
||||
/// sobald die letzten Legacy-Aufrufer (tour_service.dart,
|
||||
/// 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;
|
||||
String? specialAgreements;
|
||||
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<ImageNote> images;
|
||||
double prepayment;
|
||||
@ -230,7 +233,7 @@ class Delivery implements Comparable<Delivery> {
|
||||
DeliveryState? state,
|
||||
String? specialAgreements,
|
||||
PaymentOptions? paymentOptions,
|
||||
int? carId,
|
||||
String? carId,
|
||||
List<Note>? notes,
|
||||
List<ImageNote>? images,
|
||||
double? prepayment,
|
||||
@ -371,7 +374,10 @@ class Delivery implements Comparable<Delivery> {
|
||||
),
|
||||
paymentAtDelivery: double.tryParse(dto.paymentAtDelivery) ?? 0.0,
|
||||
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(
|
||||
dto.totalGrossValue == "" ? "0" : dto.totalGrossValue,
|
||||
),
|
||||
|
||||
@ -52,7 +52,7 @@ class Tour {
|
||||
|
||||
Map<String, List<Delivery>> deliveriesPerCar;
|
||||
|
||||
int getFinishedDeliveries(int carId) {
|
||||
int getFinishedDeliveries(String carId) {
|
||||
return deliveries
|
||||
.where((delivery) => delivery.carId == carId)
|
||||
.where((delivery) => delivery.state == DeliveryState.finished)
|
||||
@ -64,7 +64,7 @@ class Tour {
|
||||
/// 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) {
|
||||
bool hasUndeliveredLoadedArticles(String carId) {
|
||||
return deliveries.any((delivery) {
|
||||
if (delivery.carId != carId) 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/presentation/car_selection_enforcer.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/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:holzleitner_api/holzleitner_api.dart' show HolzleitnerApi;
|
||||
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_state.dart';
|
||||
@ -69,10 +69,13 @@ class _DeliveryAppState extends State<DeliveryApp> {
|
||||
CarSelectBloc(repository: CarSelectionRepository()),
|
||||
),
|
||||
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(
|
||||
repository: CarsRepository(service: CarService()),
|
||||
repository: CarsRepositoryImpl(locator<HolzleitnerApi>()),
|
||||
opBloc: context.read<OperationBloc>(),
|
||||
authBloc: context.read<AuthBloc>(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
|
||||
@ -108,7 +108,7 @@ class _HomeState extends State<Home> {
|
||||
Widget _buildForPhase(
|
||||
BuildContext context,
|
||||
DeliveryPhase phase,
|
||||
int selectedCarId,
|
||||
String selectedCarId,
|
||||
) {
|
||||
switch (phase) {
|
||||
case DeliveryPhase.auswaehlen:
|
||||
|
||||
Reference in New Issue
Block a user