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:
@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user