Reklamationen der Fahrer: Meldungen verdeckten Buttons, und nach dem Abschluss stand „Lieferung nicht in der Tour gefunden" ohne Ausweg da. Meldungen: - Erfolgs-SnackBars entfernt, bei Erfolg bleibt der Spinner - einheitliche, schwebende Meldungen statt Stau über den Buttons - Server-Rohtexte durch verständliche Texte ersetzt (api_failure_text) Laden: - fehlt eine geöffnete Lieferung, wird die Tour einmal neu geholt; die Meldung kommt nur nach erfolgreichem Abruf ohne die Lieferung - Endlos-Spinner auf Lieferdetail, Filial-Abholung und Beladen ersetzt durch Kopfzeile, Fehlergrund und „Erneut versuchen" - Tour wird nach einem Tageswechsel nachgeladen, offene Seiten bleiben dabei unberührt - Abschluss und Zuweisung überschreiben kein zwischenzeitliches Neuladen, alte Antworten drehen einen Abschluss nicht mehr zurück - leere Server-Antworten gelten als Fehler statt als „keine Daten" Anmeldung: - Token-Erneuerung wartet höchstens 12 s, der native Aufruf läuft weiter - App-Start ohne Netz zeigt einen Hinweis mit „Erneut versuchen" - Fehlerseite der Fahrzeugauswahl bietet „Abmelden" Außerdem Podfile.lock an die Plugins angeglichen. Tests für die neuen Fehlerpfade ergänzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
6.8 KiB
Dart
197 lines
6.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import 'package:hl_lieferservice/domain/entity/delivery.dart';
|
|
import 'package:hl_lieferservice/domain/entity/tour_details.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/detail/model/delivery_payment_status.dart';
|
|
import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/delivered_items_card.dart';
|
|
import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_breakdown_card.dart';
|
|
import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_record_card.dart';
|
|
import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_sheet.dart';
|
|
|
|
/// Step „Zahlung" — Zahlungsabwicklung („Abkassieren") vor der Übersicht.
|
|
///
|
|
/// Zeigt die ausgelieferten Artikel mit Menge und Preis, die Betrags-
|
|
/// aufstellung und den Zahlungsstatus. Über „Zahlung abwickeln" öffnet sich
|
|
/// das Zahlungs-Modal; die Bestätigung wird am Server protokolliert.
|
|
///
|
|
/// Ist nichts offen (bereits bezahlt), kann der Fahrer den Step
|
|
/// überspringen. Sonst gibt die Detail-Page „Weiter" erst frei, wenn eine
|
|
/// gültige Zahlung protokolliert ist (siehe `DeliveryPaymentStatus`).
|
|
class StepPayment extends StatelessWidget {
|
|
const StepPayment({super.key, required this.delivery, required this.details});
|
|
|
|
final Delivery delivery;
|
|
final TourDetails details;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final status = DeliveryPaymentStatus.of(delivery, details);
|
|
|
|
return ListView(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
|
children: [
|
|
const _SectionHeader(text: 'Ausgelieferte Artikel'),
|
|
const SizedBox(height: 8),
|
|
DeliveredItemsCard(delivery: delivery, details: details),
|
|
const SizedBox(height: 24),
|
|
const _SectionHeader(text: 'Betrag'),
|
|
const SizedBox(height: 8),
|
|
PaymentBreakdownCard(
|
|
status: status,
|
|
credit: details.creditOf(delivery.id),
|
|
),
|
|
const SizedBox(height: 16),
|
|
PaymentRecordCard(status: status),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Fester Bereich unten: offener Betrag + großer „Zahlung abwickeln"-Button.
|
|
/// Bleibt sichtbar, auch wenn die Artikelliste lang ist. Sitzt in der
|
|
/// Bottom-Bar der Detail-Page, damit SnackBars ihn nicht verdecken.
|
|
class StepPaymentCollectPanel extends StatelessWidget {
|
|
const StepPaymentCollectPanel({
|
|
super.key,
|
|
required this.delivery,
|
|
required this.details,
|
|
});
|
|
|
|
final Delivery delivery;
|
|
final TourDetails details;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final status = DeliveryPaymentStatus.of(delivery, details);
|
|
if (delivery.state != DeliveryState.active || !status.hasOpenAmount) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
return _CollectPanel(
|
|
status: status,
|
|
onCollect: () => _openPaymentSheet(context, status),
|
|
);
|
|
}
|
|
|
|
Future<void> _openPaymentSheet(
|
|
BuildContext context,
|
|
DeliveryPaymentStatus status,
|
|
) async {
|
|
final tourBloc = context.read<TourBloc>();
|
|
final actorCarId = _actorCarId(context);
|
|
final customer = details.customerOf(delivery);
|
|
|
|
final result = await showPaymentSheet(
|
|
context,
|
|
openCents: status.openCents,
|
|
customerNumber: customer?.erpCustomerId ?? 0,
|
|
// Vorauswahl: zuletzt abgewickelte Methode, sonst die vom Beleg.
|
|
preselectedMethodId:
|
|
status.latestPayment?.paymentMethodId ?? delivery.paymentMethodId,
|
|
);
|
|
|
|
switch (result) {
|
|
case PaymentConfirmed(:final method):
|
|
tourBloc.add(
|
|
RecordDeliveryPayment(
|
|
deliveryId: delivery.id,
|
|
paymentMethodId: method.id,
|
|
expectedAmountCents: status.openCents,
|
|
actorCarId: actorCarId,
|
|
),
|
|
);
|
|
case DeliveryCancelRequested(:final reason):
|
|
tourBloc.add(CancelDelivery(deliveryId: delivery.id, reason: reason));
|
|
case null:
|
|
break;
|
|
}
|
|
}
|
|
|
|
String? _actorCarId(BuildContext context) {
|
|
final state = context.read<CarSelectBloc>().state;
|
|
return state is CarSelectComplete ? state.selectedCar.id : null;
|
|
}
|
|
}
|
|
|
|
class _CollectPanel extends StatelessWidget {
|
|
const _CollectPanel({required this.status, required this.onCollect});
|
|
|
|
final DeliveryPaymentStatus status;
|
|
final VoidCallback onCollect;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final settled = status.validPayment != null;
|
|
return Material(
|
|
elevation: 6,
|
|
color: theme.colorScheme.surface,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text('Offener Betrag', style: theme.textTheme.titleMedium),
|
|
const Spacer(),
|
|
Text(
|
|
formatEuroCents(status.openCents),
|
|
style: theme.textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.w800,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
if (settled)
|
|
// Bereits abgewickelt: Korrektur möglich, aber nicht mehr im
|
|
// Vordergrund — „Weiter" ist jetzt der nächste Schritt.
|
|
OutlinedButton.icon(
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(52),
|
|
),
|
|
onPressed: onCollect,
|
|
icon: const Icon(Icons.edit_outlined),
|
|
label: const Text('Zahlung korrigieren'),
|
|
)
|
|
else
|
|
FilledButton.icon(
|
|
style: FilledButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(68),
|
|
textStyle: const TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
onPressed: onCollect,
|
|
icon: const Icon(Icons.point_of_sale, size: 28),
|
|
label: const Text('Zahlung abwickeln'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SectionHeader extends StatelessWidget {
|
|
const _SectionHeader({required this.text});
|
|
final String text;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text(
|
|
text,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
|
|
);
|
|
}
|
|
}
|