Initial draft

This commit is contained in:
Dennis Nemec
2025-09-20 16:14:06 +02:00
commit b19a6e1cd4
219 changed files with 10317 additions and 0 deletions

View File

@ -0,0 +1,33 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart';
import 'package:hl_lieferservice/feature/delivery/overview/repository/tour_repository.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
class TourBloc extends Bloc<TourEvent, TourState> {
OperationBloc opBloc;
TourRepository deliveryRepository;
TourBloc({required this.opBloc, required this.deliveryRepository})
: super(TourInitial()) {
on<LoadTour>(_load);
}
Future<void> _load(LoadTour event, Emitter<TourState> emit) async {
opBloc.add(LoadOperation());
try {
Tour tour = await deliveryRepository.loadAll(event.teamId);
List<Payment> payments = await deliveryRepository.loadPaymentOptions();
tour.paymentMethods = payments;
emit(TourLoaded(tour: tour));
opBloc.add(FinishOperation());
} catch (e) {
opBloc.add(
FailOperation(message: "Fehler beim Laden der heutigen Fahrten"),
);
}
}
}

View File

@ -0,0 +1,7 @@
abstract class TourEvent {}
class LoadTour extends TourEvent {
String teamId;
LoadTour({required this.teamId});
}

View File

@ -0,0 +1,13 @@
import '../../../../model/tour.dart';
abstract class TourState {}
class TourInitial extends TourState {}
class TourLoading extends TourState {}
class TourLoaded extends TourState {
Tour tour;
TourLoaded({required this.tour});
}

View File

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:intl/intl.dart';
class DeliveryInfo extends StatelessWidget {
final Tour tour;
const DeliveryInfo({super.key, required this.tour});
@override
Widget build(BuildContext context) {
String date = DateFormat("dd.MM.yyyy").format(tour.date);
String amountDeliveries = tour.deliveries.length.toString();
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,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.calendar_month),
Padding(
padding: const 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),
],
),
),
],
),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart';
class DeliveryListItem extends StatelessWidget {
final Delivery delivery;
const DeliveryListItem({super.key, required this.delivery});
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);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.location_on, color: Theme.of(context).primaryColor),
Text("5min"),
],
);
}
void _goToDelivery(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DeliveryDetail(delivery: delivery),
),
);
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
delivery.customer.name,
style: Theme.of(context).textTheme.titleMedium,
),
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

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'delivery_item.dart';
class DeliveryList extends StatefulWidget {
final List<Delivery> deliveries;
const DeliveryList({super.key, required this.deliveries});
@override
State<StatefulWidget> createState() => _DeliveryListState();
}
class _DeliveryListState extends State<DeliveryList> {
@override
Widget build(BuildContext context) {
if (widget.deliveries.isEmpty) {
return ListView(
children: [Center(child: const Text("Keine Auslieferungen gefunden"))],
);
}
return ListView.separated(
separatorBuilder: (context, index) => const Divider(height: 0),
itemBuilder:
(context, index) =>
DeliveryListItem(delivery: widget.deliveries[index]),
itemCount: widget.deliveries.length,
);
}
}

View File

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_info.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_list.dart';
import 'package:hl_lieferservice/model/tour.dart';
import '../../../authentication/bloc/auth_bloc.dart';
import '../../../authentication/bloc/auth_state.dart';
class DeliveryOverview extends StatefulWidget {
const DeliveryOverview({super.key, required this.tour});
final Tour tour;
@override
State<StatefulWidget> createState() => _DeliveryOverviewState();
}
class _DeliveryOverviewState extends State<DeliveryOverview> {
String? _selectedCarPlate;
@override
void initState() {
super.initState();
// Select the first car for initialization
_selectedCarPlate = widget.tour.driver.cars.firstOrNull?.plate;
}
Future<void> _loadTour() async {
Authenticated state = context.read<AuthBloc>().state as Authenticated;
context.read<TourBloc>().add(LoadTour(teamId: state.teamId));
}
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 (_selectedCarPlate == car.plate) {
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(() {
_selectedCarPlate = car.plate;
});
},
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(),
),
);
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _loadTour,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DeliveryInfo(tour: widget.tour),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
"Fahrten",
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
IconButton(icon: Icon(Icons.filter_list), onPressed: () {}),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20),
child: _carSelection(),
),
Expanded(child: DeliveryList(deliveries: widget.tour.deliveries)),
],
),
);
}
}

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
import '../bloc/tour_bloc.dart';
import '../bloc/tour_state.dart';
class DeliveryOverviewPage extends StatefulWidget {
const DeliveryOverviewPage({super.key});
@override
State<StatefulWidget> createState() => _DeliveryOverviewPageState();
}
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is TourLoaded) {
final currentState = state;
return Center(child: DeliveryOverview(tour: currentState.tour));
}
return Container();
},
);
}
}

View File

@ -0,0 +1,19 @@
import 'package:hl_lieferservice/feature/delivery/overview/service/delivery_info_service.dart';
import 'package:hl_lieferservice/model/tour.dart';
class TourRepository {
DeliveryInfoService service;
TourRepository({required this.service});
Future<Tour> loadAll(String userId) async {
Tour? tour = await service.getTourOfToday(userId);
return tour!;
}
Future<List<Payment>> loadPaymentOptions() async {
return (await service.getPaymentMethods())
.map((option) => Payment.fromDTO(option))
.toList();
}
}

View File

@ -0,0 +1,278 @@
import 'dart:convert';
import 'package:docuframe/docuframe.dart' as df;
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/dto/delivery_response.dart';
import 'package:hl_lieferservice/dto/delivery_update.dart';
import 'package:hl_lieferservice/dto/delivery_update_response.dart';
import 'package:hl_lieferservice/dto/payment.dart';
import 'package:hl_lieferservice/dto/payments.dart';
import 'package:hl_lieferservice/model/car.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/util.dart';
import 'package:hl_lieferservice/services/erpframe.dart';
import '../../../../dto/basic_response.dart';
import '../../../../dto/discount_add_response.dart';
import '../../../../dto/discount_remove_response.dart';
import '../../../../dto/discount_update_response.dart';
import '../../../../dto/scan_response.dart';
class DeliveryInfoService extends ErpFrameService {
DeliveryInfoService({required super.config});
Future<void> updateDelivery(Delivery delivery) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session).execute(
"_web_updateDelivery",
parameter: DeliveryUpdateDTO.fromEntity(delivery).toJson()
as Map<String, dynamic>);
df.Logout(config: dfConfig, session: session).logout();
Map<String, dynamic> responseJson = jsonDecode(response.body!);
DeliveryUpdateResponseDTO responseDto =
DeliveryUpdateResponseDTO.fromJson(responseJson);
if (responseDto.code == "200") {
return;
}
throw responseDto.message;
} on df.DocuFrameException catch (e, st) {
debugPrint("ERROR WHILE UPDATING DELIVERY");
debugPrint(e.errorMessage);
debugPrint(e.errorCode);
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
/// List all available deliveries for today.
Future<Tour?> getTourOfToday(String userId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session).execute(
"_web_getDeliveries",
parameter: {"driver_id": userId, "date": getTodayDate()});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
DeliveryResponseDTO responseDto =
DeliveryResponseDTO.fromJson(responseJson);
return Tour(
discountArticleNumber: responseDto.discountArticleNumber,
date: DateTime.now(),
deliveries: responseDto.deliveries.map(Delivery.fromDTO).toList(),
paymentMethods: [],
driver: Driver(
cars: responseDto.driver.cars
.map((carDto) =>
Car(id: int.parse(carDto.id), plate: carDto.plate))
.toList(),
teamNumber: int.parse(responseDto.driver.id),
name: responseDto.driver.name,
salutation: responseDto.driver.salutation));
} catch (e, stacktrace) {
debugPrint(e.toString());
debugPrint(stacktrace.toString());
debugPrint("RANDOM EXCEPTION!");
rethrow;
} finally {
await logout(session);
}
}
Future<List<PaymentMethodDTO>> getPaymentMethods() async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_getPaymentMethods", parameter: {});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
PaymentMethodListDTO responseDto =
PaymentMethodListDTO.fromJson(responseJson);
return responseDto.paymentMethods;
} catch (e, st) {
debugPrint("ERROR while retrieving allowed payment methods");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<String?> unscanArticle(
String internalId, int amount, String reason) async {
df.LoginSession? session;
debugPrint("AMOUNT: $amount");
debugPrint("ID: $internalId");
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_unscanArticle", parameter: {
"article_id": internalId,
"amount": amount.toString(),
"reason": reason
});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
debugPrint(responseJson.toString());
ScanResponseDTO responseDto = ScanResponseDTO.fromJson(responseJson);
if (responseDto.succeeded == true) {
return responseDto.noteId;
} else {
throw responseDto.message;
}
} catch (e, st) {
debugPrint("ERROR WHILE REVERTING THE SCAN OF ARTICLE $internalId");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<void> resetScannedArticleAmount(String receiptRowId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session).execute(
"_web_unscanArticleReset",
parameter: {"receipt_row_id": receiptRowId});
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 REVERTING THE UNSCAN OF ARTICLE $receiptRowId");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<DiscountAddResponseDTO> addDiscount(
String deliveryId, int discount, String note) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_addDiscount", parameter: {
"delivery_id": deliveryId,
"discount": discount.toString(),
"note": note
});
debugPrint("BODY: ${response.body!}");
Map<String, dynamic> responseJson = jsonDecode(response.body!);
// let it throw, if the values are invalid
return DiscountAddResponseDTO.fromJson(responseJson);
} catch (e, st) {
debugPrint("ERROR while adding discount");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<DiscountRemoveResponseDTO> removeDiscount(String deliveryId) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_removeDiscount", parameter: {
"delivery_id": deliveryId,
});
debugPrint("${response.body!}");
Map<String, dynamic> responseJson = jsonDecode(response.body!);
// let it throw, if the values are invalid
return DiscountRemoveResponseDTO.fromJson(responseJson);
} catch (e, st) {
debugPrint("ERROR while removing discount");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
Future<DiscountUpdateResponseDTO> updateDiscount(
String deliveryId, String? note, int? discount) async {
df.LoginSession? session;
try {
session = await getSession();
df.DocuFrameMacroResponse response =
await df.Macro(config: dfConfig, session: session)
.execute("_web_updateDiscount", parameter: {
"delivery_id": deliveryId,
"discount": discount,
"note": note
});
Map<String, dynamic> responseJson = jsonDecode(response.body!);
// let it throw, if the values are invalid
return DiscountUpdateResponseDTO.fromJson(responseJson);
} catch (e, st) {
debugPrint("ERROR while retrieving allowed payment methods");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
} finally {
await logout(session);
}
}
}

View File

@ -0,0 +1,14 @@
import 'dart:convert';
import '../../../../services/erpframe.dart';
import 'package:docuframe/docuframe.dart' as df;
import 'package:flutter/cupertino.dart';
import '../../../../dto/discount_add_response.dart';
import '../../../../dto/discount_remove_response.dart';
import '../../../../dto/discount_update_response.dart';
class DiscountService extends ErpFrameService {
DiscountService({required super.config});
}