This commit is contained in:
Dennis Nemec
2026-04-28 13:03:09 +02:00
parent de8668c11a
commit 2470299a10
53 changed files with 2409 additions and 1433 deletions

View File

@ -1,80 +1,86 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/model/delivery.dart' show DeliveryState;
import 'package:hl_lieferservice/model/tour.dart';
import 'package:intl/intl.dart';
class DeliveryInfo extends StatelessWidget {
final Tour tour;
final int? selectedCarId;
const DeliveryInfo({super.key, required this.tour});
const DeliveryInfo({super.key, required this.tour, this.selectedCarId});
@override
Widget build(BuildContext context) {
String date = DateFormat("dd.MM.yyyy").format(tour.date);
String amountDeliveries = tour.deliveries.length.toString();
final String date = DateFormat("dd.MM.yyyy").format(tour.date);
final relevantDeliveries = selectedCarId != null
? tour.deliveries.where((d) => d.carId == selectedCarId).toList()
: tour.deliveries;
final total = relevantDeliveries.length;
final done = relevantDeliveries
.where((d) => d.state == DeliveryState.finished)
.length;
final progress = total > 0 ? done / total : 0.0;
final allDone = total > 0 && done == total;
return Padding(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
"Informationen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
SizedBox(
width: double.infinity,
child: Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15),
child: SizedBox(
width: double.infinity,
child: Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.calendar_month),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text("Datum"),
),
],
const Icon(Icons.calendar_month),
const Padding(
padding: EdgeInsets.only(left: 5),
child: Text("Datum"),
),
Text(date),
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.local_shipping_outlined),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text("Lieferungen"),
),
],
),
Text(amountDeliveries),
],
),
),
Text(date),
],
),
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.local_shipping_outlined),
const Padding(
padding: EdgeInsets.only(left: 5),
child: Text("Lieferungen"),
),
],
),
Text("$done / $total"),
],
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progress,
minHeight: 6,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
allDone ? Colors.green : Theme.of(context).primaryColor,
),
),
),
],
),
),
],
),
),
);
}

View File

@ -1,5 +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/model/delivery.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart';
@ -18,60 +19,132 @@ class DeliveryListItem extends StatelessWidget {
required this.distance,
});
Widget _leading(BuildContext context) {
if (delivery.state == DeliveryState.finished) {
return Icon(Icons.check_circle, color: Colors.green);
}
if (delivery.state == DeliveryState.canceled) {
return Icon(Icons.cancel_rounded, color: Colors.red);
}
if (delivery.state == DeliveryState.onhold) {
return Icon(Icons.pause_circle, color: Colors.orange);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.location_on, color: Theme.of(context).primaryColor),
Text("${distance.toStringAsFixed(2)}km"),
],
);
}
void _goToDelivery(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => BlocProvider(
create:
(context) => NoteBloc(
deliveryId: delivery.id,
opBloc: context.read<OperationBloc>(),
repository: NoteRepository(
service: NoteService(),
),
),
child: DeliveryDetail(deliveryId: delivery.id),
),
builder: (context) => BlocProvider(
create: (context) => NoteBloc(
deliveryId: delivery.id,
opBloc: context.read<OperationBloc>(),
authBloc: context.read<AuthBloc>(),
repository: NoteRepository(service: NoteService()),
),
child: DeliveryDetail(deliveryId: delivery.id),
),
),
);
}
(Color, Color, IconData, String) _stateStyle(BuildContext context) {
switch (delivery.state) {
case DeliveryState.finished:
return (
Colors.green.withValues(alpha: 0.07),
Colors.green.withValues(alpha: 0.35),
Icons.check_circle_rounded,
"Abgeschlossen",
);
case DeliveryState.canceled:
return (
Colors.red.withValues(alpha: 0.07),
Colors.red.withValues(alpha: 0.35),
Icons.cancel_rounded,
"Storniert",
);
case DeliveryState.onhold:
return (
Colors.orange.withValues(alpha: 0.07),
Colors.orange.withValues(alpha: 0.35),
Icons.pause_circle_rounded,
"Pausiert",
);
case DeliveryState.ongoing:
return (
Theme.of(context).colorScheme.surfaceContainerLow,
Colors.transparent,
Icons.local_shipping_outlined,
"${distance.toStringAsFixed(1)} km",
);
}
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
delivery.customer.name,
style: Theme.of(context).textTheme.titleMedium,
final (cardColor, borderColor, icon, statusLabel) = _stateStyle(context);
final isOngoing = delivery.state == DeliveryState.ongoing;
final iconColor = switch (delivery.state) {
DeliveryState.finished => Colors.green,
DeliveryState.canceled => Colors.red,
DeliveryState.onhold => Colors.orange,
DeliveryState.ongoing => Theme.of(context).primaryColor,
};
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
elevation: 0,
color: cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: borderColor),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _goToDelivery(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(icon, color: iconColor, size: 28),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
delivery.customer.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: isOngoing ? null : iconColor,
),
),
const SizedBox(height: 2),
Text(
delivery.customer.address.toString(),
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
statusLabel,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isOngoing
? Theme.of(context).colorScheme.onSurfaceVariant
: iconColor,
),
),
const SizedBox(height: 4),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
],
),
),
),
leading: _leading(context),
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
subtitle: Text(delivery.customer.address.toString()),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () => _goToDelivery(context),
);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
import 'package:hl_lieferservice/feature/delivery/overview/model/sorting_information.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
import 'package:hl_lieferservice/model/delivery.dart';
@ -56,79 +56,66 @@ class _DeliveryListState extends State<DeliveryList> {
builder: (context, state) {
final currentState = state;
if (currentState is TourLoaded) {
List<Delivery> deliveries =
currentState.tour.deliveries
.where(
(delivery) =>
delivery.carId == widget.selectedCarId &&
delivery.allArticlesScanned() &&
delivery.state != DeliveryState.finished,
)
.toList();
if (widget.sortType == SortType.custom) {
return _showCustomSortedList(
currentState.tour.deliveries,
currentState.sortingInformation[widget.selectedCarId.toString()] ?? [],
currentState.distances ?? {},
);
}
List<Delivery> finishedDeliveries =
currentState.tour.deliveries
.where(
(delivery) =>
delivery.state == DeliveryState.finished &&
delivery.carId == widget.selectedCarId,
)
.toList();
final allDeliveries = currentState.tour.deliveries
.where((d) => d.carId == widget.selectedCarId)
.toList();
if (deliveries.isEmpty) {
if (allDeliveries.isEmpty) {
return ListView(
physics: NeverScrollableScrollPhysics(),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
children: [
Center(child: const Text("Keine Auslieferungen gefunden")),
children: const [
Center(child: Text("Keine Auslieferungen gefunden")),
],
);
}
final ongoing = allDeliveries
.where((d) => d.state == DeliveryState.ongoing)
.toList();
final nonOngoing = allDeliveries
.where((d) => d.state != DeliveryState.ongoing)
.toList();
int Function(Delivery, Delivery) comparator;
switch (widget.sortType) {
case SortType.custom:
return _showCustomSortedList(
currentState.tour.deliveries,
currentState.sortingInformation[widget.selectedCarId.toString()] ?? [],
currentState.distances ?? {},
);
case SortType.nameAsc:
deliveries.sort(
(a, b) => a.customer.name.compareTo(b.customer.name),
);
comparator = (a, b) => a.customer.name.compareTo(b.customer.name);
break;
case SortType.nameDesc:
deliveries.sort(
(a, b) => b.customer.name.compareTo(a.customer.name),
);
comparator = (a, b) => b.customer.name.compareTo(a.customer.name);
break;
case SortType.distance:
deliveries.sort(
(a, b) => (currentState.distances![a.id] ?? 0.0).compareTo(
currentState.distances![b.id] ?? 0.0,
),
);
comparator = (a, b) =>
(currentState.distances?[a.id] ?? 0.0)
.compareTo(currentState.distances?[b.id] ?? 0.0);
break;
default:
comparator = (a, b) => a.customer.name.compareTo(b.customer.name);
}
//deliveries.addAll(finishedDeliveries);
ongoing.sort(comparator);
nonOngoing.sort(comparator);
return ListView.separated(
separatorBuilder: (context, index) => const Divider(height: 0),
final sorted = [...ongoing, ...nonOngoing];
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
Delivery delivery = deliveries[index];
return DeliveryListItem(
delivery: delivery,
distance: currentState.distances?[delivery.id] ?? 0.0,
);
},
itemCount: deliveries.length,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 8),
itemCount: sorted.length,
itemBuilder: (context, index) => DeliveryListItem(
delivery: sorted[index],
distance: currentState.distances?[sorted[index].id] ?? 0.0,
),
);
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
import 'package:hl_lieferservice/feature/car_selection/bloc/state.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/overview/presentation/delivery_info.dart';
@ -34,8 +36,14 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
void initState() {
super.initState();
// Select the first car for initialization
_selectedCarId = widget.tour.driver.cars.firstOrNull?.id;
// Pre-select today's car from the daily car selection.
// Falls back to the first available car if no selection exists.
final carSelectState = context.read<CarSelectBloc>().state;
if (carSelectState is CarSelectComplete) {
_selectedCarId = carSelectState.selectedCar.id;
} else {
_selectedCarId = widget.tour.driver.cars.firstOrNull?.id;
}
_sortType = SortType.nameAsc;
}
@ -44,54 +52,6 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
context.read<TourBloc>().add(LoadTour(teamId: state.user.number));
}
Widget _carSelection() {
return SizedBox(
width: double.infinity,
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
children:
widget.tour.driver.cars.map((car) {
Color? backgroundColor;
Color? iconColor = Theme.of(context).primaryColor;
Color? textColor;
if (_selectedCarId == car.id) {
backgroundColor = Theme.of(context).primaryColor;
textColor = Theme.of(context).colorScheme.onSecondary;
iconColor = Theme.of(context).colorScheme.onSecondary;
}
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () {
setState(() {
_selectedCarId = car.id;
});
},
child: Chip(
backgroundColor: backgroundColor,
label: Row(
children: [
Icon(Icons.local_shipping, color: iconColor, size: 20),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
car.plate,
style: TextStyle(color: textColor, fontSize: 12),
),
),
],
),
),
),
);
}).toList(),
),
);
}
/// Highlight the text of the active sorting type.
TextStyle? _popupItemTextStyle() {
return TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold);
@ -99,17 +59,23 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
@override
Widget build(BuildContext context) {
return RefreshIndicator(
return BlocListener<CarSelectBloc, CarSelectState>(
listener: (context, carState) {
if (carState is CarSelectComplete) {
setState(() => _selectedCarId = carState.selectedCar.id);
}
},
child: RefreshIndicator(
onRefresh: _loadTour,
child: ListView(
//crossAxisAlignment: CrossAxisAlignment.start,
children: [
DeliveryInfo(tour: widget.tour),
DeliveryInfo(tour: widget.tour, selectedCarId: _selectedCarId),
Padding(
padding: const EdgeInsets.only(
left: 10,
right: 10,
top: 15,
top: 0,
bottom: 10,
),
child: Row(
@ -191,16 +157,13 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
],
),
),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20),
child: _carSelection(),
),
DeliveryList(
selectedCarId: _selectedCarId,
sortType: _sortType,
),
],
),
),
);
}
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
@ -16,25 +18,55 @@ class DeliveryOverviewPage extends StatefulWidget {
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is TourLoaded) {
final currentState = state;
final carState = context.watch<CarSelectBloc>().state;
return Center(
child: DeliveryOverview(
tour: currentState.tour,
distances: currentState.distances ?? {},
return Scaffold(
appBar: AppBar(
title: const Text("Auslieferung"),
centerTitle: false,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
actions: [
if (carState is CarSelectComplete)
Padding(
padding: const EdgeInsets.only(right: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.local_shipping,
color: Theme.of(context).colorScheme.onSecondary,
size: 20,
),
const SizedBox(width: 6),
Text(
carState.selectedCar.plate,
style: TextStyle(
color: Theme.of(context).colorScheme.onSecondary,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
],
),
body: BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is TourLoaded) {
return DeliveryOverview(
tour: state.tour,
distances: state.distances ?? {},
);
}
if (state is TourLoadingFailed) {
return DeliveryLoadingFailedPage();
}
if (state is TourLoadingFailed) {
return DeliveryLoadingFailedPage();
}
return Container();
},
return const Center(child: CircularProgressIndicator());
},
),
);
}
}