diff --git a/android/app/build.gradle b/android/app/build.gradle index d9bf2b8..cffb3e9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -49,10 +49,7 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "de.assecutor.stadtbote" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7aab44d..4acfd4e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,9 @@ + android:icon="@mipmap/launcher_icon"> { void initState() { Future.delayed(const Duration(seconds: 0), () async { _jobs = await MyApp.isarInstance.jobs.filter().stateEqualTo(Constants.stateAccepted).findAll(); + _jobs.sort((job1, job2) { + var date1 = DateTime.parse(job1.ordertime).millisecondsSinceEpoch; + var date2 = DateTime.parse(job2.ordertime).millisecondsSinceEpoch; + + return date1 - date2; + }); _initCompleted = true; @@ -88,8 +94,8 @@ class _AcceptedJobsPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - DateFormat('dd.MM.yyyy HH:mm').format(DateTime.parse(job.ordertime)).toString(), - style: TextStyle(backgroundColor: isJobInFuture(job) ? Colors.yellow : Colors.white), + DateFormat('dd.MM.yyyy HH:mm').format(DateTime.parse(job.ordertime).toLocal()).toString(), + style: TextStyle(backgroundColor: isJobInFuture(job) ? Colors.yellow : Colors.transparent), textAlign: TextAlign.left, ), Text( diff --git a/lib/content_views/approach_area_page.dart b/lib/content_views/approach_area_page.dart index efb1ed3..40251fb 100644 --- a/lib/content_views/approach_area_page.dart +++ b/lib/content_views/approach_area_page.dart @@ -1,12 +1,13 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:stadtbote/content_views/utils/content_view_widget.dart'; import 'package:stadtbote/content_views/utils/header_info.dart'; -import 'package:stadtbote/model/rank.dart'; +import 'package:stadtbote/model/ranking.dart'; import 'package:stadtbote/popups/tasks_popups/zip_popup.dart'; import 'package:stadtbote/protocoll_messages/zip_message.dart'; +import 'package:stadtbote/tools/constants.dart'; +import 'package:stadtbote/tools/message_service.dart'; +import 'package:stadtbote/tools/messages/ranking_updated.dart'; import 'package:stadtbote/tools/mqtt_core.dart'; import '../main.dart'; @@ -43,21 +44,30 @@ class ApproachAreaPage extends ContentViewWidget { } class _ApproachAreaPageState extends State { - late List? _rankings; + late Ranking? _ranking; late bool _initCompleted = false; @override void initState() { + super.initState(); + Future.delayed(const Duration(seconds: 0), () async { - _rankings = await MyApp.isarInstance.ranks.where().findAll(); + _ranking = await MyApp.isarInstance.rankings.where().findFirst(); _initCompleted = true; setState(() {}); }); - super.initState(); + _initMessages(); + } + + @override + void deactivate() { + super.deactivate(); + + MessageService().unsubscribe(Constants.messageTopicRankingUpdated, this); } @override @@ -84,18 +94,16 @@ class _ApproachAreaPageState extends State { List _fillColumns() { List columns = []; - if (_rankings != null) { - for (int i = 0; i < _rankings!.length; i++) { - var rank = _rankings![i]; + if (_ranking != null) { + for (int i = 0; i < _ranking!.ranks.length; i++) { + var rank = _ranking!.ranks[i]; List entries = []; entries.add( GestureDetector( - onTap: ()=> { + onTap: () => { if (i == 0) { showZipPopup(context, 'ACHTUNG: Wenn Sie Ihre PLZ ändern, verlieren Sie Ihr Ranking!\n', 'Freimelden', (zip) => {_sendZip(rank, zip, ModeEnum.set)}), - } else { - showZipPopup(context, 'ACHTUNG: Wenn Sie Ihre PLZ ändern, verlieren Sie Ihr Ranking!\n', 'OK', (zip) => {_sendZip(rank, zip, ModeEnum.request)}), } }, child: Container( @@ -103,7 +111,7 @@ class _ApproachAreaPageState extends State { margin: const EdgeInsets.all(2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: i == 0 ? Colors.lightGreen : Colors.white, + color: i == 0 ? Constants.colorPrimary : Colors.white, border: Border.all( color: Colors.grey, width: 0.5, @@ -114,10 +122,11 @@ class _ApproachAreaPageState extends State { child: Column(children: [ Text( rank.zip, - style: const TextStyle(fontSize: 20), + style: TextStyle(fontSize: 20, color: (i == 0 ? Colors.white : Colors.black)), ), Text( rank.area, + style: TextStyle(fontSize: 13, color: (i == 0 ? Colors.white : Colors.black)), ) ] ) @@ -168,56 +177,36 @@ class _ApproachAreaPageState extends State { } _sendZip(Rank rank, String zip, ModeEnum mode) { - var message = ZipMessage(); + var zipMessage = ZipMessage(); if (mode == ModeEnum.set) { var position = MyApp.currentPosition; - message.setPos = ZipSetPos(); - message.setPos!.zip = zip; + zipMessage.setPos = ZipSetPos(); + zipMessage.setPos!.zip = zip; if (position != null) { - message.setPos!.track = ZipTrack(); - message.setPos!.track.time = DateTime.now().toIso8601String(); - message.setPos!.track.pos = ZipTrackPos(); - message.setPos!.track.pos!.lat = position.latitude; - message.setPos!.track.pos!.long = position.longitude; - message.setPos!.track.pos!.acc = position.accuracy.toInt(); + zipMessage.setPos!.track = ZipTrack(); + zipMessage.setPos!.track!.time = DateTime.now().toIso8601String(); + zipMessage.setPos!.track!.pos = ZipTrackPos(); + zipMessage.setPos!.track!.pos!.lat = position.latitude; + zipMessage.setPos!.track!.pos!.long = position.longitude; + zipMessage.setPos!.track!.pos!.acc = position.accuracy.toInt(); } } else { - message.reqZip = ZipReqZip(); - message.reqZip!.id = rank.id; - message.reqZip!.zip = zip; + zipMessage.reqZip = ZipReqZip(); + zipMessage.reqZip!.id = rank.id; + zipMessage.reqZip!.zip = zip; } - MqttCore.instance.publishText("set_location", jsonEncode(message.toJson())); + MqttCore.instance.publishPayloadMessage("rank", zipMessage.toJson()); } -} -/* -private void SendMessage(int columnNumber, string zipNumber) - { - var payloadObject = new JObject(); + _initMessages() { + MessageService().subscribe(Constants.messageTopicRankingUpdated, this, (rankingUpdateMessage) async { + _ranking = await MyApp.isarInstance.rankings.where().findFirst(); - if (columnNumber == 0) - { - var setPosObject = new JObject(); - payloadObject.Add(new JProperty("set_pos", setPosObject)); - - setPosObject.Add(new JProperty("zip", zipNumber)); - setPosObject.Add(new JProperty("track", CreateTrackObject())); - } - else - { - var regZipObject = new JObject(); - payloadObject.Add(new JProperty("reg_zip", regZipObject)); - - regZipObject.Add(new JProperty("id", columnNumber)); - regZipObject.Add(new JProperty("zip", zipNumber)); - } - - var json = payloadObject.ToString(); - - App.Core.SendJsonMessageAsync("rank", payloadObject); - } - */ + setState(() {}); + }); + } +} \ No newline at end of file diff --git a/lib/content_views/assigned_jobs_page.dart b/lib/content_views/assigned_jobs_page.dart index 14eea96..3a54cd3 100644 --- a/lib/content_views/assigned_jobs_page.dart +++ b/lib/content_views/assigned_jobs_page.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; @@ -10,7 +8,7 @@ import 'package:badges/badges.dart' as badges; import 'package:stadtbote/protocoll_messages/accept_job_message.dart'; import 'package:stadtbote/protocoll_messages/fin_task_message.dart'; import 'package:stadtbote/tools/message_service.dart'; -import 'package:stadtbote/tools/messages/refresh_badges_message.dart'; +import 'package:stadtbote/tools/messages/job_deleted_message.dart'; import 'package:stadtbote/tools/mqtt_core.dart'; import '../main.dart'; @@ -53,6 +51,8 @@ class _AssignedJobsPageState extends State { Future.delayed(const Duration(seconds: 0), () async { _entries = await _createListEntries(); + _initMessages(); + _initCompleted = true; setState(() {}); @@ -86,6 +86,12 @@ class _AssignedJobsPageState extends State { var jobs = await MyApp.isarInstance.jobs.filter().stateEqualTo(Constants.stateAssigned).findAll(); for (var job in jobs) { + String remarkString = ''; + for (var remark in job.rem) { + remarkString += '$remark\n'; + } + remarkString = remarkString.trim(); + entries.add(ExpandableNotifier( child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), @@ -119,7 +125,7 @@ class _AssignedJobsPageState extends State { child: Column( children: [ Text( - DateFormat('dd.MM.yyyy HH:mm').format(DateTime.parse(job.ordertime)).toString(), + DateFormat('dd.MM.yyyy HH:mm').format(DateTime.parse(job.ordertime).toLocal()).toString(), textAlign: TextAlign.left, style: TextStyle(backgroundColor: isJobInFuture(job) ? Colors.yellow : Colors.white), ), @@ -144,7 +150,7 @@ class _AssignedJobsPageState extends State { )), collapsed: const SizedBox(), expanded: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( height: 200, diff --git a/lib/content_views/finished_jobs_page.dart b/lib/content_views/finished_jobs_page.dart index 7e1c1e6..a5a74ba 100644 --- a/lib/content_views/finished_jobs_page.dart +++ b/lib/content_views/finished_jobs_page.dart @@ -88,7 +88,7 @@ class _FinishedJobsPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - DateFormat('dd.MM.yyyy HH:mm').format(DateTime.parse(job.ordertime)).toString(), + DateFormat('dd.MM.yyyy HH:mm').format(DateTime.parse(job.ordertime).toLocal()).toString(), textAlign: TextAlign.left, ), Text( diff --git a/lib/content_views/main_page.dart b/lib/content_views/main_page.dart index 5c08728..14b0a3a 100644 --- a/lib/content_views/main_page.dart +++ b/lib/content_views/main_page.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:stadtbote/content_views/accepted_jobs_page.dart'; @@ -10,19 +11,21 @@ import 'package:stadtbote/content_views/messages_page.dart'; import 'package:stadtbote/content_views/utils/content_view_widget.dart'; import 'package:stadtbote/content_views/utils/header_info.dart'; import 'package:stadtbote/model/job.dart'; +import 'package:stadtbote/model/log_entry.dart'; import 'package:stadtbote/model/message.dart'; import 'package:stadtbote/model/session_status.dart'; import 'package:stadtbote/model/status.dart'; import 'package:stadtbote/popups/infos_popup.dart'; +import 'package:stadtbote/protocoll_messages/device_log_response.dart'; import 'package:stadtbote/protocoll_messages/logout_message.dart'; import 'package:stadtbote/tools/message_service.dart'; import 'package:stadtbote/tools/messages/job_deleted_message.dart'; import 'package:stadtbote/tools/messages/job_received_message.dart'; import 'package:stadtbote/tools/messages/message_message.dart'; -import 'package:stadtbote/tools/messages/refresh_badges_message.dart'; import 'package:stadtbote/tools/messages/session_info_update.dart'; import 'package:badges/badges.dart' as badges; import 'package:stadtbote/tools/messages/status_info_update.dart'; +import 'package:stadtbote/tools/messages/update_badges_message.dart'; import 'package:stadtbote/tools/mqtt_core.dart'; import '../main.dart'; @@ -30,8 +33,8 @@ import '../tools/constants.dart'; import '../tools/util.dart'; class MainPage extends ContentViewWidget { - final ValueNotifier _zipValueNotifier = ValueNotifier("----"); - final ValueNotifier sidValueNotifier = ValueNotifier("----"); + final ValueNotifier _zipValueNotifier = ValueNotifier("-----"); + final ValueNotifier sidValueNotifier = ValueNotifier("-----"); MainPage({super.key}); @@ -233,7 +236,13 @@ class _MainPageState extends State { getDefaultButton( _loggedOut ? 'Anmelden' : 'Abmelden', image: Image.asset(_loggedOut ? 'assets/anmelden.png' : 'assets/abmelden.png', width: 30, height: 30), - () async => {_logout()}), + () async { + if (!kDebugMode) { + _logout(context); + } else { + _test(); + } + }), Expanded( child: Container( decoration: BoxDecoration( @@ -278,7 +287,7 @@ class _MainPageState extends State { _refreshBadges(); }); - MessageService().subscribe(Constants.messageTopicRefreshBadges, this, (RefreshBadgesMessage refreshMessage) { + MessageService().subscribe(Constants.messageTopicUpdateBadges, this, (UpdateBadgesMessage message) { _refreshBadges(); }); } @@ -312,20 +321,58 @@ class _MainPageState extends State { } } - _logout() { + _logout(context) { if (!_loggedOut) { - _sendLogoutMessage(); - - Future.delayed(const Duration(seconds: 1), () async { - MqttCore.instance.diconnect(); - }); + showLogoutAlertDialog(context); } else { MqttCore.setup(); + + _loggedOut = false; + + setState(() {}); } + } - _loggedOut = !_loggedOut; + showLogoutAlertDialog(BuildContext context) { + Widget yesButton = TextButton( + child: const Text("Ja"), + onPressed: () { + Navigator.of(context).pop(); + _sendLogoutMessage(); - setState(() {}); + Future.delayed(const Duration(seconds: 1), () async { + MqttCore.instance.diconnect(); + }); + + _loggedOut = true; + + setState(() {}); + }, + ); + + Widget noButton = TextButton( + child: const Text("Nein"), + onPressed: () { + Navigator.of(context).pop(); + }, + ); + + // set up the AlertDialog + AlertDialog alert = AlertDialog( + title: const Text("Warnung"), + content: const Text("Wollen Sie sich wirklich abmelden?"), + actions: [ + yesButton, noButton, + ], + ); + + // show the dialog + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ); } _sendLogoutMessage() { diff --git a/lib/content_views/messages_page.dart b/lib/content_views/messages_page.dart index 4c782e1..44861e4 100644 --- a/lib/content_views/messages_page.dart +++ b/lib/content_views/messages_page.dart @@ -8,6 +8,7 @@ import 'package:stadtbote/content_views/utils/content_view_widget.dart'; import 'package:stadtbote/content_views/utils/header_info.dart'; import 'package:stadtbote/main.dart'; import 'package:stadtbote/model/message.dart' as sbmessage; +import 'package:stadtbote/model/message.dart'; import 'package:stadtbote/model/session_status.dart'; import 'package:stadtbote/tools/mqtt_core.dart'; @@ -95,16 +96,17 @@ class _MessagesPageState extends State { void _handleSendPressed(types.PartialText text) async { var now = DateTime.now(); - var message = sbmessage.Message(); - message.text = [text.text]; - message.from = _sid; - message.subj = ''; - message.time = now.toIso8601String(); - message.read = true; - message.id = await sbmessage.Message.getNextId(); + var message = MessageMessage(); + message.mssg = sbmessage.Message(); + message.mssg.text = [text.text]; + message.mssg.from = _sid; + message.mssg.subj = ''; + message.mssg.time = now.toUtc().toIso8601String(); + message.mssg.read = true; + message.mssg.id = await sbmessage.Message.getNextId(); - MyApp.isarInstance.writeTxnSync(() { - MyApp.isarInstance.messages.putSync(message); + await MyApp.isarInstance.writeTxn(() async { + await MyApp.isarInstance.messages.put(message.mssg); }); var json = jsonEncode(message.toJson()); @@ -114,7 +116,7 @@ class _MessagesPageState extends State { _messages.insert(0, types.TextMessage( author: _user, createdAt: now.millisecondsSinceEpoch, - id: message.id.toString(), + id: message.mssg.id.toString(), text: text.text, )); @@ -132,18 +134,18 @@ class _MessagesPageState extends State { } text = text.trim(); - _messages.insert(0, types.TextMessage( + _messages.add(types.TextMessage( author: message.from == _sid ? _user : _central, - createdAt: DateTime - .parse(message.time) - .millisecondsSinceEpoch, + createdAt: DateTime.parse(message.time).toLocal().millisecondsSinceEpoch, id: message.id.toString(), text: text, )); + _messages.sort((a, b) => b.createdAt! > a.createdAt! ? 1 : 0); + message.read = true; - MyApp.isarInstance.writeTxnSync(() { - MyApp.isarInstance.messages.putSync(message); + await MyApp.isarInstance.writeTxn(() async { + await MyApp.isarInstance.messages.put(message); }); } @@ -181,8 +183,8 @@ class _MessagesPageState extends State { ), const SizedBox(width: 20), Expanded( - child: getDefaultButton('Ja', () => { - MyApp.isarInstance.writeTxnSync(() => {MyApp.isarInstance.messages.deleteSync(int.parse(message.id))}), + child: getDefaultButton('Ja', () async => { + await MyApp.isarInstance.writeTxnSync(() async => { await MyApp.isarInstance.messages.delete(int.parse(message.id))}), _fetchMessages(), diff --git a/lib/content_views/task_list_page.dart b/lib/content_views/task_list_page.dart index e2c9c49..b20ef00 100644 --- a/lib/content_views/task_list_page.dart +++ b/lib/content_views/task_list_page.dart @@ -20,6 +20,7 @@ import 'package:stadtbote/popups/tasks_popups/take_photos_popup.dart'; import 'package:stadtbote/protocoll_messages/accept_job_message.dart'; import 'package:stadtbote/protocoll_messages/fin_task_message.dart'; import 'package:stadtbote/protocoll_messages/fin_tour_message.dart'; +import 'package:stadtbote/protocoll_messages/pic_message.dart'; import 'package:stadtbote/tools/constants.dart'; import 'package:stadtbote/tools/message_service.dart'; import 'package:stadtbote/tools/mqtt_core.dart'; @@ -64,11 +65,17 @@ class _TaskListPageState extends State { late List _taskList; + List _photos = []; + + late String _commisionNumber; + @override void initState() { Future.delayed(const Duration(seconds: 0), () async { _taskList = await _fetchTaskList(); + _commisionNumber = widget.tour.com ?? ""; + _initCompleted = true; setState(() {}); @@ -93,8 +100,21 @@ class _TaskListPageState extends State { child: AbsorbPointer( absorbing: widget.readOnly, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: steps, + children: [ + Visibility( + visible: _commisionNumber.length > 0, + child: Row( + children: [ + Text('Komm-#: $_commisionNumber', style: const TextStyle(fontSize: 20), textAlign: TextAlign.left), + ], + ), + ), + const SizedBox(height: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: steps, + ), + ], ), ), ), @@ -125,7 +145,7 @@ class _TaskListPageState extends State { left: 16.5, top: i == 0 ? 40 : 40, child: Container( - height: i == widget.tour.tasks!.length - 1 ? 0 : 20, + height: i == widget.tour.tasks.length - 1 ? 0 : 20, width: 6, color: isTaskActive(i) ? Constants.colorPrimary : Constants.colorPrimaryOpaque, ), @@ -159,12 +179,16 @@ class _TaskListPageState extends State { ), ), const SizedBox(height: 5), - Text( - taskInfo.additionalText ?? '', - textAlign: TextAlign.left, - style: TextStyle( - color: textColor, - fontSize: 12.0, + SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + child: Text( + taskInfo.additionalText ?? '', + textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: textColor, + fontSize: 12.0, + ), ), ), ], @@ -187,7 +211,7 @@ class _TaskListPageState extends State { Future> _fetchTaskList() async { List taskList = []; - for(var task in widget.tour.tasks!) { + for(var task in widget.tour.tasks) { var taskInfo = await MyApp.isarInstance.taskInfos.filter().sIdEqualTo(task.sId).findFirst(); if (taskInfo == null) { taskInfo = TaskInfo(); @@ -268,6 +292,8 @@ class _TaskListPageState extends State { taskInfo.additionalText = comissionNumber; taskInfo.completed = true; + _commisionNumber = comissionNumber; + setState(() {}); _finishTaskProcessing(taskInfo); @@ -282,6 +308,7 @@ class _TaskListPageState extends State { answer.codes!.add(barcode); } + taskInfo.answer = jsonEncode(answer.toJson()); taskInfo.additionalText = "Barcodes gescannt"; taskInfo.completed = true; @@ -292,13 +319,17 @@ class _TaskListPageState extends State { } _processTakePhotos(TaskInfo taskInfo) { - showTakePhotosPopup(context, taskInfo, () { - taskInfo.additionalText = "Fotos aufgenommen"; - taskInfo.completed = true; + showTakePhotosPopup(context, taskInfo, (List? photos) { + if (photos != null) { + _photos = photos; - setState(() {}); + taskInfo.additionalText = "Fotos aufgenommen"; + taskInfo.completed = true; - _finishTaskProcessing(taskInfo); + setState(() {}); + + _finishTaskProcessing(taskInfo); + } }); } @@ -308,6 +339,7 @@ class _TaskListPageState extends State { answer.val = notice; taskInfo.answer = jsonEncode(answer.toJson()); + taskInfo.additionalText = notice; taskInfo.completed = true; _finishTaskProcessing(taskInfo); @@ -349,23 +381,29 @@ class _TaskListPageState extends State { } void _processSignature(TaskInfo taskInfo) { + MyApp.suppressAcceptDialog = true; + SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft]); Future.delayed(const Duration(seconds: 1), () async { - showSignaturePopup(context, taskInfo, () { - var answer = _createAnswerHeader(taskInfo); - answer.sign = 'A110'; - - taskInfo.answer = jsonEncode(answer.toJson()); - taskInfo.completed = true; + showSignaturePopup(context, taskInfo, (String? signature) { + MyApp.suppressAcceptDialog = false; SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - _sendFinTourMessage(); + if (signature != null) { + var answer = _createAnswerHeader(taskInfo); + answer.sign = signature; - _finishTaskProcessing(taskInfo); + taskInfo.answer = jsonEncode(answer.toJson()); + taskInfo.completed = true; - MessageService().publish(Constants.messageTopicPopContentView, arguments: PopContentViewMessage(count: 2)); + _sendFinTourMessage(); + + _finishTaskProcessing(taskInfo); + + MessageService().publish(Constants.messageTopicPopContentView, arguments: PopContentViewMessage(count: 2)); + } }); }); } @@ -407,7 +445,7 @@ class _TaskListPageState extends State { message.task.pos!.long = position.longitude; } - MqttCore.instance.publishText("fin_task", jsonEncode(message.toJson())); + MqttCore.instance.publishPayloadMessage("job", finTaskMessage.toJson()); } FinTourTask _createAnswerHeader(TaskInfo taskInfo) { diff --git a/lib/content_views/tour_list_page.dart b/lib/content_views/tour_list_page.dart index 22ac92d..ab3e6f2 100644 --- a/lib/content_views/tour_list_page.dart +++ b/lib/content_views/tour_list_page.dart @@ -63,7 +63,7 @@ class _TourListPageState extends State { return Util.getWaitWidget(context); } - late List jobWidgets = _createjobWidgets(); + late List jobWidgets = _createTourWidgets(); return Padding( padding: const EdgeInsets.all(18.0), @@ -124,14 +124,26 @@ class _TourListPageState extends State { ); } - List _createjobWidgets() { - List jobWidgets = []; + List _createTourWidgets() { + List tourWidgets = []; + + JobTour? lastTour; for (var tour in _tours) { + bool tourActive = false; + + if (lastTour == null) { + tourActive = true; + } else { + if (lastTour.state != 0) { + tourActive = true; + } + } + Color color = Colors.transparent; switch (tour.state) { case 0: - color = Colors.white; + color = tourActive ? Colors.white : Colors.grey; break; case 1: @@ -143,11 +155,17 @@ class _TourListPageState extends State { break; } - jobWidgets.add( + lastTour = tour; + + tourWidgets.add( ListTile( contentPadding: const EdgeInsets.all(0), onTap: () { - _clicked(tour); + if (tourActive) { + _clicked(tour, false); + } else { + _clicked(tour, true); + } }, title: Container( decoration: BoxDecoration( @@ -179,11 +197,10 @@ class _TourListPageState extends State { ); } - return jobWidgets; + return tourWidgets; } - void _clicked(JobTour tour) { - MessageService().publish(Constants.messageTopicPushContentView, - arguments: TourDetailsPage(job: widget.job, tour: tour, readOnly: tour.state != 0)); + void _clicked(JobTour tour, bool readOnly) { + MessageService().publish(Constants.messageTopicPushContentView, arguments: TourDetailsPage(job: widget.job, tour: tour, readOnly: readOnly)); } } diff --git a/lib/main.dart b/lib/main.dart index b43686a..e255d3f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,8 +5,10 @@ import 'package:geolocator/geolocator.dart'; import 'package:isar/isar.dart'; import 'package:path_provider/path_provider.dart'; import 'package:stadtbote/messages/pop_content_view_message.dart'; +import 'package:stadtbote/model/enqueued_message.dart'; +import 'package:stadtbote/model/log_entry.dart'; import 'package:stadtbote/model/message.dart'; -import 'package:stadtbote/model/rank.dart'; +import 'package:stadtbote/model/ranking.dart'; import 'package:stadtbote/model/status.dart'; import 'package:stadtbote/model/task_info.dart'; import 'package:stadtbote/popups/accept_jobs_popup.dart'; @@ -15,8 +17,9 @@ import 'package:stadtbote/model/session_status.dart'; import 'package:stadtbote/tools/connection_status.dart'; import 'package:stadtbote/tools/message_service.dart'; import 'package:stadtbote/tools/messages/job_received_message.dart'; +import 'package:stadtbote/tools/messages/update_badges_message.dart'; +import 'package:stadtbote/tools/mqtt_core.dart'; -import 'tools/mqtt_core.dart'; import 'content_views/main_page.dart'; import 'content_views/utils/content_view_widget.dart'; import 'content_views/utils/widget_stack.dart'; @@ -34,7 +37,9 @@ Future main() async { JobSchema, TaskInfoSchema, MessageSchema, - RankSchema + RankingSchema, + EnqueuedMessageSchema, + LogEntrySchema ], directory: dir.path); runZonedGuarded(() {}, (error, stackTrace) { @@ -60,6 +65,8 @@ class MyApp extends StatelessWidget { static Position? currentPosition; + static bool suppressAcceptDialog = false; + @override Widget build(BuildContext context) { return MaterialApp( @@ -149,6 +156,8 @@ class _MainState extends State
with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _animation; + bool _dialogOpened = false; + @override void initState() { super.initState(); @@ -417,8 +426,18 @@ class _MainState extends State
with SingleTickerProviderStateMixin { } void _jobReceived(Job job) async { - if (job.state == 0) { - await showAcceptJobsPopup(context, await MyApp.isarInstance.jobs.filter().stateEqualTo(0).findAll()); + if (!MyApp.suppressAcceptDialog) { + if (job.state == 0) { + if (!_dialogOpened) { + _dialogOpened = true; + + await showAcceptJobsPopup(context, await MyApp.isarInstance.jobs.filter().stateEqualTo(0).findAll(), () { + _dialogOpened = false; + + MessageService().publish(Constants.messageTopicUpdateBadges, arguments: UpdateBadgesMessage()); + }); + } + } } } } diff --git a/lib/model/enqueued_message.dart b/lib/model/enqueued_message.dart new file mode 100644 index 0000000..27d4cce --- /dev/null +++ b/lib/model/enqueued_message.dart @@ -0,0 +1,13 @@ +import 'package:isar/isar.dart'; + +part 'enqueued_message.g.dart'; + +@collection +class EnqueuedMessage { + Id id = DateTime.now().millisecondsSinceEpoch; + + late String topic; + + late String payload; +} + diff --git a/lib/model/enqueued_message.g.dart b/lib/model/enqueued_message.g.dart new file mode 100644 index 0000000..cca2771 --- /dev/null +++ b/lib/model/enqueued_message.g.dart @@ -0,0 +1,631 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'enqueued_message.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetEnqueuedMessageCollection on Isar { + IsarCollection get enqueuedMessages => this.collection(); +} + +const EnqueuedMessageSchema = CollectionSchema( + name: r'EnqueuedMessage', + id: 884922652682878785, + properties: { + r'payload': PropertySchema( + id: 0, + name: r'payload', + type: IsarType.string, + ), + r'topic': PropertySchema( + id: 1, + name: r'topic', + type: IsarType.string, + ) + }, + estimateSize: _enqueuedMessageEstimateSize, + serialize: _enqueuedMessageSerialize, + deserialize: _enqueuedMessageDeserialize, + deserializeProp: _enqueuedMessageDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _enqueuedMessageGetId, + getLinks: _enqueuedMessageGetLinks, + attach: _enqueuedMessageAttach, + version: '3.1.0+1', +); + +int _enqueuedMessageEstimateSize( + EnqueuedMessage object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.payload.length * 3; + bytesCount += 3 + object.topic.length * 3; + return bytesCount; +} + +void _enqueuedMessageSerialize( + EnqueuedMessage object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.payload); + writer.writeString(offsets[1], object.topic); +} + +EnqueuedMessage _enqueuedMessageDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = EnqueuedMessage(); + object.id = id; + object.payload = reader.readString(offsets[0]); + object.topic = reader.readString(offsets[1]); + return object; +} + +P _enqueuedMessageDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readString(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _enqueuedMessageGetId(EnqueuedMessage object) { + return object.id; +} + +List> _enqueuedMessageGetLinks(EnqueuedMessage object) { + return []; +} + +void _enqueuedMessageAttach( + IsarCollection col, Id id, EnqueuedMessage object) { + object.id = id; +} + +extension EnqueuedMessageQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension EnqueuedMessageQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } +} + +extension EnqueuedMessageQueryFilter + on QueryBuilder { + QueryBuilder + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + payloadEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'payload', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + payloadGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'payload', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + payloadLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'payload', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + payloadBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'payload', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + payloadStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'payload', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + payloadEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'payload', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + payloadContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'payload', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + payloadMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'payload', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + payloadIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'payload', + value: '', + )); + }); + } + + QueryBuilder + payloadIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'payload', + value: '', + )); + }); + } + + QueryBuilder + topicEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'topic', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + topicGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'topic', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + topicLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'topic', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + topicBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'topic', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + topicStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'topic', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + topicEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'topic', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + topicContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'topic', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + topicMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'topic', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + topicIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'topic', + value: '', + )); + }); + } + + QueryBuilder + topicIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'topic', + value: '', + )); + }); + } +} + +extension EnqueuedMessageQueryObject + on QueryBuilder {} + +extension EnqueuedMessageQueryLinks + on QueryBuilder {} + +extension EnqueuedMessageQuerySortBy + on QueryBuilder { + QueryBuilder sortByPayload() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'payload', Sort.asc); + }); + } + + QueryBuilder + sortByPayloadDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'payload', Sort.desc); + }); + } + + QueryBuilder sortByTopic() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'topic', Sort.asc); + }); + } + + QueryBuilder + sortByTopicDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'topic', Sort.desc); + }); + } +} + +extension EnqueuedMessageQuerySortThenBy + on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByPayload() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'payload', Sort.asc); + }); + } + + QueryBuilder + thenByPayloadDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'payload', Sort.desc); + }); + } + + QueryBuilder thenByTopic() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'topic', Sort.asc); + }); + } + + QueryBuilder + thenByTopicDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'topic', Sort.desc); + }); + } +} + +extension EnqueuedMessageQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByPayload( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'payload', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByTopic( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'topic', caseSensitive: caseSensitive); + }); + } +} + +extension EnqueuedMessageQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder payloadProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'payload'); + }); + } + + QueryBuilder topicProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'topic'); + }); + } +} diff --git a/lib/model/job_tour.dart b/lib/model/job_tour.dart index 659b6aa..018ca13 100644 --- a/lib/model/job_tour.dart +++ b/lib/model/job_tour.dart @@ -14,10 +14,10 @@ class JobTour { late String mode; late String comp; late String person; - late String phone; + late String? phone; late String street; late String city; - late String com; + late String? com; late TourPosition? pos; late String rem; late List? tasks; diff --git a/lib/model/job_tour.g.dart b/lib/model/job_tour.g.dart index 5cb71c1..b7c3639 100644 --- a/lib/model/job_tour.g.dart +++ b/lib/model/job_tour.g.dart @@ -94,11 +94,21 @@ int _jobTourEstimateSize( ) { var bytesCount = offsets.last; bytesCount += 3 + object.city.length * 3; - bytesCount += 3 + object.com.length * 3; + { + final value = object.com; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.comp.length * 3; bytesCount += 3 + object.mode.length * 3; bytesCount += 3 + object.person.length * 3; - bytesCount += 3 + object.phone.length * 3; + { + final value = object.phone; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.pos; if (value != null) { @@ -164,12 +174,12 @@ JobTour _jobTourDeserialize( ) { final object = JobTour(); object.city = reader.readString(offsets[0]); - object.com = reader.readString(offsets[1]); + object.com = reader.readStringOrNull(offsets[1]); object.comp = reader.readString(offsets[2]); object.id = reader.readLong(offsets[3]); object.mode = reader.readString(offsets[4]); object.person = reader.readString(offsets[5]); - object.phone = reader.readString(offsets[6]); + object.phone = reader.readStringOrNull(offsets[6]); object.pos = reader.readObjectOrNull( offsets[7], TourPositionSchema.deserialize, @@ -198,7 +208,7 @@ P _jobTourDeserializeProp

( case 0: return (reader.readString(offset)) as P; case 1: - return (reader.readString(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 2: return (reader.readString(offset)) as P; case 3: @@ -208,7 +218,7 @@ P _jobTourDeserializeProp

( case 5: return (reader.readString(offset)) as P; case 6: - return (reader.readString(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 7: return (reader.readObjectOrNull( offset, @@ -367,8 +377,24 @@ extension JobTourQueryFilter }); } + QueryBuilder comIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'com', + )); + }); + } + + QueryBuilder comIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'com', + )); + }); + } + QueryBuilder comEqualTo( - String value, { + String? value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { @@ -381,7 +407,7 @@ extension JobTourQueryFilter } QueryBuilder comGreaterThan( - String value, { + String? value, { bool include = false, bool caseSensitive = true, }) { @@ -396,7 +422,7 @@ extension JobTourQueryFilter } QueryBuilder comLessThan( - String value, { + String? value, { bool include = false, bool caseSensitive = true, }) { @@ -411,8 +437,8 @@ extension JobTourQueryFilter } QueryBuilder comBetween( - String lower, - String upper, { + String? lower, + String? upper, { bool includeLower = true, bool includeUpper = true, bool caseSensitive = true, @@ -939,8 +965,24 @@ extension JobTourQueryFilter }); } + QueryBuilder phoneIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'phone', + )); + }); + } + + QueryBuilder phoneIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'phone', + )); + }); + } + QueryBuilder phoneEqualTo( - String value, { + String? value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { @@ -953,7 +995,7 @@ extension JobTourQueryFilter } QueryBuilder phoneGreaterThan( - String value, { + String? value, { bool include = false, bool caseSensitive = true, }) { @@ -968,7 +1010,7 @@ extension JobTourQueryFilter } QueryBuilder phoneLessThan( - String value, { + String? value, { bool include = false, bool caseSensitive = true, }) { @@ -983,8 +1025,8 @@ extension JobTourQueryFilter } QueryBuilder phoneBetween( - String lower, - String upper, { + String? lower, + String? upper, { bool includeLower = true, bool includeUpper = true, bool caseSensitive = true, @@ -1579,10 +1621,10 @@ JobTour _$JobTourFromJson(Map json) => JobTour() ..mode = json['mode'] as String ..comp = json['comp'] as String ..person = json['person'] as String - ..phone = json['phone'] as String + ..phone = json['phone'] as String? ..street = json['street'] as String ..city = json['city'] as String - ..com = json['com'] as String + ..com = json['com'] as String? ..pos = json['pos'] == null ? null : TourPosition.fromJson(json['pos'] as Map) diff --git a/lib/model/log_entry.dart b/lib/model/log_entry.dart new file mode 100644 index 0000000..b8d223e --- /dev/null +++ b/lib/model/log_entry.dart @@ -0,0 +1,29 @@ +import 'package:isar/isar.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'log_entry.g.dart'; + +enum LogEntrySeverityEnum { + info, + debug, + error +} + +@collection +@JsonSerializable() +class LogEntry { + late Id id = Isar.autoIncrement; + + late DateTime time; + + @enumerated + late LogEntrySeverityEnum severity; + + late String text; + + LogEntry(); + + factory LogEntry.fromJson(Map json) => _$LogEntryFromJson(json); + + Map toJson() => _$LogEntryToJson(this); +} \ No newline at end of file diff --git a/lib/model/log_entry.g.dart b/lib/model/log_entry.g.dart new file mode 100644 index 0000000..791b8be --- /dev/null +++ b/lib/model/log_entry.g.dart @@ -0,0 +1,662 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'log_entry.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetLogEntryCollection on Isar { + IsarCollection get logEntrys => this.collection(); +} + +const LogEntrySchema = CollectionSchema( + name: r'LogEntry', + id: -8268688274231935295, + properties: { + r'severity': PropertySchema( + id: 0, + name: r'severity', + type: IsarType.byte, + enumMap: _LogEntryseverityEnumValueMap, + ), + r'text': PropertySchema( + id: 1, + name: r'text', + type: IsarType.string, + ), + r'time': PropertySchema( + id: 2, + name: r'time', + type: IsarType.dateTime, + ) + }, + estimateSize: _logEntryEstimateSize, + serialize: _logEntrySerialize, + deserialize: _logEntryDeserialize, + deserializeProp: _logEntryDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _logEntryGetId, + getLinks: _logEntryGetLinks, + attach: _logEntryAttach, + version: '3.1.0+1', +); + +int _logEntryEstimateSize( + LogEntry object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.text.length * 3; + return bytesCount; +} + +void _logEntrySerialize( + LogEntry object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeByte(offsets[0], object.severity.index); + writer.writeString(offsets[1], object.text); + writer.writeDateTime(offsets[2], object.time); +} + +LogEntry _logEntryDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = LogEntry(); + object.id = id; + object.severity = + _LogEntryseverityValueEnumMap[reader.readByteOrNull(offsets[0])] ?? + LogEntrySeverityEnum.info; + object.text = reader.readString(offsets[1]); + object.time = reader.readDateTime(offsets[2]); + return object; +} + +P _logEntryDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (_LogEntryseverityValueEnumMap[reader.readByteOrNull(offset)] ?? + LogEntrySeverityEnum.info) as P; + case 1: + return (reader.readString(offset)) as P; + case 2: + return (reader.readDateTime(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +const _LogEntryseverityEnumValueMap = { + 'info': 0, + 'debug': 1, + 'error': 2, +}; +const _LogEntryseverityValueEnumMap = { + 0: LogEntrySeverityEnum.info, + 1: LogEntrySeverityEnum.debug, + 2: LogEntrySeverityEnum.error, +}; + +Id _logEntryGetId(LogEntry object) { + return object.id; +} + +List> _logEntryGetLinks(LogEntry object) { + return []; +} + +void _logEntryAttach(IsarCollection col, Id id, LogEntry object) { + object.id = id; +} + +extension LogEntryQueryWhereSort on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension LogEntryQueryWhere on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder idGreaterThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } +} + +extension LogEntryQueryFilter + on QueryBuilder { + QueryBuilder idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder severityEqualTo( + LogEntrySeverityEnum value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'severity', + value: value, + )); + }); + } + + QueryBuilder severityGreaterThan( + LogEntrySeverityEnum value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'severity', + value: value, + )); + }); + } + + QueryBuilder severityLessThan( + LogEntrySeverityEnum value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'severity', + value: value, + )); + }); + } + + QueryBuilder severityBetween( + LogEntrySeverityEnum lower, + LogEntrySeverityEnum upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'severity', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder textEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'text', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder textGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'text', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder textLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'text', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder textBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'text', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder textStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'text', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder textEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'text', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder textContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'text', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder textMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'text', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder textIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'text', + value: '', + )); + }); + } + + QueryBuilder textIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'text', + value: '', + )); + }); + } + + QueryBuilder timeEqualTo( + DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'time', + value: value, + )); + }); + } + + QueryBuilder timeGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'time', + value: value, + )); + }); + } + + QueryBuilder timeLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'time', + value: value, + )); + }); + } + + QueryBuilder timeBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'time', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension LogEntryQueryObject + on QueryBuilder {} + +extension LogEntryQueryLinks + on QueryBuilder {} + +extension LogEntryQuerySortBy on QueryBuilder { + QueryBuilder sortBySeverity() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'severity', Sort.asc); + }); + } + + QueryBuilder sortBySeverityDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'severity', Sort.desc); + }); + } + + QueryBuilder sortByText() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'text', Sort.asc); + }); + } + + QueryBuilder sortByTextDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'text', Sort.desc); + }); + } + + QueryBuilder sortByTime() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'time', Sort.asc); + }); + } + + QueryBuilder sortByTimeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'time', Sort.desc); + }); + } +} + +extension LogEntryQuerySortThenBy + on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenBySeverity() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'severity', Sort.asc); + }); + } + + QueryBuilder thenBySeverityDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'severity', Sort.desc); + }); + } + + QueryBuilder thenByText() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'text', Sort.asc); + }); + } + + QueryBuilder thenByTextDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'text', Sort.desc); + }); + } + + QueryBuilder thenByTime() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'time', Sort.asc); + }); + } + + QueryBuilder thenByTimeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'time', Sort.desc); + }); + } +} + +extension LogEntryQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctBySeverity() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'severity'); + }); + } + + QueryBuilder distinctByText( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'text', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByTime() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'time'); + }); + } +} + +extension LogEntryQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder + severityProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'severity'); + }); + } + + QueryBuilder textProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'text'); + }); + } + + QueryBuilder timeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'time'); + }); + } +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LogEntry _$LogEntryFromJson(Map json) => LogEntry() + ..id = json['id'] as int + ..time = DateTime.parse(json['time'] as String) + ..severity = $enumDecode(_$LogEntrySeverityEnumEnumMap, json['severity']) + ..text = json['text'] as String; + +Map _$LogEntryToJson(LogEntry instance) => { + 'id': instance.id, + 'time': instance.time.toIso8601String(), + 'severity': _$LogEntrySeverityEnumEnumMap[instance.severity]!, + 'text': instance.text, + }; + +const _$LogEntrySeverityEnumEnumMap = { + LogEntrySeverityEnum.info: 'info', + LogEntrySeverityEnum.debug: 'debug', + LogEntrySeverityEnum.error: 'error', +}; diff --git a/lib/model/message.dart b/lib/model/message.dart index 20500aa..873154b 100644 --- a/lib/model/message.dart +++ b/lib/model/message.dart @@ -5,6 +5,17 @@ import 'package:stadtbote/tools/util.dart'; part 'message.g.dart'; +@JsonSerializable() +class MessageMessage { + late Message mssg; + + MessageMessage(); + + factory MessageMessage.fromJson(Map json) => _$MessageMessageFromJson(json); + + Map toJson() => _$MessageMessageToJson(this); +} + @collection @JsonSerializable() class Message { diff --git a/lib/model/message.g.dart b/lib/model/message.g.dart index 4a485f7..8cec015 100644 --- a/lib/model/message.g.dart +++ b/lib/model/message.g.dart @@ -1098,6 +1098,15 @@ extension MessageQueryProperty // JsonSerializableGenerator // ************************************************************************** +MessageMessage _$MessageMessageFromJson(Map json) => + MessageMessage() + ..mssg = Message.fromJson(json['mssg'] as Map); + +Map _$MessageMessageToJson(MessageMessage instance) => + { + 'mssg': instance.mssg, + }; + Message _$MessageFromJson(Map json) => Message() ..id = json['id'] as int ..time = json['time'] as String diff --git a/lib/popups/accept_jobs_popup.dart b/lib/popups/accept_jobs_popup.dart index 6bff8dd..20b9454 100644 --- a/lib/popups/accept_jobs_popup.dart +++ b/lib/popups/accept_jobs_popup.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -8,8 +6,6 @@ import 'package:stadtbote/protocoll_messages/accept_job_message.dart'; import 'package:stadtbote/model/job.dart'; import 'package:stadtbote/protocoll_messages/fin_task_message.dart'; import 'package:stadtbote/tools/constants.dart'; -import 'package:stadtbote/tools/message_service.dart'; -import 'package:stadtbote/tools/messages/refresh_badges_message.dart'; import 'package:stadtbote/tools/mqtt_core.dart'; import 'package:stadtbote/tools/util.dart'; import 'package:badges/badges.dart' as badges; @@ -20,138 +16,192 @@ Future showAcceptJobsPopup(BuildContext context, List jobs) async { await showDialog( context: context, builder: (BuildContext context) { - return AlertDialog( - contentPadding: EdgeInsets.zero, - shadowColor: Colors.transparent, - backgroundColor: Colors.white, - content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { - if (jobsSet.isEmpty) { - Navigator.of(context).pop(); - } + return WillPopScope( + onWillPop: () async { + MessageService().unsubscribe(Constants.messageTopicJobDeleted, jobsMap); + MessageService().unsubscribe(Constants.messageTopicJobReceived, jobsMap); - return SizedBox( - key: GlobalKey(), - height: MediaQuery.of(context).size.height * 1, - width: MediaQuery.of(context).size.width * 1.2, - child: Container( - height: 100, - padding: const EdgeInsets.all(5), - child: ListView.builder( - itemCount: jobsSet.length, - itemBuilder: (_, i) { - var job = jobsSet.toList()[i]; + Navigator.of(context).pop(); - return ExpandableNotifier( - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - title: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Constants.colorPrimary, - width: 1, - ), - ), - child: badges.Badge( - position: badges.BadgePosition.topEnd(top: -13, end: -5), - showBadge: false, - badgeContent: const Text( - 'Neu', - style: TextStyle( - color: Colors.white, - fontSize: 18.0, + onFinished(); + + return true; + }, + child: AlertDialog( + contentPadding: EdgeInsets.zero, + shadowColor: Colors.transparent, + backgroundColor: Colors.white, + content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + if (jobsMap.isEmpty) { + Navigator.of(context).pop(); + + onFinished(); + } + + MessageService().unsubscribe(Constants.messageTopicJobDeleted, jobsMap); + MessageService().unsubscribe(Constants.messageTopicJobReceived, jobsMap); + + MessageService().subscribe(Constants.messageTopicJobDeleted, jobsMap, (jobDeletedMessage) async { + jobsMap.removeWhere((key, value) => key == jobDeletedMessage.jobId); + + if (context.mounted) { + setState(() {}); + } + }); + + MessageService().subscribe(Constants.messageTopicJobReceived, jobsMap, (jobdMessage) async { + if (jobsMap.containsKey(jobdMessage.job.id)) { + jobsMap.removeWhere((key, value) => key == jobdMessage.job.id); + } + + jobsMap[jobdMessage.job.id] = jobdMessage.job; + + if (context.mounted) { + setState(() {}); + } + }); + + return SizedBox( + key: GlobalKey(), + height: MediaQuery.of(context).size.height * 1, + width: MediaQuery.of(context).size.width * 1.2, + child: Container( + height: 100, + padding: const EdgeInsets.all(5), + child: ListView.builder( + itemCount: jobsMap.length, + itemBuilder: (_, i) { + var job = jobsMap.values.toList()[i]; + + String remarkString = ''; + for (var remark in job.rem) { + remarkString += '$remark\n'; + } + remarkString = remarkString.trim(); + + return ExpandableNotifier( + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + title: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Constants.colorPrimary, + width: 1, + ), ), - ), - badgeStyle: badges.BadgeStyle( - shape: badges.BadgeShape.square, - badgeColor: Constants.colorPrimary, - padding: const EdgeInsets.all(5), - borderRadius: BorderRadius.circular(5), - borderSide: const BorderSide(color: Constants.colorPrimary, width: 0), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - children: [ - SizedBox( - width: double.infinity, - child: Text( - DateFormat('dd.MM.yyyy HH:mm').format(DateTime.parse(job.ordertime)).toString(), - textAlign: TextAlign.left, - style: TextStyle(backgroundColor: isJobInFuture(job) ? Colors.yellow : Colors.white), - ), + child: badges.Badge( + position: badges.BadgePosition.topEnd(top: -13, end: -5), + showBadge: false, + badgeContent: const Text( + 'Neu', + style: TextStyle( + color: Colors.white, + fontSize: 18.0, ), - SizedBox(width: double.infinity, child: Text('Auftragsnummer: ${job.id}', textAlign: TextAlign.left)), - SizedBox(width: double.infinity, child: Text(job.tours[0].comp, textAlign: TextAlign.left)), - SizedBox(width: double.infinity, child: Text(job.tours[0].street, textAlign: TextAlign.left)), - SizedBox(width: double.infinity, child: Text(job.tours[0].city, textAlign: TextAlign.left)), - Row( + ), + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.square, + badgeColor: Constants.colorPrimary, + padding: const EdgeInsets.all(5), + borderRadius: BorderRadius.circular(5), + borderSide: const BorderSide(color: Constants.colorPrimary, width: 0), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( children: [ - Image.asset("assets/${mapVehicleTypeToImageName(job.vhtId)}", width: 60, height: 60), - const SizedBox(width: 10), - Text(mapVehicleTypeToText(job.vhtId)), - ], - ), - const SizedBox(height: 10), - ScrollOnExpand( - scrollOnExpand: true, - scrollOnCollapse: false, - child: ExpandablePanel( - theme: const ExpandableThemeData( - headerAlignment: ExpandablePanelHeaderAlignment.center, - tapBodyToCollapse: true, + SizedBox( + width: double.infinity, + child: Text( + DateFormat('dd.MM.yyyy HH:mm').format(DateTime.parse(job.ordertime).toLocal()).toString(), + textAlign: TextAlign.left, + style: TextStyle(backgroundColor: isJobInFuture(job) ? Colors.yellow : Colors.transparent), + ), ), - header: const Padding( - padding: EdgeInsets.all(0), - child: Text( - "Weitere Infos....", - style: TextStyle(fontSize: 15, color: Colors.black, fontWeight: FontWeight.bold), - )), - collapsed: const SizedBox(), - expanded: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: const Color(0xfffff4e4), - border: Border.all( - color: Colors.grey, - width: 0.5, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(job.rem), - ), - ), + SizedBox(width: double.infinity, child: Text('Auftragsnummer: ${job.id}', textAlign: TextAlign.left)), + SizedBox(width: double.infinity, child: Text(job.tours[0].comp, textAlign: TextAlign.left)), + SizedBox(width: double.infinity, child: Text(job.tours[0].street, textAlign: TextAlign.left)), + SizedBox(width: double.infinity, child: Text(job.tours[0].city, textAlign: TextAlign.left)), + Row( + children: [ + Image.asset("assets/${mapVehicleTypeToImageName(job.vhtId)}", width: 60, height: 60), + const SizedBox(width: 10), + Text(mapVehicleTypeToText(job.vhtId)), ], ), - ), + const SizedBox(height: 10), + ScrollOnExpand( + scrollOnExpand: true, + scrollOnCollapse: false, + child: ExpandablePanel( + theme: const ExpandableThemeData( + headerAlignment: ExpandablePanelHeaderAlignment.center, + tapBodyToCollapse: true, + ), + header: const Padding( + padding: EdgeInsets.all(0), + child: Text( + "Weitere Infos....", + style: TextStyle(fontSize: 15, color: Colors.black, fontWeight: FontWeight.bold), + )), + collapsed: const SizedBox(), + expanded: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: const Color(0xfffff4e4), + border: Border.all( + color: Colors.grey, + width: 0.5, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(remarkString), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + Row(children: [ + Expanded( + child: getDefaultButton(!job.global ? 'Ablehnen' : 'Ignorieren', + () { + _sendAnswer(job, !job.global ? 2 : 3); + + jobsMap.removeWhere((key, value) => key == job.id); + + setState(() {}); + }), + ), + const SizedBox(width: 20), + Expanded( + child: getDefaultButton('Annehmen', () { + _sendAnswer(job, 1); + + jobsMap.removeWhere((key, value) => key == job.id); + + setState(() {}); + }), + ) + ]) + ], ), - const SizedBox(height: 20), - Row(children: [ - Expanded( - child: getDefaultButton(!job.global ? 'Ablehnen' : 'Ignorieren', - () => {_sendAnswer(job, !job.global ? 2 : 3), jobsSet.remove(job), setState(() {})}), - ), - const SizedBox(width: 20), - Expanded( - child: getDefaultButton('Annehmen', () => {_sendAnswer(job, 1), jobsSet.remove(job), setState(() {})}), - ) - ]) - ], + ), ), ), ), - ), - ), - ); - }, - )), - ); - })); + ); + }, + )), + ); + }))); }); } diff --git a/lib/popups/infos_popup.dart b/lib/popups/infos_popup.dart index 1d9c6f8..ec7c200 100644 --- a/lib/popups/infos_popup.dart +++ b/lib/popups/infos_popup.dart @@ -5,7 +5,9 @@ import 'package:stadtbote/tools/mqtt_core.dart'; import 'package:stadtbote/tools/util.dart'; void showInfosPopup(BuildContext context, SessionStatus? status) async { - var text = "Versionsnummer: a203\n"; + var version = await Util.getAppVersion(); + + var text = "Versionsnummer: a$version\n"; text += "Verbindungsstatus: ${MqttCore.instance.connectionState == ConnectionStateEnum.connected ? "Verbunden" : "NICHT verbunden"}\n"; text += "GPS-Status: ${MyApp.currentPosition != null ? MyApp.currentPosition!.timestamp.toString() : "Unbekannt"}\n"; text += "\n"; diff --git a/lib/popups/map_popup.dart b/lib/popups/map_popup.dart index 6f53d8d..b93bfae 100644 --- a/lib/popups/map_popup.dart +++ b/lib/popups/map_popup.dart @@ -4,6 +4,7 @@ import 'package:animated_toggle_switch/animated_toggle_switch.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:stadtbote/tools/util.dart'; import '../model/job_tour.dart'; @@ -27,12 +28,16 @@ showMapPopup(BuildContext context, JobTour currentTour, List tourList) tourWidgets.add( GestureDetector( onTap: () async { - mapController.move(LatLng(tour.pos!.lat, tour.pos!.long), mapController.zoom); + if (tour.pos != null) { + mapController.move(LatLng(tour.pos!.lat, tour.pos!.long), mapController.zoom); - streamController[currentTour.sort]!.sink.add(false); + streamController[currentTour.sort]!.sink.add(false); - currentTour = tour; - streamController[tour.sort]!.sink.add(true); + currentTour = tour; + streamController[tour.sort]!.sink.add(true); + } else { + showAlertDialog(context, 'Information', 'Es liegt keine Position für diese Station vor'); + } }, child: SizedBox( width: 70, @@ -87,17 +92,20 @@ showMapPopup(BuildContext context, JobTour currentTour, List tourList) List markers = []; for (var tour in tourList) { - markers.add(Marker( - builder: (context) => const Icon(Icons.person_pin_circle, size: 56, color: Colors.blue), - point: LatLng(tour.pos!.lat, tour.pos!.long))); + if (tour.pos != null) { + markers.add(Marker( + builder: (context) => const Icon(Icons.person_pin_circle, size: 56, color: Colors.blue), + point: LatLng(tour.pos!.lat, tour.pos!.long))); + } } return Container( - padding: const EdgeInsets.fromLTRB(30, 0, 30, 0), + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), key: GlobalKey(), height: MediaQuery.of(context).size.height * 0.6, child: Container( height: 100, + width: MediaQuery.of(context).size.width * 1, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Colors.white, diff --git a/lib/popups/tasks_popups/receipt_giver_popup.dart b/lib/popups/tasks_popups/receipt_giver_popup.dart index a303387..15d9044 100644 --- a/lib/popups/tasks_popups/receipt_giver_popup.dart +++ b/lib/popups/tasks_popups/receipt_giver_popup.dart @@ -40,10 +40,15 @@ showReceiptGiverPopup(BuildContext context, TaskInfo taskInfo, Function(String) controller: textEditingController, autofocus: true), const SizedBox(height: 20), - getDefaultButton('Übernehmen', () => { - Navigator.of(context).pop(), + getDefaultButton('Übernehmen', () { + if (textEditingController.text.isNotEmpty) { + Navigator.of(context).pop(); - onFinished(textEditingController.text)}) + onFinished(textEditingController.text); + } else { + showAlertDialog(context, "Fehler", "Sie müssen einen Text eingeben"); + } + }) ], ), ), diff --git a/lib/popups/tasks_popups/signature_popup.dart b/lib/popups/tasks_popups/signature_popup.dart index fd484d3..d0ceff8 100644 --- a/lib/popups/tasks_popups/signature_popup.dart +++ b/lib/popups/tasks_popups/signature_popup.dart @@ -3,11 +3,19 @@ import 'package:signature/signature.dart'; import 'package:stadtbote/model/task_info.dart'; import 'package:stadtbote/tools/util.dart'; -showSignaturePopup(BuildContext context, TaskInfo taskInfo, Function() onFinished) async { +showSignaturePopup(BuildContext context, TaskInfo taskInfo, Function(String?) onFinished) async { await showDialog( context: context, builder: (BuildContext context) { - return AlertDialog( + return WillPopScope( + onWillPop: () async { + Navigator.of(context).pop(); + + onFinished(null); + + return true; + },// False will prevent and true will allow to dismiss + child: AlertDialog( contentPadding: EdgeInsets.zero, shadowColor: Colors.transparent, backgroundColor: Colors.white, @@ -18,6 +26,24 @@ showSignaturePopup(BuildContext context, TaskInfo taskInfo, Function() onFinishe var popupHeight = MediaQuery.of(context).size.height * 0.85; var popupWidth = MediaQuery.of(context).size.width * 0.85; + String convertPoints(List points) { + if (points.isNotEmpty) { + String result = ""; + + for (var point in points) { + if (point.type != PointType.tap) { + result += "${point.offset.dx.toInt()},${point.offset.dy.toInt()};"; + } else { + result += "A"; + } + } + + return result.replaceAll("AA", "A"); + } + + return ""; + } + return SizedBox( key: GlobalKey(), height: popupHeight, @@ -46,14 +72,19 @@ showSignaturePopup(BuildContext context, TaskInfo taskInfo, Function() onFinishe ), ), getDefaultButton('Übernehmen', buttonHeight: 50, () => { - Navigator.of(context).pop(), + if (controller.points.length > 50) { + Navigator.of(context).pop(), - onFinished()}) + onFinished(convertPoints(controller.points)) + } else { + showAlertDialog(context, "Fehler", "Die Unterschrift ist zu kurz.") + } + }) ], ), ), )); }, - )); + ))); }); } diff --git a/lib/popups/tasks_popups/take_photos_popup.dart b/lib/popups/tasks_popups/take_photos_popup.dart index 88af5b4..1a37a92 100644 --- a/lib/popups/tasks_popups/take_photos_popup.dart +++ b/lib/popups/tasks_popups/take_photos_popup.dart @@ -1,10 +1,11 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:image/image.dart' as imageLib; import 'package:stadtbote/model/task_info.dart'; import 'package:stadtbote/tools/util.dart'; -Future showTakePhotosPopup(BuildContext context, TaskInfo taskInfo, Function() onFinished) async { +Future showTakePhotosPopup(BuildContext context, TaskInfo taskInfo, Function(List) onFinished) async { List imageDataList = []; List imageWidgetsList = []; @@ -19,22 +20,35 @@ Future showTakePhotosPopup(BuildContext context, TaskInfo taskInfo, Functi backgroundColor: Colors.white, content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { void capturePhoto() async { - var photo = await Util.capturePhoto(); + try { + var photo = await Util.capturePhoto(); - if (photo != null) { - var imageData = await photo.readAsBytes(); - photo.delete(); + if (photo != null) { + var fullsizeImageData = await photo.readAsBytes(); - imageDataList.add(imageData); + imageLib.Image? image = imageLib.decodeImage(fullsizeImageData); - imageWidgetsList.add(GestureDetector( - onTap: () {}, - child: Image.memory(imageData), - )); + if (image != null) { + var thumbnail = imageLib.copyResize(image, width: 800); - imageWidgetsListNotifier.value = imageWidgetsList; + var resizedImageData = imageLib.encodeJpg(thumbnail, quality: 75); - setState(() {}); + photo.delete(); + + imageDataList.add(resizedImageData); + + imageWidgetsList.add(GestureDetector( + onTap: () {}, + child: Image.memory(resizedImageData), + )); + + imageWidgetsListNotifier.value = imageWidgetsList; + + setState(() {}); + } + } + } catch (ex) { + Util.logException(ex.toString()); } } @@ -90,7 +104,7 @@ Future showTakePhotosPopup(BuildContext context, TaskInfo taskInfo, Functi buttonHeight: 50, () => { if (imageDataList.length == taskInfo.task.min! && imageDataList.length <= taskInfo.task.max!) - {Navigator.of(context).pop(), onFinished()} + {Navigator.of(context).pop(), onFinished(imageDataList)} else {showAlertDialog(context, 'Fehler', 'Sie müssen mindestens ${taskInfo.task.min} Foto(s) aufnehmen')} }) diff --git a/lib/protocoll_messages/accept_job_message.dart b/lib/protocoll_messages/accept_job_message.dart index fdd3317..6414eae 100644 --- a/lib/protocoll_messages/accept_job_message.dart +++ b/lib/protocoll_messages/accept_job_message.dart @@ -20,7 +20,8 @@ class Reaction { @JsonSerializable() @JsonSerializable()class Track { late String time; - late FinTaskPos pos; + + FinTaskPos? pos; Track(); diff --git a/lib/protocoll_messages/accept_job_message.g.dart b/lib/protocoll_messages/accept_job_message.g.dart index 890970f..3c2195d 100644 --- a/lib/protocoll_messages/accept_job_message.g.dart +++ b/lib/protocoll_messages/accept_job_message.g.dart @@ -21,7 +21,9 @@ Map _$ReactionToJson(Reaction instance) => { Track _$TrackFromJson(Map json) => Track() ..time = json['time'] as String - ..pos = FinTaskPos.fromJson(json['pos'] as Map); + ..pos = json['pos'] == null + ? null + : FinTaskPos.fromJson(json['pos'] as Map); Map _$TrackToJson(Track instance) => { 'time': instance.time, diff --git a/lib/protocoll_messages/acknowledge_job.dart b/lib/protocoll_messages/acknowledge_job.dart new file mode 100644 index 0000000..6fb92e2 --- /dev/null +++ b/lib/protocoll_messages/acknowledge_job.dart @@ -0,0 +1,32 @@ +import 'package:isar/isar.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'acknowledge_job.g.dart'; + +@JsonSerializable() +class AcknowledgeJob { + late AcknowledgeJobReceived received; + + AcknowledgeJob(); + + factory AcknowledgeJob.fromJson(Map json) => _$AcknowledgeJobFromJson(json); + + Map toJson() => _$AcknowledgeJobToJson(this); +} + +@JsonSerializable() +class AcknowledgeJobReceived { + late int id; + + late int state; + + late int ver; + + late int utc; + + AcknowledgeJobReceived(); + + factory AcknowledgeJobReceived.fromJson(Map json) => _$AcknowledgeJobReceivedFromJson(json); + + Map toJson() => _$AcknowledgeJobReceivedToJson(this); +} diff --git a/lib/protocoll_messages/acknowledge_job.g.dart b/lib/protocoll_messages/acknowledge_job.g.dart new file mode 100644 index 0000000..697f489 --- /dev/null +++ b/lib/protocoll_messages/acknowledge_job.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'acknowledge_job.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AcknowledgeJob _$AcknowledgeJobFromJson(Map json) => + AcknowledgeJob() + ..received = AcknowledgeJobReceived.fromJson( + json['received'] as Map); + +Map _$AcknowledgeJobToJson(AcknowledgeJob instance) => + { + 'received': instance.received, + }; + +AcknowledgeJobReceived _$AcknowledgeJobReceivedFromJson( + Map json) => + AcknowledgeJobReceived() + ..id = json['id'] as int + ..state = json['state'] as int + ..ver = json['ver'] as int + ..utc = json['utc'] as int; + +Map _$AcknowledgeJobReceivedToJson( + AcknowledgeJobReceived instance) => + { + 'id': instance.id, + 'state': instance.state, + 'ver': instance.ver, + 'utc': instance.utc, + }; diff --git a/lib/protocoll_messages/device_log_request.dart b/lib/protocoll_messages/device_log_request.dart new file mode 100644 index 0000000..976ed81 --- /dev/null +++ b/lib/protocoll_messages/device_log_request.dart @@ -0,0 +1,18 @@ +import 'dart:core'; + +import 'package:json_annotation/json_annotation.dart'; + +part 'device_log_request.g.dart'; + +@JsonSerializable() +class DeviceLogRequest { + late DateTime startDateTime; + + late DateTime endDateTime; + + DeviceLogRequest(); + + Map toJson() => _$DeviceLogRequestToJson(this); + + factory DeviceLogRequest.fromJson(Map json) => _$DeviceLogRequestFromJson(json); +} diff --git a/lib/protocoll_messages/device_log_request.g.dart b/lib/protocoll_messages/device_log_request.g.dart new file mode 100644 index 0000000..ed73e40 --- /dev/null +++ b/lib/protocoll_messages/device_log_request.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_log_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DeviceLogRequest _$DeviceLogRequestFromJson(Map json) => + DeviceLogRequest() + ..startDateTime = DateTime.parse(json['startDateTime'] as String) + ..endDateTime = DateTime.parse(json['endDateTime'] as String); + +Map _$DeviceLogRequestToJson(DeviceLogRequest instance) => + { + 'startDateTime': instance.startDateTime.toIso8601String(), + 'endDateTime': instance.endDateTime.toIso8601String(), + }; diff --git a/lib/protocoll_messages/device_log_response.dart b/lib/protocoll_messages/device_log_response.dart new file mode 100644 index 0000000..44ed022 --- /dev/null +++ b/lib/protocoll_messages/device_log_response.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stadtbote/model/log_entry.dart'; + +part 'device_log_response.g.dart'; + +@JsonSerializable() +class DeviceLogResponse { + late List entries; + + DeviceLogResponse(); + + Map toJson() => _$DeviceLogResponseToJson(this); + + factory DeviceLogResponse.fromJson(Map json) => _$DeviceLogResponseFromJson(json); +} diff --git a/lib/protocoll_messages/device_log_response.g.dart b/lib/protocoll_messages/device_log_response.g.dart new file mode 100644 index 0000000..afd2517 --- /dev/null +++ b/lib/protocoll_messages/device_log_response.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_log_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DeviceLogResponse _$DeviceLogResponseFromJson(Map json) => + DeviceLogResponse() + ..entries = (json['entries'] as List) + .map((e) => LogEntry.fromJson(e as Map)) + .toList(); + +Map _$DeviceLogResponseToJson(DeviceLogResponse instance) => + { + 'entries': instance.entries, + }; diff --git a/lib/protocoll_messages/fin_task_message.dart b/lib/protocoll_messages/fin_task_message.dart index ef675e9..d74378b 100644 --- a/lib/protocoll_messages/fin_task_message.dart +++ b/lib/protocoll_messages/fin_task_message.dart @@ -35,7 +35,7 @@ class Task { late String time; - late FinTaskPos? pos; + FinTaskPos? pos; Task(); diff --git a/lib/protocoll_messages/fin_tour_message.dart b/lib/protocoll_messages/fin_tour_message.dart index c453321..460871b 100644 --- a/lib/protocoll_messages/fin_tour_message.dart +++ b/lib/protocoll_messages/fin_tour_message.dart @@ -23,7 +23,7 @@ class FinTour { late String com; - late Track track; + Track? track; late List tasks; @@ -49,7 +49,7 @@ class FinTourTask { List? codes; - late FinTaskPos pos; + FinTaskPos? pos; FinTourTask(); diff --git a/lib/protocoll_messages/fin_tour_message.g.dart b/lib/protocoll_messages/fin_tour_message.g.dart index 4e55c17..91cf2d4 100644 --- a/lib/protocoll_messages/fin_tour_message.g.dart +++ b/lib/protocoll_messages/fin_tour_message.g.dart @@ -18,7 +18,9 @@ Map _$FinTourMessageToJson(FinTourMessage instance) => FinTour _$FinTourFromJson(Map json) => FinTour() ..trId = json['tr_id'] as int ..com = json['com'] as String - ..track = Track.fromJson(json['track'] as Map) + ..track = json['track'] == null + ? null + : Track.fromJson(json['track'] as Map) ..tasks = (json['tasks'] as List) .map((e) => FinTourTask.fromJson(e as Map)) .toList(); @@ -37,7 +39,9 @@ FinTourTask _$FinTourTaskFromJson(Map json) => FinTourTask() ..code = json['code'] as int? ..sign = json['sign'] as String? ..codes = (json['codes'] as List?)?.map((e) => e as String).toList() - ..pos = FinTaskPos.fromJson(json['pos'] as Map); + ..pos = json['pos'] == null + ? null + : FinTaskPos.fromJson(json['pos'] as Map); Map _$FinTourTaskToJson(FinTourTask instance) => { diff --git a/lib/protocoll_messages/logout_message.dart b/lib/protocoll_messages/logout_message.dart index 8026364..5eec9b7 100644 --- a/lib/protocoll_messages/logout_message.dart +++ b/lib/protocoll_messages/logout_message.dart @@ -4,13 +4,9 @@ part 'logout_message.g.dart'; @JsonSerializable() class LogoutMessage { - late String time; + late bool logout; - late double lat; - - late double long; - - late int acc; + LogoutMessagePosition? pos; LogoutMessage(); @@ -19,3 +15,20 @@ class LogoutMessage { Map toJson() => _$LogoutMessageToJson(this); } +@JsonSerializable() +class LogoutMessagePosition { + late String time; + + late double lat; + + late double long; + + late int acc; + + LogoutMessagePosition(); + + factory LogoutMessagePosition.fromJson(Map json) => _$LogoutMessagePositionFromJson(json); + + Map toJson() => _$LogoutMessagePositionToJson(this); +} + diff --git a/lib/protocoll_messages/pic_message.dart b/lib/protocoll_messages/pic_message.dart new file mode 100644 index 0000000..4058275 --- /dev/null +++ b/lib/protocoll_messages/pic_message.dart @@ -0,0 +1,50 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'pic_message.g.dart'; + +@JsonSerializable() +class PicMessage { + late PicData pic; + + PicMessagePos? pos; + + PicMessage(); + + factory PicMessage.fromJson(Map json) => _$PicMessageFromJson(json); + + Map toJson() => _$PicMessageToJson(this); +} + +@JsonSerializable() +class PicData +{ + @JsonKey(name: "tr_id") + late int trId; + + late int num; + + late int time; + + late String data; + + PicData(); + + factory PicData.fromJson(Map json) => _$PicDataFromJson(json); + + Map toJson() => _$PicDataToJson(this); +} + +@JsonSerializable() +class PicMessagePos { + late double lat; + + late double long; + + late int acc = 0; + + PicMessagePos(); + + factory PicMessagePos.fromJson(Map json) => _$PicMessagePosFromJson(json); + + Map toJson() => _$PicMessagePosToJson(this); +} \ No newline at end of file diff --git a/lib/protocoll_messages/pic_message.g.dart b/lib/protocoll_messages/pic_message.g.dart new file mode 100644 index 0000000..0c8361c --- /dev/null +++ b/lib/protocoll_messages/pic_message.g.dart @@ -0,0 +1,45 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pic_message.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PicMessage _$PicMessageFromJson(Map json) => PicMessage() + ..pic = PicData.fromJson(json['pic'] as Map) + ..pos = json['pos'] == null + ? null + : PicMessagePos.fromJson(json['pos'] as Map); + +Map _$PicMessageToJson(PicMessage instance) => + { + 'pic': instance.pic, + 'pos': instance.pos, + }; + +PicData _$PicDataFromJson(Map json) => PicData() + ..trId = json['tr_id'] as int + ..num = json['num'] as int + ..time = json['time'] as int + ..data = json['data'] as String; + +Map _$PicDataToJson(PicData instance) => { + 'tr_id': instance.trId, + 'num': instance.num, + 'time': instance.time, + 'data': instance.data, + }; + +PicMessagePos _$PicMessagePosFromJson(Map json) => + PicMessagePos() + ..lat = (json['lat'] as num).toDouble() + ..long = (json['long'] as num).toDouble() + ..acc = json['acc'] as int; + +Map _$PicMessagePosToJson(PicMessagePos instance) => + { + 'lat': instance.lat, + 'long': instance.long, + 'acc': instance.acc, + }; diff --git a/lib/protocoll_messages/track_message.dart b/lib/protocoll_messages/track_message.dart index 07f1885..37552e1 100644 --- a/lib/protocoll_messages/track_message.dart +++ b/lib/protocoll_messages/track_message.dart @@ -4,7 +4,7 @@ part 'track_message.g.dart'; @JsonSerializable() class TrackMessage { - late TrackPosition pos; + TrackPosition? pos; TrackMessage(); diff --git a/lib/protocoll_messages/track_message.g.dart b/lib/protocoll_messages/track_message.g.dart index 643695e..bf02fad 100644 --- a/lib/protocoll_messages/track_message.g.dart +++ b/lib/protocoll_messages/track_message.g.dart @@ -7,7 +7,9 @@ part of 'track_message.dart'; // ************************************************************************** TrackMessage _$TrackMessageFromJson(Map json) => TrackMessage() - ..pos = TrackPosition.fromJson(json['pos'] as Map); + ..pos = json['pos'] == null + ? null + : TrackPosition.fromJson(json['pos'] as Map); Map _$TrackMessageToJson(TrackMessage instance) => { diff --git a/lib/protocoll_messages/track_station_message.dart b/lib/protocoll_messages/track_station_message.dart index c2941cb..ce813d8 100644 --- a/lib/protocoll_messages/track_station_message.dart +++ b/lib/protocoll_messages/track_station_message.dart @@ -10,7 +10,7 @@ class TrackStationMessage { late String time; - late FinTaskPos? pos; + FinTaskPos? pos; TrackStationMessage(); diff --git a/lib/protocoll_messages/zip_message.dart b/lib/protocoll_messages/zip_message.dart index b3e4030..2159279 100644 --- a/lib/protocoll_messages/zip_message.dart +++ b/lib/protocoll_messages/zip_message.dart @@ -61,7 +61,7 @@ class ZipReqZip { class ZipSetPos { late String zip; - late ZipTrack track; + ZipTrack? track; ZipSetPos(); @@ -75,7 +75,7 @@ class ZipSetPos { class ZipTrack { late String time; - late ZipTrackPos? pos; + ZipTrackPos? pos; ZipTrack(); diff --git a/lib/protocoll_messages/zip_message.g.dart b/lib/protocoll_messages/zip_message.g.dart index c4c764c..cbd677f 100644 --- a/lib/protocoll_messages/zip_message.g.dart +++ b/lib/protocoll_messages/zip_message.g.dart @@ -298,9 +298,13 @@ int _zipSetPosEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; - bytesCount += 3 + - ZipTrackSchema.estimateSize( - object.track, allOffsets[ZipTrack]!, allOffsets); + { + final value = object.track; + if (value != null) { + bytesCount += 3 + + ZipTrackSchema.estimateSize(value, allOffsets[ZipTrack]!, allOffsets); + } + } bytesCount += 3 + object.zip.length * 3; return bytesCount; } @@ -328,11 +332,10 @@ ZipSetPos _zipSetPosDeserialize( ) { final object = ZipSetPos(); object.track = reader.readObjectOrNull( - offsets[0], - ZipTrackSchema.deserialize, - allOffsets, - ) ?? - ZipTrack(); + offsets[0], + ZipTrackSchema.deserialize, + allOffsets, + ); object.zip = reader.readString(offsets[1]); return object; } @@ -346,11 +349,10 @@ P _zipSetPosDeserializeProp

( switch (propertyId) { case 0: return (reader.readObjectOrNull( - offset, - ZipTrackSchema.deserialize, - allOffsets, - ) ?? - ZipTrack()) as P; + offset, + ZipTrackSchema.deserialize, + allOffsets, + )) as P; case 1: return (reader.readString(offset)) as P; default: @@ -360,6 +362,22 @@ P _zipSetPosDeserializeProp

( extension ZipSetPosQueryFilter on QueryBuilder { + QueryBuilder trackIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'track', + )); + }); + } + + QueryBuilder trackIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'track', + )); + }); + } + QueryBuilder zipEqualTo( String value, { bool caseSensitive = true, @@ -1046,7 +1064,9 @@ Map _$ZipReqZipToJson(ZipReqZip instance) => { ZipSetPos _$ZipSetPosFromJson(Map json) => ZipSetPos() ..zip = json['zip'] as String - ..track = ZipTrack.fromJson(json['track'] as Map); + ..track = json['track'] == null + ? null + : ZipTrack.fromJson(json['track'] as Map); Map _$ZipSetPosToJson(ZipSetPos instance) => { 'zip': instance.zip, diff --git a/lib/tools/constants.dart b/lib/tools/constants.dart index 775341a..2f7d228 100644 --- a/lib/tools/constants.dart +++ b/lib/tools/constants.dart @@ -11,7 +11,7 @@ class Constants { static const String messageTopicRankingUpdated = "ranking_updated"; static const String messageTopicJobReceived = "job_received"; static const String messageTopicJobDeleted = "job_deleted"; - static const String messageTopicRefreshBadges = "refresh_badges"; + static const String messageTopicUpdateBadges = "update_badges"; static const String messageTopicMessageReceived = "message_received"; static const String messageTopicPushContentView = "push_content_view"; static const String messageTopicPopContentView = "pop_content_view"; diff --git a/lib/tools/messages/update_badges_message.dart b/lib/tools/messages/update_badges_message.dart new file mode 100644 index 0000000..c6041d7 --- /dev/null +++ b/lib/tools/messages/update_badges_message.dart @@ -0,0 +1,4 @@ + +class UpdateBadgesMessage { + UpdateBadgesMessage(); +} \ No newline at end of file diff --git a/lib/tools/mqtt_core.dart b/lib/tools/mqtt_core.dart index e9d9085..6fb3b1a 100644 --- a/lib/tools/mqtt_core.dart +++ b/lib/tools/mqtt_core.dart @@ -1,19 +1,25 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'dart:math'; +import 'package:archive/archive.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:isar/isar.dart'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_server_client.dart'; -import 'package:stadtbote/model/rank.dart'; +import 'package:stadtbote/model/enqueued_message.dart'; +import 'package:stadtbote/model/log_entry.dart'; +import 'package:stadtbote/model/ranking.dart'; import 'package:stadtbote/model/status.dart'; +import 'package:stadtbote/protocoll_messages/acknowledge_job.dart'; import 'package:stadtbote/protocoll_messages/bootstrap_request.dart'; import 'package:stadtbote/protocoll_messages/bootstrap_response_payload.dart'; +import 'package:stadtbote/protocoll_messages/device_log_request.dart'; +import 'package:stadtbote/protocoll_messages/device_log_response.dart'; +import 'package:stadtbote/protocoll_messages/device_object.dart'; import 'package:stadtbote/model/job.dart'; import 'package:stadtbote/model/message.dart'; -import 'package:stadtbote/protocoll_messages/device_object.dart'; -import 'package:stadtbote/protocoll_messages/job_del_message.dart'; +import 'package:stadtbote/protocoll_messages/payload_container.dart'; import 'package:stadtbote/protocoll_messages/start_session_message.dart'; import 'package:stadtbote/protocoll_messages/sync_jobs_message.dart'; import 'package:stadtbote/protocoll_messages/track_message.dart'; @@ -28,6 +34,7 @@ import 'package:stadtbote/tools/messages/status_info_update.dart'; // ignore: depend_on_referenced_packages import 'package:typed_data/typed_buffers.dart'; import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; import '../main.dart'; import '../model/session_status.dart'; @@ -49,7 +56,6 @@ class MqttCore { late String _brokerUri; late int _brokerPort; late String _fromServerTopic; - late String _clientId; late String _toServerTopic; late MqttServerClient _mqttClient; @@ -94,7 +100,6 @@ class MqttCore { _mqttClient.keepAlivePeriod = 20; _mqttClient.autoReconnect = false; _mqttClient.onDisconnected = _clientDisconnected; - _mqttClient.connectionMessage = MqttConnectMessage() .withClientIdentifier(await Util.getAppUuid()) .withWillQos(MqttQos.atMostOnce) @@ -129,73 +134,96 @@ class MqttCore { } } - publishText(String subtopic, String text) { + publishBinary(String topic, List data) { + var buffer = Uint8Buffer(); + buffer.addAll(data); + + _mqttClient.publishMessage(topic, MqttQos.exactlyOnce, buffer); + + Util.logInfo("binary message send to $topic"); + } + + publishText(String topic, String text) { + var buffer = Uint8Buffer(); + buffer.addAll(utf8.encode(text)); + + _mqttClient.publishMessage(topic, MqttQos.exactlyOnce, buffer); + + Util.logInfo("message\n$text\nsend to $topic"); + } + + publishPayloadMessage(String topic, Map payload) { + var payloadContainer = PayloadContainer(); + payloadContainer.id = const Uuid().v4().toString(); + payloadContainer.recv = topic; + payloadContainer.store = true; + payloadContainer.payload = payload; + + var json = jsonEncode(payloadContainer.toJson()); + try { - var buffer = Uint8Buffer(); - buffer.addAll(utf8.encode(text)); - - _mqttClient.publishMessage(subtopic, MqttQos.exactlyOnce, buffer); - - var topic = "$_toServerTopic$subtopic/$_clientId"; - - _mqttClient.publishMessage( topic, MqttQos.exactlyOnce, buffer); - - Util.logInfo("send message $text to $topic"); + publishText(_toServerTopic, json); } catch (e) { - Util.logException(e.toString()); + _enqueueMessage(_toServerTopic, jsonEncode(payloadContainer.toJson())); } } void _setupSubscriptions() async { Util.logInfo('_setupSubscriptions called'); - _mqttClient.updates?.listen((List> messages) { + _mqttClient.updates?.listen((List> c) { try { - for (var mqttMessage in messages) { - final MqttPublishMessage message = mqttMessage.payload as MqttPublishMessage; - String payload = utf8.decode(MqttPublishPayload.bytesToStringAsString(message.payload.message).codeUnits); - var topic = message.variableHeader?.topicName; + final MqttPublishMessage message = c[0].payload as MqttPublishMessage; + String payload = utf8.decode(MqttPublishPayload.bytesToStringAsString(message.payload.message).codeUnits); + var topic = message.variableHeader?.topicName; - if (topic!.endsWith(_clientId)) { - Util.logInfo('message received'); - Util.logInfo('topic: $topic'); - Util.logInfo('mesage: $payload'); + Util.logInfo('message received\ntopic: $topic\nmessage: $payload'); - var topicArray = topic.split('/'); - var messageType = topicArray[3]; + Map messageData = jsonDecode(payload); - switch(messageType) { - case 'session': - _sessionMessageReceived(jsonDecode(payload)); - break; + for (var entry in messageData.keys) { + if (entry == 'payload') { + var payload = messageData[entry]; + for (var payloadEntry in payload.keys) { + var payloadData = payload[payloadEntry]; - case 'status': - _statusMessageReceived(jsonDecode(payload)); - break; + switch(payloadEntry) { + case 'session': + _sessionMessageReceived(payloadData); + break; - case 'ranking': - _rankingMessageReceived(jsonDecode(payload)); - break; + case 'status': + _statusMessageReceived(payloadData); + break; - case 'job': - _jobMessageReceived(jsonDecode(payload)); - break; + case 'ranks': + _rankingMessageReceived(payloadData); + break; - case 'job_del': - _jobDeleteReceived(jsonDecode(payload)); - break; + case 'jobs': + _jobMessageReceived(payloadData); + break; - case 'sync_request': - _syncRequestReceived(jsonDecode(payload)); - break; + case 'job_del': + _jobDeleteReceived(payloadData); + break; - case "mssg": - _messageMessageReceived(payload); - break; + case 'sync_request': + _syncRequestReceived(payloadData); + break; - default: - Util.logInfo('Unknown topic: $messageType'); - break; + case "mssg": + _messageMessageReceived(payloadData); + break; + + case "upload_device_log": + _messageUploadDeviceLogReceived(payloadData); + break; + + default: + Util.logInfo('Unknown topic: $payloadEntry'); + break; + } } } } @@ -204,9 +232,8 @@ class MqttCore { } }); - var topic = "$_fromServerTopic#"; - _mqttClient.subscribe(topic, MqttQos.exactlyOnce); - Util.logInfo('subscribed to topic: $topic'); + _mqttClient.subscribe(_fromServerTopic, MqttQos.exactlyOnce); + Util.logInfo('subscribed to topic: $_fromServerTopic'); MessageService().publish(Constants.messageTopicConnectionStatusChanged, arguments: ConnectionStatus(status: ConnectionStatusEnum.connected)); } @@ -216,7 +243,9 @@ class MqttCore { await _sendSyncMessage(); - var sessionStatusMessage = SessionStatus.fromJson(payloadData); + await _sendEnqueuedMessages(); + + var sessionStatusMessage = SessionStatus.fromJson(payloadData['status']); await MyApp.isarInstance.writeTxn(() async { await MyApp.isarInstance.sessionStatus.clear(); @@ -240,42 +269,52 @@ class MqttCore { } void _rankingMessageReceived(List payloadData) async { - for (var entry in payloadData) { - var rank = Rank.fromJson(entry); - - await MyApp.isarInstance.writeTxn(() async { - await MyApp.isarInstance.ranks.put(rank); - }); - } + Map rankingMap = {}; + rankingMap["ranks"] = payloadData; + var ranking = Ranking.fromJson(rankingMap); + await MyApp.isarInstance.writeTxn(() async { + await MyApp.isarInstance.rankings.put(ranking); + }); MessageService().publish(Constants.messageTopicRankingUpdated, arguments: RankingUpdated()); } void _jobMessageReceived(payloadData) async { - Job newJob = Job.fromJson(payloadData); + for (var foundJobData in payloadData) { + Job newJob = Job.fromJson(foundJobData); - MyApp.isarInstance.writeTxnSync(() async { - MyApp.isarInstance.jobs.putSync(newJob); - }); + await MyApp.isarInstance.writeTxn(() async { + await MyApp.isarInstance.jobs.put(newJob); + }); - if (newJob.state == Constants.stateAssigned) { - if (newJob.global) { - await Util.playMp3('aaa.mp3'); - } else { - await Util.playMp3('job_received.mp3'); + if (newJob.state == Constants.stateAssigned) { + if (newJob.global) { + await Util.playMp3('aaa.mp3'); + } else { + await Util.playMp3('job_received.mp3'); + } } - } - MessageService().publish(Constants.messageTopicJobReceived, arguments: JobReceivedMessage(job: newJob)); + try { + MessageService().publish(Constants.messageTopicJobReceived, arguments: JobReceivedMessage(job: newJob)); + } catch (ex) { + Util.logException("publish messageTopicJobReceived failed!"); + } + + _acknowlegdeJob(newJob); + } } - void _jobDeleteReceived(payloadData) async { - var jobDelMessage = JobDelMessage.fromJson(payloadData); + void _jobDeleteReceived(List payloadData) async { + Map map = {}; + map["jobDel"] = payloadData; + var jobDelMessage = JobDelMessage.fromJson(map); - await MyApp.isarInstance.writeTxn(() async { - await MyApp.isarInstance.jobs.delete(jobDelMessage.id); - }); + for(var jobId in jobDelMessage.jobDel) { + await MyApp.isarInstance.writeTxn(() async { + await MyApp.isarInstance.jobs.delete(jobId); + }); MessageService().publish(Constants.messageTopicJobDeleted, arguments: JobDeletedMessage(id: jobDelMessage.id)); } @@ -334,19 +373,22 @@ class MqttCore { void _processBootstrap() async { try { - String? responseText = ""; + var deviceInfoPlugin = DeviceInfoPlugin(); + + AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; + BaseDeviceInfo deviceInfo = await deviceInfoPlugin.deviceInfo; var deviceObject = DeviceObject(); - deviceObject.manufacturer = "sven"; - deviceObject.model = "emulator"; - deviceObject.os = "hat es"; - deviceObject.osVer = "0.0"; + deviceObject.manufacturer = deviceInfo.data['manufacturer']; + deviceObject.model = deviceInfo.data['model']; + deviceObject.os = androidInfo.version.release; + deviceObject.osVer = androidInfo.version.sdkInt.toString(); var handshakeMessage = BootstrapRequest(); handshakeMessage.app = "sb"; handshakeMessage.mqttClientId = await Util.getAppUuid(); handshakeMessage.deviceObject = deviceObject; - handshakeMessage.ver = 200; + handshakeMessage.ver = await Util.getAppVersion(); //var jsonMap = handshakeMessage.toJson(); //var json = jsonEncode(jsonMap); @@ -405,12 +447,12 @@ class MqttCore { if (answer != null) { var trackMessage = TrackMessage(); trackMessage.pos = TrackPosition(); - trackMessage.pos.time = answer.timestamp.toIso8601String(); - trackMessage.pos.lat = answer.latitude; - trackMessage.pos.long = answer.longitude; - trackMessage.pos.acc = answer.accuracy.toInt(); + trackMessage.pos!.time = position.timestamp.toIso8601String(); + trackMessage.pos!.lat = position.latitude; + trackMessage.pos!.long = position.longitude; + trackMessage.pos!.acc = position.accuracy.toInt(); - publishText("track", jsonEncode(answer.toJson())); + publishPayloadMessage("track", trackMessage.toJson()); } }); } @@ -421,4 +463,42 @@ class MqttCore { _trackTimer = null; } } + + void _enqueueMessage(String receiver, String payload) async { + var enqueuedMessage = EnqueuedMessage(); + enqueuedMessage.topic = receiver; + enqueuedMessage.payload = payload; + + await MyApp.isarInstance.writeTxn(() async { + await MyApp.isarInstance.enqueuedMessages.put(enqueuedMessage); + }); + } + + _sendEnqueuedMessages() async { + var enqueuedMessages = await MyApp.isarInstance.enqueuedMessages.where().findAll(); + if (enqueuedMessages.isNotEmpty) { + for (var enqueuedMessage in enqueuedMessages) { + try { + publishText(enqueuedMessage.topic, enqueuedMessage.payload); + + await MyApp.isarInstance.writeTxn(() async { + await MyApp.isarInstance.enqueuedMessages.delete(enqueuedMessage.id); + }); + } catch (ex) { + Util.logInfo("resend enqueued message failed: $ex"); + } + } + } + } + + void _acknowlegdeJob(Job job) { + var message = AcknowledgeJob(); + message.received = AcknowledgeJobReceived(); + message.received.id = job.id; + message.received.state = job.state; + message.received.ver = job.ver; + message.received.utc = DateTime.now().millisecondsSinceEpoch; + + MqttCore.instance.publishPayloadMessage('job', message.toJson()); + } } diff --git a/lib/tools/util.dart b/lib/tools/util.dart index f4f1a3b..20b3652 100644 --- a/lib/tools/util.dart +++ b/lib/tools/util.dart @@ -1,12 +1,14 @@ -import 'dart:developer'; import 'dart:io'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:package_info/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stadtbote/main.dart'; import 'package:stadtbote/model/job.dart'; +import 'package:stadtbote/model/log_entry.dart'; import 'package:stadtbote/tools/constants.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; @@ -73,12 +75,44 @@ class Util { return currentUuid; } + static Future getAppVersion() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + return int.parse(packageInfo.version.replaceAll(".0.0", "")); + } + + static _printLongText(String text) { + final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk + pattern.allMatches(text).forEach((match) => debugPrint(match.group(0))); + } + + static void _persistLogMessage(String text, LogEntrySeverityEnum severity) async { + var logEntry = LogEntry(); + logEntry.time = DateTime.now(); + logEntry.text = text; + logEntry.severity = severity; + + await MyApp.isarInstance.writeTxn(() async { + await MyApp.isarInstance.logEntrys.put(logEntry); + }); + } + static logInfo(String message) { - log(message); + _printLongText(message); + + _persistLogMessage(message, LogEntrySeverityEnum.info); + } + + static logDebug(String message) { + _printLongText(message); + + _persistLogMessage(message, LogEntrySeverityEnum.debug); } static logException(String message) { - log('Exception: $message'); + _printLongText(message); + + _persistLogMessage(message, LogEntrySeverityEnum.error); } static int fastHash(String string) { @@ -197,7 +231,11 @@ Widget getDefaultButton(String text, Function() onPressed, {Image? image, double Radius.circular(15), ), ), - onPressed: onPressed, + onPressed: () { + Util.logInfo("Button \"$text\" clicked"); + + onPressed(); + }, child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [