Erweiterungen
This commit is contained in:
1 parent
ba1f7d5440
commit
bb3dc9e856
28 files changed
+1672
-1291
No files matched your search
@@ -8,6 +8,7 @@ import 'package:stadtbote/messages/pop_content_view_message.dart';
|
||||
import '../main.dart';
|
||||
import '../model/job.dart';
|
||||
import '../objectbox.g.dart';
|
||||
import '../popups/files_popup.dart';
|
||||
import '../tools/constants.dart';
|
||||
import '../tools/util.dart';
|
||||
|
||||
@@ -92,38 +93,48 @@ class _AcceptedJobsPageState extends State<AcceptedJobsPage>
|
||||
border: Border.all(color: Colors.grey, width: 0.5),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(13))),
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('dd.MM.yyyy HH:mm')
|
||||
.format(DateTime.fromMillisecondsSinceEpoch(job.ordertime * 1000, isUtc: true).toLocal())
|
||||
.toString(),
|
||||
style: TextStyle(
|
||||
backgroundColor: isJobTooFarInFuture(job)
|
||||
? Colors.red
|
||||
: Colors.transparent),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
job.crFilter != null ? job.crFilter.toString() : "",
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/${mapVehicleTypeToImageName(job.vhtId)}",
|
||||
width: 60,
|
||||
height: 60),
|
||||
const SizedBox(width: 10),
|
||||
Text(mapVehicleTypeToText(job.vhtId)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('dd.MM.yyyy HH:mm')
|
||||
.format(DateTime.fromMillisecondsSinceEpoch(job.ordertime * 1000, isUtc: true).toLocal())
|
||||
.toString(),
|
||||
style: TextStyle(
|
||||
backgroundColor: isJobTooFarInFuture(job)
|
||||
? Colors.red
|
||||
: Colors.transparent),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
job.crFilter != null ? job.crFilter.toString() : "",
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/${mapVehicleTypeToImageName(job.vhtId)}",
|
||||
width: 60,
|
||||
height: 60),
|
||||
const SizedBox(width: 10),
|
||||
Text(mapVehicleTypeToText(job.vhtId)),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
bottomText,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
bottomText,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
if (job.hasFiles)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: FilesBadge(files: job.files),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:stadtbote/tools/messages/remove_state_0_job_from_map.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../model/job.dart';
|
||||
import '../popups/files_popup.dart';
|
||||
import '../tools/constants.dart';
|
||||
import '../tools/util.dart';
|
||||
|
||||
@@ -38,6 +39,7 @@ class _AssignedJobsPageState extends State<AssignedJobsPage> with ProducerMixin,
|
||||
late bool _initCompleted = false;
|
||||
|
||||
List<Job> _jobs = [];
|
||||
final List<ExpandableController> _expandableControllers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -59,6 +61,11 @@ class _AssignedJobsPageState extends State<AssignedJobsPage> with ProducerMixin,
|
||||
// Unsubscribe from all message topics
|
||||
unsubscribe(queueId: Constants.messageTopicJobReceived);
|
||||
|
||||
// Dispose all expandable controllers
|
||||
for (var controller in _expandableControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -92,9 +99,16 @@ class _AssignedJobsPageState extends State<AssignedJobsPage> with ProducerMixin,
|
||||
List<Widget> _createWidgets() {
|
||||
List<Widget> widgets = [];
|
||||
|
||||
// Clear previous controllers before creating new ones
|
||||
for (var controller in _expandableControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
_expandableControllers.clear();
|
||||
|
||||
for (var job in _jobs) {
|
||||
var expandableController = ExpandableController();
|
||||
expandableController.expanded = true;
|
||||
_expandableControllers.add(expandableController);
|
||||
|
||||
widgets.add(ExpandableNotifier(
|
||||
child: ListTile(
|
||||
@@ -126,27 +140,29 @@ class _AssignedJobsPageState extends State<AssignedJobsPage> with ProducerMixin,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
child: Stack(
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('dd.MM.yyyy HH:mm').format(DateTime.fromMillisecondsSinceEpoch(job.ordertime * 1000, isUtc: true).toLocal()).toString(),
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(backgroundColor: isJobTooFarInFuture(job) ? Colors.red : Colors.white),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
Column(
|
||||
children: [
|
||||
Image.asset("assets/${mapVehicleTypeToImageName(job.vhtId)}", width: 60, height: 60),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
mapVehicleTypeToText(job.vhtId)
|
||||
DateFormat('dd.MM.yyyy HH:mm').format(DateTime.fromMillisecondsSinceEpoch(job.ordertime * 1000, isUtc: true).toLocal()).toString(),
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(backgroundColor: isJobTooFarInFuture(job) ? Colors.red : Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text('Auftragsnummer: ${job.externalId}'),
|
||||
Text(job.tours[0].comp),
|
||||
Text(job.tours[0].street),
|
||||
Text(job.tours[0].city),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset("assets/${mapVehicleTypeToImageName(job.vhtId)}", width: 60, height: 60),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
mapVehicleTypeToText(job.vhtId)
|
||||
),
|
||||
],
|
||||
),
|
||||
Text('Auftragsnummer: ${job.externalId}'),
|
||||
Text(job.tours[0].comp),
|
||||
Text(job.tours[0].street),
|
||||
Text(job.tours[0].city),
|
||||
const SizedBox(height: 20),
|
||||
ScrollOnExpand(
|
||||
scrollOnExpand: true,
|
||||
@@ -217,11 +233,19 @@ class _AssignedJobsPageState extends State<AssignedJobsPage> with ProducerMixin,
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
if (job.hasFiles)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: FilesBadge(files: job.files),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return widgets;
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:stadtbote/messages/pop_content_view_message.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../model/job.dart';
|
||||
import '../popups/files_popup.dart';
|
||||
import '../tools/constants.dart';
|
||||
import '../tools/util.dart';
|
||||
|
||||
@@ -85,34 +86,44 @@ class _FinishedJobsPageState extends State<FinishedJobsPage>
|
||||
border: Border.all(color: Colors.grey, width: 0.5),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(13))),
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('dd.MM.yyyy HH:mm')
|
||||
.format(DateTime.fromMillisecondsSinceEpoch(job.ordertime * 1000, isUtc: true).toLocal())
|
||||
.toString(),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
job.crFilter != null ? job.crFilter.toString() : "",
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/${mapVehicleTypeToImageName(job.vhtId)}",
|
||||
width: 60,
|
||||
height: 60),
|
||||
const SizedBox(width: 10),
|
||||
Text(mapVehicleTypeToText(job.vhtId)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('dd.MM.yyyy HH:mm')
|
||||
.format(DateTime.fromMillisecondsSinceEpoch(job.ordertime * 1000, isUtc: true).toLocal())
|
||||
.toString(),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
job.crFilter != null ? job.crFilter.toString() : "",
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/${mapVehicleTypeToImageName(job.vhtId)}",
|
||||
width: 60,
|
||||
height: 60),
|
||||
const SizedBox(width: 10),
|
||||
Text(mapVehicleTypeToText(job.vhtId)),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
bottomText,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
bottomText,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
if (job.hasFiles)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: FilesBadge(files: job.files),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
|
||||
@@ -102,6 +102,12 @@ class _SetDesiredZipPageState extends State<SetDesiredZipPage>
|
||||
acknowledgeRequired: true),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textEditController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderWidget extends StatefulWidget {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:stadtbote/messages/pop_content_view_message.dart';
|
||||
import 'package:stadtbote/model/job_tour.dart';
|
||||
|
||||
import '../model/job.dart';
|
||||
import '../popups/files_popup.dart';
|
||||
import '../popups/map_popup.dart';
|
||||
import '../tools/constants.dart';
|
||||
import '../tools/util.dart';
|
||||
@@ -103,6 +104,22 @@ class _TourDetailsPageState extends State<TourDetailsPage> with ProducerMixin {
|
||||
"Komm-#: ${widget.tour.com}",
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
if (widget.tour.files != null && widget.tour.files!.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
FilesBadge(files: widget.tour.files),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Dateien verfügbar',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
getDefaultButton(
|
||||
'Karte',
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:stadtbote/model/job_tour.dart';
|
||||
import 'package:stadtbote/popups/map_popup.dart';
|
||||
|
||||
import '../model/job.dart';
|
||||
import '../popups/files_popup.dart';
|
||||
import '../tools/constants.dart';
|
||||
import '../tools/util.dart';
|
||||
|
||||
@@ -152,9 +153,16 @@ class _TourListPageState extends State<TourListPage> with ProducerMixin {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${"Station ${tour.sort}"} ${mapTourMode(tour.mode)}",
|
||||
textAlign: TextAlign.left,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${"Station ${tour.sort}"} ${mapTourMode(tour.mode)}",
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
FilesBadge(files: tour.files),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
tour.comp,
|
||||
|
||||
+33
-1
@@ -188,7 +188,7 @@ class Main extends StatefulWidget {
|
||||
State<Main> createState() => _MainState();
|
||||
}
|
||||
|
||||
class _MainState extends State<Main> with SingleTickerProviderStateMixin, ProducerMixin, ConsumerMixin {
|
||||
class _MainState extends State<Main> with SingleTickerProviderStateMixin, ProducerMixin, ConsumerMixin, WidgetsBindingObserver {
|
||||
DateTime? _lastBackPressed;
|
||||
late ContentViewWidget _currentContent;
|
||||
late WidgetStack<ContentViewWidget> contentStack = WidgetStack();
|
||||
@@ -210,7 +210,11 @@ class _MainState extends State<Main> with SingleTickerProviderStateMixin, Produc
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Unregister from app lifecycle events
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
|
||||
_animationController?.dispose();
|
||||
_checkForNewState0JobsJobsTimer?.cancel();
|
||||
|
||||
// Unsubscribe from all message topics
|
||||
unsubscribe(queueId: Constants.messageTopicPushContentView);
|
||||
@@ -228,6 +232,9 @@ class _MainState extends State<Main> with SingleTickerProviderStateMixin, Produc
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Register for app lifecycle events
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000));
|
||||
_animationController!.repeat(reverse: true);
|
||||
@@ -253,6 +260,31 @@ class _MainState extends State<Main> with SingleTickerProviderStateMixin, Produc
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
|
||||
switch (state) {
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.hidden:
|
||||
// App geht in den Hintergrund - Animation und Timer pausieren
|
||||
_animationController?.stop();
|
||||
_checkForNewState0JobsJobsTimer?.cancel();
|
||||
Util.logInfo('App in Hintergrund - Rendering pausiert');
|
||||
break;
|
||||
case AppLifecycleState.resumed:
|
||||
// App kommt zurück in den Vordergrund - Animation und Timer fortsetzen
|
||||
_animationController?.repeat(reverse: true);
|
||||
_startCheckForNewState0JobsJobsTimer();
|
||||
Util.logInfo('App im Vordergrund - Rendering fortgesetzt');
|
||||
break;
|
||||
case AppLifecycleState.detached:
|
||||
// App wird beendet
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_initCompleted) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:objectbox/objectbox.dart';
|
||||
import 'package:stadtbote/model/job_tour.dart';
|
||||
import 'package:stadtbote/model/tour_file.dart';
|
||||
|
||||
@Entity()
|
||||
class Job {
|
||||
@@ -34,6 +35,25 @@ class Job {
|
||||
toursJson = jsonEncode(value.map((e) => e.toJson()).toList());
|
||||
}
|
||||
|
||||
// Store files as JSON string for ObjectBox
|
||||
String filesJson = '';
|
||||
|
||||
@Transient()
|
||||
List<TourFile> get files {
|
||||
if (filesJson.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
final List<dynamic> decoded = jsonDecode(filesJson);
|
||||
return decoded.map((e) => TourFile.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
set files(List<TourFile> value) {
|
||||
filesJson = jsonEncode(value.map((e) => e.toJson()).toList());
|
||||
}
|
||||
|
||||
// Helper method to check if job has files
|
||||
bool get hasFiles => files.isNotEmpty;
|
||||
|
||||
Job();
|
||||
|
||||
factory Job.fromJson(Map<String, dynamic> json) {
|
||||
@@ -52,6 +72,11 @@ class Job {
|
||||
job.tours = (json['tours'] as List<dynamic>)
|
||||
.map((e) => JobTour.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
if (json['files'] != null) {
|
||||
job.files = (json['files'] as List<dynamic>)
|
||||
.map((e) => TourFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
return job;
|
||||
}
|
||||
|
||||
@@ -68,6 +93,7 @@ class Job {
|
||||
'rem': rem,
|
||||
'delayOrderTime': delayOrderTime,
|
||||
'tours': tours.map((e) => e.toJson()).toList(),
|
||||
'files': files.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:stadtbote/model/tour_file.dart';
|
||||
import 'package:stadtbote/model/tour_position.dart';
|
||||
import 'package:stadtbote/model/tour_task.dart';
|
||||
|
||||
@@ -15,6 +16,7 @@ class JobTour {
|
||||
TourPosition? pos;
|
||||
late String rem;
|
||||
List<TourTask>? tasks;
|
||||
List<TourFile>? files;
|
||||
|
||||
JobTour();
|
||||
|
||||
@@ -39,6 +41,11 @@ class JobTour {
|
||||
.map((e) => TourTask.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null;
|
||||
tour.files = json['files'] != null
|
||||
? (json['files'] as List<dynamic>)
|
||||
.map((e) => TourFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null;
|
||||
return tour;
|
||||
}
|
||||
|
||||
@@ -57,6 +64,7 @@ class JobTour {
|
||||
'pos': pos?.toJson(),
|
||||
'rem': rem,
|
||||
'tasks': tasks?.map((e) => e.toJson()).toList(),
|
||||
'files': files?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
class TourFile {
|
||||
late int index;
|
||||
late String name;
|
||||
String? remark;
|
||||
late String url;
|
||||
|
||||
TourFile();
|
||||
|
||||
factory TourFile.fromJson(Map<String, dynamic> json) {
|
||||
final file = TourFile();
|
||||
file.index = json['index'] as int;
|
||||
file.name = json['name'] as String;
|
||||
file.remark = json['remark'] as String?;
|
||||
file.url = json['url'] as String;
|
||||
return file;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'index': index,
|
||||
'name': name,
|
||||
'remark': remark,
|
||||
'url': url,
|
||||
};
|
||||
}
|
||||
|
||||
bool get isPdf {
|
||||
return name.toLowerCase().endsWith('.pdf');
|
||||
}
|
||||
|
||||
bool get isImage {
|
||||
final ext = name.toLowerCase();
|
||||
return ext.endsWith('.jpg') ||
|
||||
ext.endsWith('.jpeg') ||
|
||||
ext.endsWith('.png') ||
|
||||
ext.endsWith('.gif') ||
|
||||
ext.endsWith('.webp');
|
||||
}
|
||||
}
|
||||
+72
-51
@@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"id": "3:808686831791135454",
|
||||
"lastPropertyId": "12:5320049580625017283",
|
||||
"lastPropertyId": "13:1803006975816940878",
|
||||
"name": "Job",
|
||||
"properties": [
|
||||
{
|
||||
@@ -99,6 +99,11 @@
|
||||
"type": 6,
|
||||
"flags": 40,
|
||||
"indexId": "1:2108836818156887439"
|
||||
},
|
||||
{
|
||||
"id": "13:1803006975816940878",
|
||||
"name": "filesJson",
|
||||
"type": 9
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
@@ -302,52 +307,6 @@
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "11:8606238749478059367",
|
||||
"lastPropertyId": "7:4056540326670791484",
|
||||
"name": "ChatMessage",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:3236243497308902958",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:1689142031630531044",
|
||||
"name": "time",
|
||||
"type": 6
|
||||
},
|
||||
{
|
||||
"id": "3:9168654785572108691",
|
||||
"name": "from",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "4:3167039386322657979",
|
||||
"name": "subj",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "5:371212500165499194",
|
||||
"name": "text",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "6:7798301556204918939",
|
||||
"name": "read",
|
||||
"type": 1
|
||||
},
|
||||
{
|
||||
"id": "7:4056540326670791484",
|
||||
"name": "externalId",
|
||||
"type": 6,
|
||||
"flags": 40,
|
||||
"indexId": "2:428171949166894655"
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "12:4865606913057136676",
|
||||
"lastPropertyId": "5:4858121848992922038",
|
||||
@@ -381,17 +340,65 @@
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "14:3621615714595976689",
|
||||
"lastPropertyId": "7:5084802055406173119",
|
||||
"name": "ChatMessage",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:8160599835026826926",
|
||||
"name": "id",
|
||||
"type": 6,
|
||||
"flags": 1
|
||||
},
|
||||
{
|
||||
"id": "2:5736027494386837185",
|
||||
"name": "externalId",
|
||||
"type": 6,
|
||||
"flags": 40,
|
||||
"indexId": "4:8487631368009819350"
|
||||
},
|
||||
{
|
||||
"id": "3:4672344908186587803",
|
||||
"name": "time",
|
||||
"type": 6
|
||||
},
|
||||
{
|
||||
"id": "4:2026513613729432961",
|
||||
"name": "from",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "5:6544369704442218939",
|
||||
"name": "subj",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "6:8234519373253706290",
|
||||
"name": "text",
|
||||
"type": 9
|
||||
},
|
||||
{
|
||||
"id": "7:5084802055406173119",
|
||||
"name": "read",
|
||||
"type": 1
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "12:4865606913057136676",
|
||||
"lastIndexId": "2:428171949166894655",
|
||||
"lastEntityId": "14:3621615714595976689",
|
||||
"lastIndexId": "4:8487631368009819350",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 5,
|
||||
"modelVersionParserMinimum": 5,
|
||||
"retiredEntityUids": [
|
||||
3121960281516643928,
|
||||
3330049220205502332
|
||||
3330049220205502332,
|
||||
8606238749478059367,
|
||||
9126058286032860192
|
||||
],
|
||||
"retiredIndexUids": [],
|
||||
"retiredPropertyUids": [
|
||||
@@ -405,7 +412,21 @@
|
||||
3530187464938423960,
|
||||
2939471599149775609,
|
||||
4870701960967093484,
|
||||
9072329172508444422
|
||||
9072329172508444422,
|
||||
3236243497308902958,
|
||||
1689142031630531044,
|
||||
9168654785572108691,
|
||||
3167039386322657979,
|
||||
371212500165499194,
|
||||
7798301556204918939,
|
||||
4056540326670791484,
|
||||
2028590175083469347,
|
||||
7733678916130816919,
|
||||
5111729991837061065,
|
||||
2981635918450905844,
|
||||
4551221086536914442,
|
||||
1176594062059570557,
|
||||
4272081635478028034
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
|
||||
+933
-1097
File diff suppressed because it is too large.
Load diff
@@ -133,6 +133,7 @@ Future<void> showAcceptJobsPopup(SharedMapReference mapReference, Function() onF
|
||||
// Ensure we always clean up subscriptions even if the dialog is dismissed via barrier tap or back button
|
||||
dialogFuture.whenComplete(() {
|
||||
isActive = false;
|
||||
_scrollController.dispose();
|
||||
if (builderCreated) {
|
||||
try {
|
||||
extendedStatefulBuilder.unsubscribe(queueId: Constants.messageTopicJobReceived);
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stadtbote/content_views/pdf_viewer_page.dart';
|
||||
import 'package:stadtbote/model/tour_file.dart';
|
||||
import 'package:stadtbote/tools/constants.dart';
|
||||
import 'package:stadtbote/tools/util.dart' show showAlertDialog;
|
||||
import 'package:stadtbote/tools/util.dart' as util;
|
||||
|
||||
void showFilesPopup(BuildContext context, List<TourFile> files) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Dateien'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: 400,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: files.length,
|
||||
itemBuilder: (context, index) {
|
||||
final file = files[index];
|
||||
return _FileListItem(file: file);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Schließen'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _FileListItem extends StatefulWidget {
|
||||
final TourFile file;
|
||||
|
||||
const _FileListItem({required this.file});
|
||||
|
||||
@override
|
||||
State<_FileListItem> createState() => _FileListItemState();
|
||||
}
|
||||
|
||||
class _FileListItemState extends State<_FileListItem> {
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
IconData iconData;
|
||||
if (widget.file.isPdf) {
|
||||
iconData = Icons.picture_as_pdf;
|
||||
} else if (widget.file.isImage) {
|
||||
iconData = Icons.image;
|
||||
} else {
|
||||
iconData = Icons.insert_drive_file;
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
leading: Icon(iconData, color: Constants.colorPrimary),
|
||||
title: Text(widget.file.name),
|
||||
subtitle: widget.file.remark != null && widget.file.remark!.isNotEmpty
|
||||
? Text(widget.file.remark!)
|
||||
: null,
|
||||
trailing: _isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.open_in_new),
|
||||
onTap: () => _openFile(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openFile(BuildContext context) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
if (widget.file.isPdf) {
|
||||
Navigator.of(context).pop(); // Close the files dialog
|
||||
|
||||
// Navigate to PDF viewer
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PdfViewerPage(
|
||||
pdfUrl: widget.file.url,
|
||||
title: widget.file.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (widget.file.isImage) {
|
||||
Navigator.of(context).pop(); // Close the files dialog
|
||||
|
||||
// Show image in a dialog
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AppBar(
|
||||
title: Text(widget.file.name),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
Flexible(
|
||||
child: Image.network(
|
||||
widget.file.url,
|
||||
fit: BoxFit.contain,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Fehler beim Laden des Bildes:\n$error',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// For other file types, try to open with external app
|
||||
showAlertDialog(
|
||||
'Information',
|
||||
'Dateityp wird nicht direkt unterstützt. Die Datei wird im Browser geöffnet.',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
util.Util.logException('Error opening file: $e');
|
||||
if (context.mounted) {
|
||||
showAlertDialog(
|
||||
'Fehler',
|
||||
'Fehler beim Öffnen der Datei: $e',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget to display a file icon button if files are available
|
||||
class FilesIconButton extends StatelessWidget {
|
||||
final List<TourFile>? files;
|
||||
final double iconSize;
|
||||
|
||||
const FilesIconButton({
|
||||
super.key,
|
||||
required this.files,
|
||||
this.iconSize = 24,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (files == null || files!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.attach_file),
|
||||
iconSize: iconSize,
|
||||
color: Constants.colorPrimary,
|
||||
tooltip: '${files!.length} Datei(en)',
|
||||
onPressed: () => showFilesPopup(context, files!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Badge widget showing file count with icon
|
||||
class FilesBadge extends StatelessWidget {
|
||||
final List<TourFile>? files;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const FilesBadge({
|
||||
super.key,
|
||||
required this.files,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (files == null || files!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap ?? () => showFilesPopup(context, files!),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Constants.colorPrimary.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Constants.colorPrimary, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.attach_file,
|
||||
size: 16,
|
||||
color: Constants.colorPrimary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${files!.length}',
|
||||
style: TextStyle(
|
||||
color: Constants.colorPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import '../model/job_tour.dart';
|
||||
List<Color> _colors = [Colors.red, Colors.green];
|
||||
|
||||
showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList) async {
|
||||
MapController? mapController;
|
||||
Map<int, StreamController<bool>> streamControllers = {};
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
@@ -19,24 +22,24 @@ showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList)
|
||||
shadowColor: Colors.transparent,
|
||||
backgroundColor: Colors.white,
|
||||
content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
var mapController = MapController();
|
||||
Map<int, StreamController<bool>> streamController = {};
|
||||
mapController = MapController();
|
||||
streamControllers = {};
|
||||
|
||||
List<Widget> tourWidgets = [];
|
||||
for (JobTour tour in tourList) {
|
||||
var currentStreamController = StreamController<bool>();
|
||||
streamController[tour.sort] = currentStreamController;
|
||||
streamControllers[tour.sort] = currentStreamController;
|
||||
|
||||
tourWidgets.add(
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (tour.pos != null) {
|
||||
mapController.move(LatLng(tour.pos!.lat, tour.pos!.long), mapController.camera.zoom);
|
||||
mapController!.move(LatLng(tour.pos!.lat, tour.pos!.long), mapController!.camera.zoom);
|
||||
|
||||
streamController[currentTour.sort]!.sink.add(false);
|
||||
streamControllers[currentTour.sort]!.sink.add(false);
|
||||
|
||||
currentTour = tour;
|
||||
streamController[tour.sort]!.sink.add(true);
|
||||
streamControllers[tour.sort]!.sink.add(true);
|
||||
} else {
|
||||
showAlertDialog('Information', 'Es liegt keine Position für diese Station vor');
|
||||
}
|
||||
@@ -158,7 +161,7 @@ showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList)
|
||||
height: 25,
|
||||
onChanged: (b) {
|
||||
valueNotifier.value = b;
|
||||
mapController.move(mapController.camera.center, b ? 14.0 : 16.0);
|
||||
mapController!.move(mapController!.camera.center, b ? 14.0 : 16.0);
|
||||
},
|
||||
styleBuilder: (i) => ToggleStyle(indicatorColor: i == true ? _colors[0] : _colors[1]),
|
||||
textBuilder: (value) => value ? const Center(child: Text('Fern')) : const Center(child: Text('Nah')),
|
||||
@@ -173,7 +176,7 @@ showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList)
|
||||
child: AbsorbPointer(
|
||||
absorbing: true,
|
||||
child: FlutterMap(
|
||||
mapController: mapController,
|
||||
mapController: mapController!,
|
||||
options: MapOptions(
|
||||
initialCenter: LatLng(currentTour.pos!.lat, currentTour.pos!.long),
|
||||
initialZoom: 14,
|
||||
@@ -196,4 +199,10 @@ showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList)
|
||||
));
|
||||
}));
|
||||
});
|
||||
|
||||
// Dispose all controllers after dialog is closed
|
||||
mapController?.dispose();
|
||||
for (var controller in streamControllers.values) {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,12 @@ showComissionNumberPopup(BuildContext context, TaskInfo taskInfo, Function(Strin
|
||||
controller: textEditingController,
|
||||
autofocus: true),
|
||||
const SizedBox(height: 20),
|
||||
getDefaultButton('Übernehmen', () => {Navigator.of(context).pop(), onFinished(textEditingController.text)})
|
||||
getDefaultButton('Übernehmen', () {
|
||||
final result = textEditingController.text;
|
||||
textEditingController.dispose();
|
||||
Navigator.of(context).pop();
|
||||
onFinished(result);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -24,6 +24,8 @@ showPickupDeliveryStatusPopup(BuildContext context, TaskInfo taskInfo, Function(
|
||||
if (!didPop) {
|
||||
final shouldPop = await taskDialogSecurityException(context);
|
||||
if (shouldPop && context.mounted) {
|
||||
controller.dispose();
|
||||
remController.dispose();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
@@ -73,6 +75,9 @@ showPickupDeliveryStatusPopup(BuildContext context, TaskInfo taskInfo, Function(
|
||||
if (remController.text.isNotEmpty) {
|
||||
Navigator.of(context).pop(),
|
||||
|
||||
controller.dispose(),
|
||||
remController.dispose(),
|
||||
|
||||
onFinished(selectedTourEntry, remController.text)
|
||||
} else {
|
||||
showAlertDialog("Fehler", "Bitte geben Sie eine Bemerkung ein")
|
||||
@@ -80,6 +85,9 @@ showPickupDeliveryStatusPopup(BuildContext context, TaskInfo taskInfo, Function(
|
||||
} else {
|
||||
Navigator.of(context).pop(),
|
||||
|
||||
controller.dispose(),
|
||||
remController.dispose(),
|
||||
|
||||
onFinished(selectedTourEntry, null)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,7 +77,11 @@ showReceiptGiverPopup(BuildContext context, TaskInfo taskInfo, Function(String,
|
||||
if (textEditingController.text.isNotEmpty && selectedTourEntry != null) {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
onFinished(textEditingController.text, selectedTourEntry!);
|
||||
final result = textEditingController.text;
|
||||
textEditingController.dispose();
|
||||
controller.dispose();
|
||||
|
||||
onFinished(result, selectedTourEntry!);
|
||||
} else {
|
||||
showAlertDialog("Fehler", "Sie müssen einen Text eingeben und ein Geschlecht auswählen");
|
||||
}
|
||||
|
||||
@@ -54,10 +54,12 @@ showStationNoticePopup(BuildContext context, TaskInfo taskInfo, Function(String)
|
||||
autofocus: true),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
getDefaultButton('Übernehmen', buttonHeight: 50, () => {
|
||||
Navigator.of(context).pop(),
|
||||
|
||||
onFinished(textEditingController.text)})
|
||||
getDefaultButton('Übernehmen', buttonHeight: 50, () {
|
||||
final result = textEditingController.text;
|
||||
textEditingController.dispose();
|
||||
Navigator.of(context).pop();
|
||||
onFinished(result);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -67,15 +67,16 @@ showZipPopup(BuildContext context, String text, String buttonText,
|
||||
const SizedBox(height: 20),
|
||||
getDefaultButton(
|
||||
buttonText,
|
||||
() => {
|
||||
if (_checkInput(context,
|
||||
textEditingController.text))
|
||||
{
|
||||
Navigator.of(context).pop(),
|
||||
onFinished(
|
||||
textEditingController.text)
|
||||
},
|
||||
}),
|
||||
() {
|
||||
if (_checkInput(context,
|
||||
textEditingController.text)) {
|
||||
final result = textEditingController.text;
|
||||
textEditingController.dispose();
|
||||
focusNode.dispose();
|
||||
Navigator.of(context).pop();
|
||||
onFinished(result);
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:stadtbote/model/chat_message.dart';
|
||||
import 'package:stadtbote/model/location_status.dart';
|
||||
import 'package:stadtbote/model/rank.dart';
|
||||
import 'package:stadtbote/model/status.dart';
|
||||
import 'package:stadtbote/model/tour_file.dart';
|
||||
import 'package:stadtbote/objectbox.g.dart';
|
||||
import 'package:stadtbote/network_messages/acknowledge_job.dart';
|
||||
import 'package:stadtbote/network_messages/acknowledge_message.dart';
|
||||
@@ -401,8 +402,16 @@ class MqttCore with ProducerMixin, ConsumerMixin {
|
||||
query.close();
|
||||
|
||||
if (existingJob != null) {
|
||||
// Update existing job
|
||||
// Update existing job - preserve the ID
|
||||
newJob.id = existingJob.id;
|
||||
|
||||
// Ensure files are updated even if the job already exists
|
||||
// This handles migration from old app versions without files support
|
||||
if (payloadData['files'] != null) {
|
||||
newJob.files = (payloadData['files'] as List<dynamic>)
|
||||
.map((e) => TourFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
box.put(newJob);
|
||||
@@ -754,7 +763,7 @@ class MqttCore with ProducerMixin, ConsumerMixin {
|
||||
|
||||
void _commandLogoutReceived(Map<String, dynamic> payload) {
|
||||
try {
|
||||
String timestamp = payload['timestamp'] ?? 'Unknown timestamp';
|
||||
String timestamp = payload['timestamp']?.toString() ?? 'Unknown timestamp';
|
||||
Util.logInfo('Command logout received with timestamp: $timestamp');
|
||||
|
||||
// Disconnect MQTT client gracefully
|
||||
|
||||
@@ -9,6 +9,7 @@ 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/objectbox.g.dart';
|
||||
import 'package:stadtbote/tools/constants.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -97,6 +98,28 @@ class Util {
|
||||
|
||||
final box = MyApp.objectBoxStore.box<LogEntry>();
|
||||
box.put(logEntry);
|
||||
|
||||
// Cleanup old logs (delete entries older than 3 days)
|
||||
_cleanupOldLogs();
|
||||
}
|
||||
|
||||
static void _cleanupOldLogs() async {
|
||||
try {
|
||||
final box = MyApp.objectBoxStore.box<LogEntry>();
|
||||
|
||||
// Delete logs older than 3 days
|
||||
final threeDaysAgo = DateTime.now().subtract(const Duration(days: 3));
|
||||
final oldLogsQuery = box.query(LogEntry_.time.lessThan(threeDaysAgo.millisecondsSinceEpoch)).build();
|
||||
final oldLogs = oldLogsQuery.find();
|
||||
oldLogsQuery.close();
|
||||
|
||||
if (oldLogs.isNotEmpty) {
|
||||
box.removeMany(oldLogs.map((e) => e.id).toList());
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail for cleanup - don't log to avoid infinite loop
|
||||
debugPrint('Log cleanup failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static logInfo(String message) {
|
||||
|
||||
Reference in new issue
Block a user