From e36fc69edc0b3ac78fde3b223b59417300366a9c Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Wed, 24 Jun 2026 08:11:54 +0200 Subject: [PATCH] =?UTF-8?q?Feature=20"Wunschziel=20f=C3=BCr=20heute"=20ent?= =?UTF-8?q?fernen=20und=20BWV-Seite=20erg=C3=A4nzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entfernt den Button und die komplette Funktionalität "Wunschziel für heute" (desired_zip): Seite, Netzwerk-Nachricht, Preference-Konstante, Übermittlung beim Session-Start sowie alle Übersetzungen. Ergänzt die neue BWV-Seite samt SetBwvLocationMessage und preferencesRemove-Helfer. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content_views/bwv_page.dart | 544 ++++++++++++++++++ lib/content_views/main_page.dart | 6 +- lib/content_views/set_desired_zip_page.dart | 214 ------- .../set_bwv_location_message.dart | 44 ++ .../set_desired_zip_message.dart | 15 - .../set_desired_zip_message.g.dart | 17 - .../start_session_message.dart | 3 - .../start_session_message.g.dart | 6 +- lib/tools/app_localizations.dart | 41 +- lib/tools/constants.dart | 9 +- lib/tools/mqtt_core.dart | 1 - lib/tools/util.dart | 4 + 12 files changed, 626 insertions(+), 278 deletions(-) create mode 100644 lib/content_views/bwv_page.dart delete mode 100644 lib/content_views/set_desired_zip_page.dart create mode 100644 lib/network_messages/set_bwv_location_message.dart delete mode 100644 lib/network_messages/set_desired_zip_message.dart delete mode 100644 lib/network_messages/set_desired_zip_message.g.dart diff --git a/lib/content_views/bwv_page.dart b/lib/content_views/bwv_page.dart new file mode 100644 index 0000000..ec3793b --- /dev/null +++ b/lib/content_views/bwv_page.dart @@ -0,0 +1,544 @@ +import 'package:dart_mq/dart_mq.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stadtbote/content_views/utils/content_view_widget.dart'; +import 'package:stadtbote/messages/pop_content_view_message.dart'; +import 'package:stadtbote/network_messages/set_bwv_location_message.dart'; +import 'package:stadtbote/tools/messages/publish_payload_message.dart'; + +import '../tools/app_localizations.dart'; +import '../tools/constants.dart'; +import '../tools/util.dart'; + +class BWVPage extends ContentViewWidget { + const BWVPage({super.key}); + + @override + State createState() => _BWVPageState(); + + @override + Future getHeaderWidget(BuildContext context) async { + await Future.delayed(const Duration(microseconds: 0)); + + return _HeaderWidget(); + } +} + +class _BWVPageState extends State with ProducerMixin { + // Wann bin ich wo zum Start verfügbar (Uhrzeiten, Datum = heute) + TimeOfDay? _startFrom; + TimeOfDay? _startTo; + final TextEditingController _startFromController = TextEditingController(); + final TextEditingController _startToController = TextEditingController(); + final TextEditingController _startPlaceController = TextEditingController(); + + // Wo möchte ich mit dem nächsten BWV Auftrag enden oder beginnen (Datum + Uhrzeit) + DateTime? _nextFrom; + DateTime? _nextTo; + final TextEditingController _nextFromController = TextEditingController(); + final TextEditingController _nextToController = TextEditingController(); + final TextEditingController _nextPlaceController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadPersisted(); + } + + @override + void dispose() { + _startFromController.dispose(); + _startToController.dispose(); + _startPlaceController.dispose(); + _nextFromController.dispose(); + _nextToController.dispose(); + _nextPlaceController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCard( + title: 'bwv_start_availability'.tr, + onClear: _clearStartBlock, + children: [ + _buildFieldRow( + label: 'bwv_from'.tr, + child: _buildPickerField( + _startFromController, + () => _pickTime(_startFromController, (t) => _startFrom = t), + ), + ), + const SizedBox(height: 12), + _buildFieldRow( + label: 'bwv_to'.tr, + child: _buildPickerField( + _startToController, + () => _pickTime(_startToController, (t) => _startTo = t), + ), + ), + const SizedBox(height: 12), + _buildFieldRow( + label: 'bwv_place'.tr, + child: _buildZipField(_startPlaceController), + ), + ], + ), + const SizedBox(height: 20), + _buildCard( + title: 'bwv_next_job_location'.tr, + onClear: _clearNextBlock, + children: [ + _buildFieldRow( + label: 'bwv_from'.tr, + child: _buildPickerField( + _nextFromController, + () => _pickDateTime(_nextFromController, (d) => _nextFrom = d), + ), + ), + const SizedBox(height: 12), + _buildFieldRow( + label: 'bwv_to'.tr, + child: _buildPickerField( + _nextToController, + () => _pickDateTime(_nextToController, (d) => _nextTo = d), + ), + ), + const SizedBox(height: 12), + _buildFieldRow( + label: 'bwv_place'.tr, + child: _buildZipField(_nextPlaceController), + ), + ], + ), + const SizedBox(height: 24), + getDefaultButton('apply'.tr, buttonHeight: 50, () => _apply()), + const SizedBox(height: 24), + Image.asset( + 'assets/sb_logo.png', + width: 80, + height: 80, + ), + ], + ), + ), + ); + } + + Widget _buildCard({ + required String title, + required VoidCallback onClear, + required List children, + }) { + return Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 245, 245, 245), + borderRadius: BorderRadius.circular(10), + border: const Border( + left: BorderSide(color: Constants.colorPrimary, width: 4), + ), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + title, + style: + const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + ), + IconButton( + tooltip: 'bwv_clear_block'.tr, + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.backspace_outlined, + size: 20, color: Constants.colorPrimary), + onPressed: onClear, + ), + ], + ), + const SizedBox(height: 16), + ...children, + ], + ), + ); + } + + Widget _buildFieldRow({required String label, required Widget child}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 50, + child: Text( + label, + style: const TextStyle(fontSize: 13, color: Colors.black54), + ), + ), + const SizedBox(width: 8), + Expanded(child: child), + ], + ); + } + + /// Lesefeld mit Picker (per onTap). + Widget _buildPickerField(TextEditingController controller, VoidCallback onTap) { + return TextField( + controller: controller, + readOnly: true, + onTap: onTap, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + border: Util.getDefaultBorder(), + enabledBorder: Util.getDefaultBorder(), + focusedBorder: Util.getDefaultFocusedBorder(), + ), + ); + } + + Widget _buildZipField(TextEditingController controller) { + return TextField( + controller: controller, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(5), + ], + maxLength: 5, + decoration: InputDecoration( + counterText: '', + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + border: Util.getDefaultBorder(), + enabledBorder: Util.getDefaultBorder(), + focusedBorder: Util.getDefaultFocusedBorder(), + ), + ); + } + + Future _pickTime( + TextEditingController controller, void Function(TimeOfDay) onPicked) async { + final picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ), + ); + + if (picked != null) { + onPicked(picked); + controller.text = '${_two(picked.hour)}:${_two(picked.minute)}'; + } + } + + Future _pickDateTime( + TextEditingController controller, void Function(DateTime) onPicked) async { + final now = DateTime.now(); + final date = await showDatePicker( + context: context, + initialDate: now, + firstDate: now.subtract(const Duration(days: 1)), + lastDate: now.add(const Duration(days: 365)), + ); + + if (date == null || !mounted) return; + + final time = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ), + ); + + if (time == null) return; + + final dt = + DateTime(date.year, date.month, date.day, time.hour, time.minute); + onPicked(dt); + controller.text = + '${_two(dt.day)}.${_two(dt.month)}.${dt.year} ${_two(dt.hour)}:${_two(dt.minute)}'; + } + + String _two(int value) => value.toString().padLeft(2, '0'); + + void _clearStartBlock() { + setState(() { + _startFrom = null; + _startTo = null; + _startFromController.clear(); + _startToController.clear(); + _startPlaceController.clear(); + }); + } + + void _clearNextBlock() { + setState(() { + _nextFrom = null; + _nextTo = null; + _nextFromController.clear(); + _nextToController.clear(); + _nextPlaceController.clear(); + }); + } + + /// Anzahl der gefüllten Felder eines Blocks (für die "Alles oder nichts"-Regel). + int _filledCount(List filled) => filled.where((f) => f).length; + + void _apply() { + // Block "Verfügbarkeit": entweder alle Felder gefüllt oder alle leer. + final startFilled = [ + _startFrom != null, + _startTo != null, + _startPlaceController.text.isNotEmpty, + ]; + final startCount = _filledCount(startFilled); + if (startCount != 0 && startCount != startFilled.length) { + showAlertDialog('error'.tr, 'bwv_incomplete_block'.tr); + return; + } + + // Block "nächster Auftrag": entweder alle Felder gefüllt oder alle leer. + final nextFilled = [ + _nextFrom != null, + _nextTo != null, + _nextPlaceController.text.isNotEmpty, + ]; + final nextCount = _filledCount(nextFilled); + if (nextCount != 0 && nextCount != nextFilled.length) { + showAlertDialog('error'.tr, 'bwv_incomplete_block'.tr); + return; + } + + // PLZ-Validierung (fünfstellig, sofern gefüllt). + for (final zip in [_startPlaceController.text, _nextPlaceController.text]) { + if (zip.isNotEmpty && zip.length != 5) { + showAlertDialog('error'.tr, 'zip_validation_error'.tr); + return; + } + } + + final now = DateTime.now(); + + // Verfügbarkeit: Uhrzeiten beziehen sich auf das heutige Datum. + BwvTimeWindow? availabilityStart; + if (startCount == startFilled.length) { + final from = DateTime( + now.year, now.month, now.day, _startFrom!.hour, _startFrom!.minute); + final to = DateTime( + now.year, now.month, now.day, _startTo!.hour, _startTo!.minute); + // Limit nur im logischen Sinne: von < bis. + if (!from.isBefore(to)) { + showAlertDialog('error'.tr, 'bwv_invalid_range'.tr); + return; + } + availabilityStart = BwvTimeWindow( + fromUtc: Util.toUnixSecondsUtc(from), + toUtc: Util.toUnixSecondsUtc(to), + postalCode: _startPlaceController.text, + ); + } + + BwvTimeWindow? bwvLocationPreference; + if (nextCount == nextFilled.length) { + // Limit nur im logischen Sinne: von < bis. + if (!_nextFrom!.isBefore(_nextTo!)) { + showAlertDialog('error'.tr, 'bwv_invalid_range'.tr); + return; + } + bwvLocationPreference = BwvTimeWindow( + fromUtc: Util.toUnixSecondsUtc(_nextFrom!), + toUtc: Util.toUnixSecondsUtc(_nextTo!), + postalCode: _nextPlaceController.text, + ); + } + + _persist(); + + final message = SetBwvLocationMessage( + timestamp: Util.nowUnixSecondsUtc(), + availabilityStart: availabilityStart, + bwvLocationPreference: bwvLocationPreference, + ); + + sendMessage( + routingKey: Constants.messageTopicPublishPayload, + payload: PublishPayloadMessage( + topic: "set_bwv_location", + message: message.toJson(), + acknowledgeRequired: false, + ), + ); + + sendMessage( + routingKey: Constants.messageTopicPopContentView, + payload: PopContentViewMessage(count: 1), + ); + } + + /// Lädt die zuletzt gespeicherten Werte (App ist datenführend). + /// Ist der Zeitraum des unteren Blocks abgelaufen, werden dessen Felder genullt. + void _loadPersisted() { + final startFrom = + Util.preferencesGetString(Constants.prefsBwvStartFrom, null); + final startTo = Util.preferencesGetString(Constants.prefsBwvStartTo, null); + final startPlace = + Util.preferencesGetString(Constants.prefsBwvStartPlace, null); + if (startFrom != null && startTo != null) { + _startFrom = _parseTime(startFrom); + _startTo = _parseTime(startTo); + _startFromController.text = startFrom; + _startToController.text = startTo; + _startPlaceController.text = startPlace ?? ''; + } + + final nextFromMillis = + Util.preferencesGetInt(Constants.prefsBwvNextFrom, null); + final nextToMillis = Util.preferencesGetInt(Constants.prefsBwvNextTo, null); + final nextPlace = + Util.preferencesGetString(Constants.prefsBwvNextPlace, null); + if (nextFromMillis != null && nextToMillis != null) { + final from = DateTime.fromMillisecondsSinceEpoch(nextFromMillis); + final to = DateTime.fromMillisecondsSinceEpoch(nextToMillis); + + if (to.isBefore(DateTime.now())) { + // Zeitraum abgelaufen -> Felder nullen und Persistenz bereinigen. + _clearPersistedNextBlock(); + } else { + _nextFrom = from; + _nextTo = to; + _nextFromController.text = + '${_two(from.day)}.${_two(from.month)}.${from.year} ${_two(from.hour)}:${_two(from.minute)}'; + _nextToController.text = + '${_two(to.day)}.${_two(to.month)}.${to.year} ${_two(to.hour)}:${_two(to.minute)}'; + _nextPlaceController.text = nextPlace ?? ''; + } + } + } + + TimeOfDay? _parseTime(String value) { + final parts = value.split(':'); + if (parts.length != 2) return null; + final hour = int.tryParse(parts[0]); + final minute = int.tryParse(parts[1]); + if (hour == null || minute == null) return null; + return TimeOfDay(hour: hour, minute: minute); + } + + void _persist() { + // Block "Verfügbarkeit" + if (_startFrom != null && _startTo != null) { + Util.preferencesSaveString(Constants.prefsBwvStartFrom, + '${_two(_startFrom!.hour)}:${_two(_startFrom!.minute)}'); + Util.preferencesSaveString(Constants.prefsBwvStartTo, + '${_two(_startTo!.hour)}:${_two(_startTo!.minute)}'); + Util.preferencesSaveString( + Constants.prefsBwvStartPlace, _startPlaceController.text); + } else { + Util.preferencesRemove(Constants.prefsBwvStartFrom); + Util.preferencesRemove(Constants.prefsBwvStartTo); + Util.preferencesRemove(Constants.prefsBwvStartPlace); + } + + // Block "nächster Auftrag" + if (_nextFrom != null && _nextTo != null) { + Util.preferencesSaveInt( + Constants.prefsBwvNextFrom, _nextFrom!.millisecondsSinceEpoch); + Util.preferencesSaveInt( + Constants.prefsBwvNextTo, _nextTo!.millisecondsSinceEpoch); + Util.preferencesSaveString( + Constants.prefsBwvNextPlace, _nextPlaceController.text); + } else { + _clearPersistedNextBlock(); + } + } + + void _clearPersistedNextBlock() { + Util.preferencesRemove(Constants.prefsBwvNextFrom); + Util.preferencesRemove(Constants.prefsBwvNextTo); + Util.preferencesRemove(Constants.prefsBwvNextPlace); + } +} + +class _HeaderWidget extends StatefulWidget { + @override + State<_HeaderWidget> createState() => _HeaderWidgetState(); +} + +class _HeaderWidgetState extends State<_HeaderWidget> with ProducerMixin { + @override + Widget build(BuildContext context) { + var cellWidth = (MediaQuery.of(context).size.width - 60 - 60 - 25) / 2; + + return Padding( + padding: const EdgeInsets.all(8), + child: Stack( + alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: FittedBox( + child: GestureDetector( + onTap: () async { + sendMessage( + routingKey: Constants.messageTopicPopContentView, + payload: PopContentViewMessage(count: 1), + ); + }, + child: const CircleAvatar( + radius: 10, + backgroundColor: Constants.colorPrimary, + child: Padding( + padding: EdgeInsets.all(1.0), + child: Icon(Icons.arrow_back, + size: 20.0, color: Colors.white)), + ), + ), + ), + ), + const SizedBox(width: 60), + ], + ), + SizedBox( + height: 100, + width: cellWidth * 2, + child: Center( + child: Text( + textAlign: TextAlign.center, + 'bwv'.tr, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.white), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/content_views/main_page.dart b/lib/content_views/main_page.dart index e2390b5..7ab6043 100644 --- a/lib/content_views/main_page.dart +++ b/lib/content_views/main_page.dart @@ -4,9 +4,9 @@ import 'package:stadtbote/content_views/accepted_jobs_page.dart'; import 'package:stadtbote/content_views/approach_area_page.dart'; import 'package:stadtbote/objectbox.g.dart'; import 'package:stadtbote/content_views/assigned_jobs_page.dart'; +import 'package:stadtbote/content_views/bwv_page.dart'; import 'package:stadtbote/content_views/finished_jobs_page.dart'; import 'package:stadtbote/content_views/messages_page.dart'; -import 'package:stadtbote/content_views/set_desired_zip_page.dart'; import 'package:stadtbote/content_views/utils/content_view_widget.dart'; import 'package:stadtbote/main.dart'; import 'package:stadtbote/model/job.dart'; @@ -245,13 +245,13 @@ class _MainPageState extends State with ConsumerMixin, ProducerMixin { Visibility( visible: !_loggedOut && _showBWVButton, child: getDefaultButton( - 'desired_zip'.tr, + 'bwv'.tr, image: Image.asset('assets/wunsch_plz.png', width: 30, height: 30), () async => { sendMessage( routingKey: Constants.messageTopicPushContentView, - payload: const SetDesiredZipPage(), + payload: const BWVPage(), ) }), ), diff --git a/lib/content_views/set_desired_zip_page.dart b/lib/content_views/set_desired_zip_page.dart deleted file mode 100644 index aac784a..0000000 --- a/lib/content_views/set_desired_zip_page.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'package:dart_mq/dart_mq.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:stadtbote/content_views/utils/content_view_widget.dart'; -import 'package:stadtbote/messages/pop_content_view_message.dart'; -import 'package:stadtbote/network_messages/set_desired_zip_message.dart'; -import 'package:stadtbote/tools/messages/publish_payload_message.dart'; - -import '../tools/app_localizations.dart'; -import '../tools/constants.dart'; -import '../tools/util.dart'; - -class SetDesiredZipPage extends ContentViewWidget { - const SetDesiredZipPage({super.key}); - - @override - State createState() => _SetDesiredZipPageState(); - - @override - Future getHeaderWidget(BuildContext context) async { - await Future.delayed(const Duration(microseconds: 0)); - - return _HeaderWidget(); - } -} - -class _SetDesiredZipPageState extends State - with ProducerMixin { - TextEditingController textEditController = TextEditingController(); - - @override - void initState() { - var savedDesiredZip = - Util.preferencesGetString(Constants.prefsDesiredZip, null); - if (savedDesiredZip != null) { - textEditController.text = savedDesiredZip; - } - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column(children: [ - Text('enter_desired_zip'.tr), - const SizedBox(height: 20), - Row( - mainAxisAlignment: - MainAxisAlignment.center, // Zentriert das Textfeld horizontal - children: [ - SizedBox( - width: 120, // Festlegen der Breite des Textfelds - child: Util.getDefaultTextField('PLZ', - textInputType: TextInputType.number, - formatter: [ - // Erlaubt nur Ziffern - FilteringTextInputFormatter.digitsOnly, - // Begrenzt die maximale Eingabelänge auf 5 Zeichen - LengthLimitingTextInputFormatter(5), - ], - controller: textEditController, - maxLength: 5, - autofocus: true), - ), - ], - ), - const SizedBox(height: 20), - getDefaultButton( - 'apply'.tr, - () => { - if (textEditController.text.isEmpty || - textEditController.text.length == 5) - { - Util.preferencesSaveString( - Constants.prefsDesiredZip, textEditController.text), - _sendDesiredZipMessage(textEditController.text), - sendMessage( - routingKey: Constants.messageTopicPopContentView, - payload: PopContentViewMessage(count: 1), - ) - } - else - { - showAlertDialog('error'.tr, - 'zip_validation_error'.tr) - } - }) - ]), - ); - } - - _sendDesiredZipMessage(String desiredZip) { - var desiredZipMessage = SetDesiredZipMessage(); - desiredZipMessage.desiredZip = desiredZip; - - sendMessage( - routingKey: Constants.messageTopicPublishPayload, - payload: PublishPayloadMessage( - topic: "set_desired_zip", - message: desiredZipMessage.toJson(), - acknowledgeRequired: true), - ); - } - - @override - void dispose() { - textEditController.dispose(); - super.dispose(); - } -} - -class _HeaderWidget extends StatefulWidget { - @override - State<_HeaderWidget> createState() => _HeaderWidgetState(); -} - -class _HeaderWidgetState extends State<_HeaderWidget> with ProducerMixin { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - var cellWidth = (MediaQuery.of(context).size.width - 60 - 60 - 25) / 2; - - return Padding( - padding: const EdgeInsets.all(8), - child: Stack( - alignment: Alignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Visibility( - visible: true, - child: Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Colors.white, - ), - child: FittedBox( - child: GestureDetector( - onTap: () async { - sendMessage( - routingKey: Constants.messageTopicPopContentView, - payload: PopContentViewMessage(count: 1), - ); - }, - child: const CircleAvatar( - radius: 10, - backgroundColor: Constants.colorPrimary, - child: Padding( - padding: EdgeInsets.all(1.0), - child: Icon(Icons.arrow_back, - size: 20.0, color: Colors.white)), - ), - ), - ), - ), - ), - Visibility( - visible: false, - child: Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Colors.red, - ), - child: FittedBox( - child: GestureDetector( - onTap: () async { - sendMessage( - routingKey: Constants.messageTopicPopContentView, - payload: PopContentViewMessage(count: 1), - ); - }, - child: const CircleAvatar( - backgroundColor: Constants.colorPrimary, - radius: 10, - child: Padding( - padding: EdgeInsets.all(1.0), - child: Icon(Icons.info_outline, - size: 20.0, color: Colors.white))), - ), - ), - ), - ), - ], - ), - SizedBox( - height: 100, - width: cellWidth * 2, - child: const Center( - child: Text( - textAlign: TextAlign.center, - "Wunschziel für heute", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: Colors.white), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/network_messages/set_bwv_location_message.dart b/lib/network_messages/set_bwv_location_message.dart new file mode 100644 index 0000000..913c0c4 --- /dev/null +++ b/lib/network_messages/set_bwv_location_message.dart @@ -0,0 +1,44 @@ +/// Zeitfenster mit Postleitzahl für das Setzen der BWV-Location. +class BwvTimeWindow { + final int fromUtc; + final int toUtc; + final String postalCode; + + BwvTimeWindow({ + required this.fromUtc, + required this.toUtc, + required this.postalCode, + }); + + Map toJson() => { + 'fromUtc': fromUtc, + 'toUtc': toUtc, + 'postalCode': postalCode, + }; +} + +/// Payload für das Topic `set_bwv_location`. +class SetBwvLocationMessage { + final int timestamp; + final BwvTimeWindow? availabilityStart; + final BwvTimeWindow? bwvLocationPreference; + + SetBwvLocationMessage({ + required this.timestamp, + this.availabilityStart, + this.bwvLocationPreference, + }); + + Map toJson() { + final map = {'timestamp': timestamp}; + + if (availabilityStart != null) { + map['availabilityStart'] = availabilityStart!.toJson(); + } + if (bwvLocationPreference != null) { + map['bwvLocationPreference'] = bwvLocationPreference!.toJson(); + } + + return map; + } +} diff --git a/lib/network_messages/set_desired_zip_message.dart b/lib/network_messages/set_desired_zip_message.dart deleted file mode 100644 index 273f129..0000000 --- a/lib/network_messages/set_desired_zip_message.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'set_desired_zip_message.g.dart'; - -@JsonSerializable() -class SetDesiredZipMessage { - @JsonKey(name: "desired_zip") - late String desiredZip; - - SetDesiredZipMessage(); - - factory SetDesiredZipMessage.fromJson(Map json) => _$SetDesiredZipMessageFromJson(json); - - Map toJson() => _$SetDesiredZipMessageToJson(this); -} diff --git a/lib/network_messages/set_desired_zip_message.g.dart b/lib/network_messages/set_desired_zip_message.g.dart deleted file mode 100644 index 1d6d394..0000000 --- a/lib/network_messages/set_desired_zip_message.g.dart +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'set_desired_zip_message.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SetDesiredZipMessage _$SetDesiredZipMessageFromJson( - Map json) => - SetDesiredZipMessage()..desiredZip = json['desired_zip'] as String; - -Map _$SetDesiredZipMessageToJson( - SetDesiredZipMessage instance) => - { - 'desired_zip': instance.desiredZip, - }; diff --git a/lib/network_messages/start_session_message.dart b/lib/network_messages/start_session_message.dart index 51d661c..8d2f6a9 100644 --- a/lib/network_messages/start_session_message.dart +++ b/lib/network_messages/start_session_message.dart @@ -4,9 +4,6 @@ part 'start_session_message.g.dart'; @JsonSerializable() class StartSessionMessage { - @JsonKey(name: "desired_zip") - late String desiredZip; - StartSessionMessage(); factory StartSessionMessage.fromJson(Map json) => _$StartSessionMessageFromJson(json); diff --git a/lib/network_messages/start_session_message.g.dart b/lib/network_messages/start_session_message.g.dart index adc7706..e99df88 100644 --- a/lib/network_messages/start_session_message.g.dart +++ b/lib/network_messages/start_session_message.g.dart @@ -7,10 +7,8 @@ part of 'start_session_message.dart'; // ************************************************************************** StartSessionMessage _$StartSessionMessageFromJson(Map json) => - StartSessionMessage()..desiredZip = json['desired_zip'] as String; + StartSessionMessage(); Map _$StartSessionMessageToJson( StartSessionMessage instance) => - { - 'desired_zip': instance.desiredZip, - }; + {}; diff --git a/lib/tools/app_localizations.dart b/lib/tools/app_localizations.dart index 690a111..95dda4b 100644 --- a/lib/tools/app_localizations.dart +++ b/lib/tools/app_localizations.dart @@ -72,7 +72,7 @@ class AppLocalizations { 'finished_jobs': 'Erledigte Aufträge', 'messages': 'Nachrichten', 'approach_area': 'Anfahrtsbereich', - 'desired_zip': 'Wunschziel für heute', + 'bwv': 'BWV', 'login': 'Anmelden', 'logout': 'Abmelden', 'courier_id': 'Kurierkennung', @@ -172,7 +172,6 @@ class AppLocalizations { 'release': 'Freimelden', 'zip': 'Postleitzahl', 'enter_zip': 'Bitte geben Sie eine fünfstellige Postleitzahl ein', - 'enter_desired_zip': 'Bitte geben Sie Ihr Wunschziel für heute ein', 'zip_hint': 'PLZ', 'zip_validation_error': 'Das Feld muss leer sein, oder es müssen fünf Zahlen eingegeben werden', 'enter_commission_number': 'Bitte geben Sie die Kommisionsnummer ein', @@ -185,6 +184,16 @@ class AppLocalizations { // Liste schließen 'close_list': 'Liste schliessen', + + // BWV / Vorbestellungen + 'bwv_start_availability': 'Wann bin ich wo zum Start verfügbar', + 'bwv_next_job_location': 'Wo möchte ich mit dem nächsten BWV Auftrag enden oder beginnen', + 'bwv_from': 'von', + 'bwv_to': 'bis', + 'bwv_place': 'Ort', + 'bwv_clear_block': 'Block leeren', + 'bwv_incomplete_block': 'Bitte alle Felder eines Blocks ausfüllen oder den Block leeren.', + 'bwv_invalid_range': 'Der „von"-Zeitpunkt muss vor dem „bis"-Zeitpunkt liegen.', }, // English @@ -195,7 +204,6 @@ class AppLocalizations { 'finished_jobs': 'Finished Jobs', 'messages': 'Messages', 'approach_area': 'Approach Area', - 'desired_zip': 'Desired Location for Today', 'login': 'Login', 'logout': 'Logout', 'courier_id': 'Courier ID', @@ -295,7 +303,6 @@ class AppLocalizations { 'release': 'Release', 'zip': 'ZIP Code', 'enter_zip': 'Please enter a five-digit ZIP code', - 'enter_desired_zip': 'Please enter your desired location for today', 'zip_hint': 'ZIP', 'zip_validation_error': 'The field must be empty or five digits must be entered', 'enter_commission_number': 'Please enter the commission number', @@ -308,6 +315,16 @@ class AppLocalizations { // Close List 'close_list': 'Close List', + + // BWV / Pre-orders + 'bwv_start_availability': 'When and where am I available to start', + 'bwv_next_job_location': 'Where do I want to end or begin with the next BWV job', + 'bwv_from': 'from', + 'bwv_to': 'to', + 'bwv_place': 'Place', + 'bwv_clear_block': 'Clear block', + 'bwv_incomplete_block': 'Please fill in all fields of a block or clear the block.', + 'bwv_invalid_range': 'The "from" time must be before the "to" time.', }, // Spanish @@ -318,7 +335,6 @@ class AppLocalizations { 'finished_jobs': 'Trabajos Terminados', 'messages': 'Mensajes', 'approach_area': 'Área de Aproximación', - 'desired_zip': 'Ubicación Deseada para Hoy', 'login': 'Iniciar Sesión', 'logout': 'Cerrar Sesión', 'courier_id': 'ID de Mensajero', @@ -418,7 +434,6 @@ class AppLocalizations { 'release': 'Liberar', 'zip': 'Código Postal', 'enter_zip': 'Por favor ingrese un código postal de cinco dígitos', - 'enter_desired_zip': 'Por favor ingrese su ubicación deseada para hoy', 'zip_hint': 'CP', 'zip_validation_error': 'El campo debe estar vacío o deben ingresarse cinco dígitos', 'enter_commission_number': 'Por favor ingrese el número de comisión', @@ -441,7 +456,6 @@ class AppLocalizations { 'finished_jobs': 'Tâches Terminées', 'messages': 'Messages', 'approach_area': "Zone d'Approche", - 'desired_zip': 'Lieu Souhaité pour Aujourd\'hui', 'login': 'Connexion', 'logout': 'Déconnexion', 'courier_id': 'ID Coursier', @@ -541,7 +555,6 @@ class AppLocalizations { 'release': 'Libérer', 'zip': 'Code Postal', 'enter_zip': 'Veuillez saisir un code postal à cinq chiffres', - 'enter_desired_zip': 'Veuillez saisir votre lieu souhaité pour aujourd\'hui', 'zip_hint': 'CP', 'zip_validation_error': 'Le champ doit être vide ou cinq chiffres doivent être saisis', 'enter_commission_number': 'Veuillez saisir le numéro de commission', @@ -564,7 +577,6 @@ class AppLocalizations { 'finished_jobs': 'Zakończone Zadania', 'messages': 'Wiadomości', 'approach_area': 'Strefa Dojazdu', - 'desired_zip': 'Pożądana Lokalizacja na Dziś', 'login': 'Zaloguj', 'logout': 'Wyloguj', 'courier_id': 'ID Kuriera', @@ -664,7 +676,6 @@ class AppLocalizations { 'release': 'Zwolnij', 'zip': 'Kod Pocztowy', 'enter_zip': 'Proszę wprowadzić pięciocyfrowy kod pocztowy', - 'enter_desired_zip': 'Proszę wprowadzić pożądaną lokalizację na dziś', 'zip_hint': 'Kod', 'zip_validation_error': 'Pole musi być puste lub muszą zostać wprowadzone pięć cyfr', 'enter_commission_number': 'Proszę wprowadzić numer zlecenia', @@ -687,7 +698,6 @@ class AppLocalizations { 'finished_jobs': 'Завершенные Задания', 'messages': 'Сообщения', 'approach_area': 'Зона Доступа', - 'desired_zip': 'Желаемое Местоположение на Сегодня', 'login': 'Войти', 'logout': 'Выйти', 'courier_id': 'ID Курьера', @@ -787,7 +797,6 @@ class AppLocalizations { 'release': 'Освободить', 'zip': 'Почтовый Индекс', 'enter_zip': 'Пожалуйста, введите пятизначный почтовый индекс', - 'enter_desired_zip': 'Пожалуйста, введите желаемое местоположение на сегодня', 'zip_hint': 'Индекс', 'zip_validation_error': 'Поле должно быть пустым или должны быть введены пять цифр', 'enter_commission_number': 'Пожалуйста, введите номер комиссии', @@ -810,7 +819,6 @@ class AppLocalizations { 'finished_jobs': 'Tamamlanan Görevler', 'messages': 'Mesajlar', 'approach_area': 'Yaklaşım Bölgesi', - 'desired_zip': 'Bugün İçin İstenen Konum', 'login': 'Giriş', 'logout': 'Çıkış', 'courier_id': 'Kurye ID', @@ -910,7 +918,6 @@ class AppLocalizations { 'release': 'Serbest Bırak', 'zip': 'Posta Kodu', 'enter_zip': 'Lütfen beş haneli bir posta kodu girin', - 'enter_desired_zip': 'Lütfen bugün için istediğiniz konumu girin', 'zip_hint': 'PK', 'zip_validation_error': 'Alan boş olmalı veya beş rakam girilmelidir', 'enter_commission_number': 'Lütfen komisyon numarasını girin', @@ -933,7 +940,6 @@ class AppLocalizations { 'finished_jobs': 'Lõpetatud Ülesanded', 'messages': 'Sõnumid', 'approach_area': 'Ligipääsu Piirkond', - 'desired_zip': 'Tänane Soovitud Asukoht', 'login': 'Logi Sisse', 'logout': 'Logi Välja', 'courier_id': 'Kulleri ID', @@ -1033,7 +1039,6 @@ class AppLocalizations { 'release': 'Vabasta', 'zip': 'Postiindeks', 'enter_zip': 'Palun sisestage viiekohaline postiindeks', - 'enter_desired_zip': 'Palun sisestage tänane soovitud asukoht', 'zip_hint': 'Indeks', 'zip_validation_error': 'Väli peab olema tühi või tuleb sisestada viis numbrit', 'enter_commission_number': 'Palun sisestage komisjoni number', @@ -1056,7 +1061,6 @@ class AppLocalizations { 'finished_jobs': 'Pabeigtie Uzdevumi', 'messages': 'Ziņojumi', 'approach_area': 'Piekļuves Zona', - 'desired_zip': 'Vēlamā Atrašanās Vieta Šodien', 'login': 'Pieteikties', 'logout': 'Iziet', 'courier_id': 'Kurjera ID', @@ -1156,7 +1160,6 @@ class AppLocalizations { 'release': 'Atbrīvot', 'zip': 'Pasta Indekss', 'enter_zip': 'Lūdzu, ievadiet piecciparu pasta indeksu', - 'enter_desired_zip': 'Lūdzu, ievadiet vēlamo atrašanās vietu šodien', 'zip_hint': 'Indekss', 'zip_validation_error': 'Lauks jābūt tukšam vai jāievada pieci cipari', 'enter_commission_number': 'Lūdzu, ievadiet komisijas numuru', @@ -1179,7 +1182,6 @@ class AppLocalizations { 'finished_jobs': 'Baigtos Užduotys', 'messages': 'Žinutės', 'approach_area': 'Prieigos Zona', - 'desired_zip': 'Pageidaujama Vieta Šiandien', 'login': 'Prisijungti', 'logout': 'Atsijungti', 'courier_id': 'Kurjerio ID', @@ -1279,7 +1281,6 @@ class AppLocalizations { 'release': 'Atleisti', 'zip': 'Pašto Kodas', 'enter_zip': 'Prašome įvesti penkiaženklį pašto kodą', - 'enter_desired_zip': 'Prašome įvesti pageidaujamą vietą šiandien', 'zip_hint': 'Kodas', 'zip_validation_error': 'Laukas turi būti tuščias arba turi būti įvesti penki skaitmenys', 'enter_commission_number': 'Prašome įvesti komisijos numerį', diff --git a/lib/tools/constants.dart b/lib/tools/constants.dart index 95f2122..e6d12ce 100644 --- a/lib/tools/constants.dart +++ b/lib/tools/constants.dart @@ -31,9 +31,16 @@ class Constants { static const String prefsPreviousMqttSubscription = "previous_mqtt_subscription"; static const String prefsBWVEnabled = "bwv_enabled"; - static const String prefsDesiredZip = "desired_zip"; static const String prefsAppLanguage = "app_language"; + // BWV / Vorbestellungen (App ist datenführend, lokal persistiert) + static const String prefsBwvStartFrom = "bwv_start_from"; // "HH:mm" + static const String prefsBwvStartTo = "bwv_start_to"; // "HH:mm" + static const String prefsBwvStartPlace = "bwv_start_place"; // PLZ + static const String prefsBwvNextFrom = "bwv_next_from"; // epoch millis (lokal) + static const String prefsBwvNextTo = "bwv_next_to"; // epoch millis (lokal) + static const String prefsBwvNextPlace = "bwv_next_place"; // PLZ + static const int stateAssigned = 0; static const int stateAccepted = 1; static const int stateFinished = 2; diff --git a/lib/tools/mqtt_core.dart b/lib/tools/mqtt_core.dart index 5bd37c7..e54cf01 100644 --- a/lib/tools/mqtt_core.dart +++ b/lib/tools/mqtt_core.dart @@ -588,7 +588,6 @@ class MqttCore with ProducerMixin, ConsumerMixin { _initSession() async { var startSessionMessage = StartSessionMessage(); - startSessionMessage.desiredZip = (Util.preferencesGetString(Constants.prefsDesiredZip, ""))!; var jsonMap = startSessionMessage.toJson(); diff --git a/lib/tools/util.dart b/lib/tools/util.dart index 44b0a52..c98e285 100644 --- a/lib/tools/util.dart +++ b/lib/tools/util.dart @@ -68,6 +68,10 @@ class Util { return defaultValue; } + static void preferencesRemove(String key) async { + await prefs.remove(key); + } + static String getAppUuid() { var currentUuid = preferencesGetString('uuid', null);