Files
2026-02-25 10:40:29 +01:00

396 lines
13 KiB
Dart

import 'package:dart_mq/dart_mq.dart';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:stadtbote/content_views/utils/content_view_widget.dart';
import 'package:stadtbote/objectbox.g.dart';
import 'package:intl/intl.dart';
import 'package:badges/badges.dart' as badges;
import 'package:stadtbote/messages/pop_content_view_message.dart';
import 'package:stadtbote/network_messages/accept_job_message.dart';
import 'package:stadtbote/network_messages/fin_task_message.dart';
import 'package:stadtbote/tools/messages/job_received_message.dart';
import 'package:stadtbote/tools/messages/publish_payload_message.dart';
import 'package:stadtbote/tools/messages/refresh_badges_message.dart';
import 'package:stadtbote/tools/messages/remove_job_message.dart';
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/app_localizations.dart';
import '../tools/constants.dart';
import '../tools/util.dart';
class AssignedJobsPage extends ContentViewWidget {
const AssignedJobsPage({super.key});
@override
State<AssignedJobsPage> createState() => _AssignedJobsPageState();
@override
Future<Widget> getHeaderWidget(BuildContext context) async {
await Future.delayed(const Duration(microseconds: 0));
return _HeaderWidget();
}
}
class _AssignedJobsPageState extends State<AssignedJobsPage> with ProducerMixin, ConsumerMixin {
late bool _initCompleted = false;
List<Job> _jobs = [];
final List<ExpandableController> _expandableControllers = [];
@override
void initState() {
Future.delayed(const Duration(seconds: 0), () async {
_jobs = await _fetchState0Jobs();
_initMessages();
_initCompleted = true;
setState(() {});
});
super.initState();
}
@override
void dispose() {
// Unsubscribe from all message topics
unsubscribe(queueId: Constants.messageTopicJobReceived);
// Dispose all expandable controllers
for (var controller in _expandableControllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_initCompleted) {
return Util.getWaitWidget(context);
}
var jobWidgets = _createWidgets();
return Padding(
padding: const EdgeInsets.all(8.0),
child: Scrollbar(
thumbVisibility: true,
child: ListView(
children: jobWidgets
),
),
);
}
Future<List<Job>> _fetchState0Jobs() async {
final box = MyApp.objectBoxStore.box<Job>();
final query = box.query(Job_.state.equals(Constants.stateAssigned)).build();
final jobs = query.find();
query.close();
return jobs;
}
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(
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: true,
badgeContent: Text(
'new'.tr,
style: const TextStyle(
color: Colors.white,
fontSize: 18.0,
),
),
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: Stack(
children: [
Column(
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,
children: [
Image.asset("assets/${mapVehicleTypeToImageName(job.vhtId)}", width: 60, height: 60),
const SizedBox(width: 10),
Text(
mapVehicleTypeToText(job.vhtId)
),
],
),
Text('${'order_number'.tr}: ${job.externalId}'),
Text(job.tours[0].comp),
Text(job.tours[0].street),
Text(job.tours[0].city),
const SizedBox(height: 20),
ScrollOnExpand(
scrollOnExpand: true,
scrollOnCollapse: false,
child: ExpandablePanel(
controller: expandableController,
theme: const ExpandableThemeData(
headerAlignment: ExpandablePanelHeaderAlignment.center,
tapBodyToCollapse: true,
),
header: Padding(
padding: const EdgeInsets.all(0),
child: Text(
'more_info'.tr,
style: const TextStyle(fontSize: 15, color: Colors.black, fontWeight: FontWeight.bold),
)),
collapsed: const SizedBox(),
expanded: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: const Color(0xfffff4e4),
border: Border.all(
color: Colors.grey,
width: 0.5,
),
),
child: Text(job.rem),
),
],
),
),
),
const SizedBox(height: 20),
Row(children: [
Expanded(
child: getDefaultButton(!job.global ? 'decline'.tr : 'ignore'.tr, () {
sendMessage(
routingKey: Constants.messageTopicRemoveJob,
payload: RemoveJobMessage(id: job.externalId),
);
_sendAnswer(job, !job.global ? 2 : 3);
_jobs.removeWhere((databaseJob) => databaseJob.id == job.id);
setState(() {});
}),
),
const SizedBox(width: 20),
Expanded(
child: getDefaultButton('accept'.tr, () {
sendMessage(
routingKey: Constants.messageTopicRemoveJob,
payload: RemoveJobMessage(id: job.externalId),
);
_sendAnswer(job, 1);
_jobs.removeWhere((databaseJob) => databaseJob.id == job.id);
setState(() {});
}),
)
])
],
),
if (job.hasFiles)
Positioned(
top: 0,
right: 0,
child: FilesBadge(files: job.files),
),
],
),
),
),
),
),
));
}
return widgets;
}
_sendAnswer(Job job, int item) async {
var currentPosition = await MyApp.getCurrentPosition();
var message = Reaction();
message.id = job.externalId; // Use external ID for network communication
message.ver = job.ver.toString();
message.item = item;
message.track = Track();
message.track.time = Util.nowUnixSecondsUtc();
message.track.pos = FinTaskPos();
message.track.pos!.time = currentPosition != null ? Util.toUnixSecondsUtc(currentPosition.timestamp) : 0;
message.track.pos!.lat = currentPosition != null ? currentPosition.latitude : 0.0;
message.track.pos!.long = currentPosition != null ? currentPosition.longitude : 0.0;
message.track.pos!.acc = currentPosition != null ? currentPosition.accuracy.toInt() : 0;
sendMessage(
routingKey: Constants.messageTopicRemoveState0JobFromMap,
payload: RemoveState0JobFromMapMessage(jobId: job.externalId), // Use external ID
);
sendMessage(
routingKey: Constants.messageTopicPublishPayload,
payload: PublishPayloadMessage(topic: "reaction", message: message.toJson(), acknowledgeRequired: true),
);
sendMessage(
routingKey: Constants.messageTopicUpdateBadges,
payload: RefreshBadgesMessage(),
);
}
void _initMessages() {
subscribe(
queueId: Constants.messageTopicJobReceived,
callback: (message) {
var job = (message.payload as JobReceivedMessage).job;
if (job.state == Constants.stateAssigned) {
_jobs.add(job);
if (mounted) {
setState(() {});
}
}
},
);
}
}
class _HeaderWidget extends StatefulWidget {
@override
State<_HeaderWidget> createState() => _HeaderWidgetState();
}
class _HeaderWidgetState extends State<_HeaderWidget> with ProducerMixin {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
var cellWidth = (MediaQuery.of(context).size.width - 60 - 60 - 25) / 2;
return Padding(
padding: const EdgeInsets.all(8),
child: Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Visibility(
visible: true,
child: Container(
width: 60,
height: 60,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
child: FittedBox(
child: GestureDetector(
onTap: () async {
sendMessage(
routingKey: Constants.messageTopicPopContentView,
payload: PopContentViewMessage(count: 1),
);
},
child: const CircleAvatar(
radius: 10,
backgroundColor: Constants.colorPrimary,
child: Padding(padding: EdgeInsets.all(1.0), child: Icon(Icons.arrow_back, size: 20.0, color: Colors.white)),
),
),
),
),
),
Visibility(
visible: false,
child: Container(
width: 60,
height: 60,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
child: FittedBox(
child: GestureDetector(
onTap: () async {
sendMessage(
routingKey: Constants.messageTopicPopContentView,
payload: PopContentViewMessage(count: 1),
);
},
child: const CircleAvatar(
backgroundColor: Constants.colorPrimary,
radius: 10,
child: Padding(padding: EdgeInsets.all(1.0), child: Icon(Icons.info_outline, size: 20.0, color: Colors.white))),
),
),
),
),
],
),
SizedBox(
height: 100,
width: cellWidth * 2,
child: Center(
child: Text(
textAlign: TextAlign.center,
'assigned_jobs'.tr,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.white),
),
),
),
],
),
);
}
}